KĂŒrzlich hatten wir bei Liip die Gelegenheit, mit der Bobst SA zusammenzuarbeiten. Das Unternehmen verkauft GerĂ€te, die authentifiziert werden mĂŒssen, und wollte seinen aktuellen Prozess verbessern, bei dem die Kunden die GerĂ€te mit einem speziellen Tool scannen und manuell eine Referenz im Webportal von Bobst angeben mĂŒssen. Beim neuen Verfahren werden jetzt RFID-Tags verwendet. Unser Ziel bestand darin, eine mobile App zu entwickeln, mit der diese Tags gescannt werden können und die sich automatisch mit dem Konto des Benutzers auf den Servern von Bobst verbindet, um diesem das richtige Zertifikat zuzuweisen. Mit der App kann Bobst dieses Zertifikat dann auf das GerĂ€t schreiben.

Technologien

Wie bei den meisten Apps, die wir bei Liip entwickeln, haben wir uns auch bei dieser dafĂŒr entschieden, beide Plattformen mit nativen Technologien zu entwickeln – die Android-App mit Kotlin (Kotlin Version 1.4.10, Ziel Android 11, API 30) und die iOS-App mit Swift (Swift 5, Ziel iOS 14).

Zumeist bevorzugen wir diese Technologien gegenĂŒber plattformĂŒbergreifenden Alternativen aus verschiedenen GrĂŒnden, unter anderem wegen der Leistung, des zuverlĂ€ssigen UI-Erlebnisses fĂŒr Benutzer beider Plattformen und der Wartbarkeit. In diesem Fall ist uns die Wahl sogar noch einfacher gefallen, da die NFC-Funktionen Teil der integrierten Systembibliotheken sind. Eine plattformĂŒbergreifende App wĂŒrde daher sowieso ÜberbrĂŒckungsmodule fĂŒr beide Plattformen erfordern.

RFID Standards

RFID steht fĂŒr Radiofrequenz-Identifikation, und einfache RFID-Tags benötigen nicht einmal eine Stromversorgung. RFID-GerĂ€te können nach verschiedenen Standards implementiert werden. Zu diesen gehört beispielsweise die ISO 15693-3, die die Tags von Bobst nutzen.

NFC oder Near Field Communication, st eine prĂ€zisere Untergruppe von RadiofrequenzgerĂ€ten. NFC-GerĂ€te können sowohl Tags als auch Reader sein, und sie können aktiv miteinander kommunizieren. Moderne Smartphones sind fast alle NFC-fĂ€hig. NFC-GerĂ€te kommunizieren standardmĂ€ssig ĂŒber NDEF (NFC Data Exchange Format), bei der Kommunikation mit RFID-ISO-Standards muss jedoch ein anderes Format verwendet werden, zum Beispiel: NFC Typ 2 (auch bekannt als Typ A) und Typ 4 (auch bekannt als Typ B) werden fĂŒr die Kommunikation mit dem RFID-ISO-Standard 14443 verwendet
.
In unserem Fall werden wir fĂŒr die Kommunikation mit unserem ISO 15693-Tag den NFC-Typ 5 (auch bekannt als Typ V) verwenden.

NFC-UnterstĂŒtzung fĂŒr Android seit Mai 2021

Typ Beschreibung
NfcA Bietet Zugriff auf NFC-A-Eigenschaften (ISO 14443-3A) und E/A-VorgÀnge.
NfcB Bietet Zugriff auf NFC-B-Eigenschaften (ISO 14443-3B) und E/A-VorgÀnge.
NfcF Bietet Zugriff auf NFC-F-Eigenschaften (JIS 6319-4) und E/A-VorgÀnge.
NfcV Bietet Zugriff auf NFC-V-Eigenschaften (ISO 15693) und E/A-VorgÀnge.
IsoDep Bietet Zugriff auf ISO-DEP-Eigenschaften (ISO 14443-4) und E/A-VorgÀnge.
Ndef Bietet Zugriff auf NDEF-Daten und VorgÀnge auf NFC-Tags, die als NDEF formatiert wurden.
NdefFormatable Bietet einen Formatierungsvorgang fĂŒr Tags, die NDEF-formatierbar sein können.
MifareClassic Bietet Zugriff auf MIFARE-Classic-Eigenschaften und E/A-VorgĂ€nge. ⚠ Optional: Es gibt keine Garantie dafĂŒr, dass ein Android-Smartphone mit NFC diesen Typ unterstĂŒtzt.
MifareUltralight Bietet Zugriff auf MIFARE-Ultralight-Eigenschaften und E/A-VorgĂ€nge. ⚠ Optional: Es gibt keine Garantie dafĂŒr, dass ein Android-Smartphone mit NFC diesen Typ unterstĂŒtzt.

NFC-UnterstĂŒtzung fĂŒr iOS seit Mai 2021

Typ Beschreibung
NFCISO7816Tag Ein Interface zur Interaktion mit einem ISO 7816-Tag.
NFCISO15693Tag Ein Interface zur Interaktion mit einem ISO 15693-Tag.
NFCFeliCaTag Ein Interface zur Interaktion mit einem FeliCaℱ-Tag.
NFCMiFareTag Ein Interface zur Interaktion mit einem MIFAREÂź-Tag.
NFCNDEFTag Ein Interface zur Interaktion mit einem NDEF-Tag.

Implementierung auf beiden Plattformen

In den obigen Tabellen ist ersichtlich, dass sowohl Android als auch iOS die RFID-Chips von Bobst unterstĂŒtzen. Wir mussten fĂŒr Android den NfcV -Typ und fĂŒr iOS den NFCISO15693Tag -Typ verwenden.

FĂŒr beide Systeme musste zudem die entsprechende Berechtigung fĂŒr den Zugriff auf die NFC-Funktionen erteilt werden, aber sowohl Android- als auch iOS-Dokumente decken diesen Bereich im Detail ab.

Mit anderen Worten: Unsere Implementierung musste drei VorgĂ€nge unterstĂŒtzen:

  • ein Tag in Reichweite identifizieren
  • den ihalt des Tags lesen
  • neuen Inhalt auf das Tag schreiben

Wir haben diese VorgÀnge wie folgt verwendet:

  1. Identifizieren und lesen: Abwarten, bis ein Tag identifiziert ist (das System erhÀlt dessen individuelle ID), und dann mit dieser individuellen ID einen Lesevorgang anfordern.
  2. Das Ergebnis des Lesevorgangs an den Server von Bobst senden, um ein Zertifikat zu erhalten (hier kein RFID-Vorgang).
  3. Identifizieren und schreiben: Ein schliessendes Tag identifizieren und sicherstellen, dass es die gleiche ID hat wie das zuvor gelesene (es ist dasselbe Tag). Danach das Zertifikat darauf schreiben.

Schritt 1: Ein Tag identifizieren

Mit beiden Systemen kann die ID eines in der NĂ€he befindlichen Tags auf einfache Weise ĂŒbermittelt werden. Sie unterscheiden sich jedoch leicht:

Bei iOS muss das System aufgefordert werden, die Identifizierung von RFID-Tags zu starten. An dieser Stelle wird eine kleine modale Ansicht angezeigt und das System sucht nach einem Tag. GrundsÀtzlich reichen die folgenden Angaben aus:

func startScan() {
    guard  NFCTagReaderSession.readingAvailable else {
        delegate?.onScanningNotSupported()
        return
    }
    nfcSession = NFCTagReaderSession(pollingOption: NFCTagReaderSession.PollingOption.iso15693, delegate: self, queue: **nil**)
    nfcSession?.alertMessage = ... // whatever alert message such as "hold the phone near the tag"
    nfcSession?.begin()
}

func stopScan() {
    nfcSession?.invalidate()
}

Bei Android wird das System ĂŒber den Android-Intent-Mechanismus gesteuert. Das bedeutet, dass man die App registrieren muss, damit sie Scan-Intents des entsprechenden NFC-Typs akzeptiert. Danach erhĂ€lt die App eine entsprechende Meldung. Aus diesem Grund gibt es keine Starttaste. Wenn sich ein Tag in Reichweite befindet, wird die App, selbst wenn sie geschlossen ist, geöffnet und umgehend benachrichtigt!

Die NFC-FÀhigkeit im Verzeichnis der AktivitÀt registrieren, welche die Informationen empfangen soll:

<intent-filter>
    <action android:name="android.nfc.action.TECH_DISCOVERED" />
</intent-filter>
<meta-data
    android:name="android.nfc.action.TECH_DISCOVERED"
    android:resource="@xml/filter_nfc"
/>

Die Filterliste wird in die Ressourcendatei eingefĂŒgt, auf die wir im Verzeichnis verwiesen haben, z. B. xml/filter_nfc/xml (in unserem Fall verwenden wir einfach den Typ NfcV, wie in der Tabelle oben):

<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
    <tech-list>
        <tech>android.nfc.tech.NfcV</tech>
    </tech-list>
</resources>

Our activity will be notified via onNewIntent:

override fun onNewIntent(intent: Intent) {
  super.onNewIntent(intent)

  if (NfcAdapter.ACTION_TECH_DISCOVERED == intent.action) {
    val tag = intent.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG)
    viewModel.scanned(tag)  // Or whatever else
  }
}

Die beiden Plattformen unterscheiden sich deutlich in der Bedienung, funktionieren aber dennoch auf folgende Weise: Die obigen Schritte ergeben lediglich ein Tag-Objekt, das die ID des erkannten Tags enthĂ€lt. Nun mĂŒssen wir damit lesen oder schreiben, was wir brauchen.

Schritt 2: Ein Tag lesen

Bei iOS beinhaltet das System bereits eine Basisimplementierung der ISO 15693, was alles einfacher macht.

Die konkrete Implementierung hÀngt von den spezifischen Anforderungen ab. GrundsÀtzlich besteht die Idee aber darin, die generische Tag-ID in die richtige ISO-Implementierung, welche in die Core-NFC-Bibliothek integriert ist, zu konvertieren:

if case let NFCTag.iso15693(tagType5) = tag {
}

Und dann integrierte Funktionen wie tag.readMultipleBlocks() zu verwenden. Diese Funktion akzeptiert eine Reihe von Argumenten, einschliesslich hilfreicher Request-Flags.

tag.readMultipleBlocks(requestFlags: [.highDataRate], blockRange: NSRange(location: 0, length: NfcService.MAXREADBLOCK)) { data, error in
... // check for error and get data using the variables above
}

An dieser Stelle besteht die einzige Schwierigkeit darin, herauszufinden, wie die Bytes angeordnet wurden. Bobst hat uns eine umfassende Dokumentation zur VerfĂŒgung gestellt, damit wir ihre Tags richtig lesen können. In unserem Fall waren sie in der Regel in Viererblöcken, die nach dem Lesen jeweils einzeln umgedreht werden mussten, angeordnet.

Bei Android bietet das System keine integrierte Implementierung der ISO-Standard-Appart-Funktionen zum Senden von unbearbeiteten Byte-Blöcken. Diese Rohblöcke beinhalten eine Befehlsanfrage fĂŒr das Tag, gegliedert nach ISO 15693 https://www.iso.org/standard/73602.html.

Der erste Schritt Àhnelt dem bei iOS: Ermittlung des unserem Tag entsprechenden Systemtyps

val nfcVTag = NfcV.get(tag) ?: return

Der nÀchste Teil ist allerdings ein bisschen komplizierter. Das 70 Seiten umfassende Dokument zur Beschreibung der ISO 15693 zeigt, dass unter allen möglichen Befehlen die von iOS integrierten readMultipleBlocks tatsÀchlich durch Senden des Befehlscodes 0x23 ausgegeben werden.

Darin wird zudem mitgeteilt, dass zum Senden eines Befehls folgende Informationen erforderlich sind:
— Flags
— Befehlscode
— Felder fĂŒr obligatorische und optionale Parameter, je nach Befehl

In unserem Fall mĂŒssen die Flags so gesetzt werden, dass ein «adressierter» Befehl ausgegeben wird, d. h. der Befehl nimmt die Tag-ID als Argument und wird nur auf diesem bestimmten Tag ausgefĂŒhrt. GemĂ€ss Dokumentation entspricht dieses Flag dem sechsten Bit des Flag-Bytes. Zudem wird das High-Rate-Flag (zweites Bit) gesetzt. Unser gesamtes Flag-Byte ist 00100010, was 0x22 entspricht.

Folglich besteht unsere Android-Anfrage aus folgendem Byte-Array:

val offset = 0 // offset of first block to read
val blocks = 32 // number of blocks to read
val cmd = mutableListOf<Byte>().apply {
  add(0x22.toByte()) // flags: addressed (= UID field present) + high data rate
  add(0x23.toByte()) // command: READ MULTIPLE BLOCKS
  addAll(tag.id.toList()) // tag UID
  add((offset and 0x0ff).toByte()) // first block number. add 0xff to ensure we send two bytes
  add((blocks - 1 and 0x0ff).toByte()) // number of blocks (-1 as 0x00 means one block). add 0xff to ensure we send two bytes
}.toByteArray()

Wir empfangen einfach die Antwort mit der transceive -Methode und ĂŒberprĂŒfen das erste Byte zur Sicherstellung des Erfolgs.

nfcVTag.connect()
val responseBytes = nfcVTag.transceive(cmd)
if (responseBytes.first() != 0x00.toByte()) {
  return NfcResponse.Error.Read
}
nfcVTag.close()

Das Response-Byte-Array konnte nun gemÀss der Bobst-Dokumentation geparst werden, um die richtigen Tag-Informationen zu extrahieren.

Schritt 3: Auf ein Tag schreiben

Das Schreiben auf ein Tag ist ungefÀhr derselbe Prozess wie das Lesen (d. h. ein einfacher bei iOS und ein komplexerer bei Android).

Bei iOS stellen wir alle Informationen zum Schreiben in einem Byte-Array zusammen. Leider waren die MehrfachschreibvorgĂ€nge bei den Bobst-Tags deaktiviert, weshalb wir die Bytes einzeln senden mussten. Wir haben einfach die NFC-Core-Funktion transceive in einem Loop verwendet. Die Funktion ĂŒbernimmt wiederum alle notwendigen Flags

tag.writeSingleBlock(requestFlags: [.highDataRate, .address], blockNumber: UInt8(startBlock), dataBlock: dataBlock) { error in
    ... // Simply check for error and react accordingly
}

Bei Android
Wie zuvor mĂŒssen wir den entsprechenden Byte-Befehl senden und die transceive-Funktion verwenden. Wie zuvor nutzt dieser Befehl einen Flag-Wert von 0x22. GemĂ€ss der ISO 15693-Dokumentation wird die writeSingleBlock-Anfrage mit der Eingabe von 0x21 als Befehlsbyte gestellt. Wie zuvor mĂŒssen wir die Tag-ID eingeben, da wir einen «adressierten» Befehl ausfĂŒhren. Dann können wir die ID-Bytes und anschliessend alle Daten, die wir schreiben wollen, eingeben.

private fun createCommand(tag: Tag, blockOffset: Int, blockData: List<Byte>) = mutableListOf<Byte>().apply {
  add(0x22.toByte()) // flags: addressed (= UID field present) + high data rate : 00100010
  add(0x21.toByte()) // command: WRITE SINGLE BLOCK (multi read not supported)
  addAll(tag.id.toList()) // tag UID
  add((blockOffset and 0x0ff).toByte()) // first block number. add 0xff to ensure we send two bytes
  addAll(blockData) // The bytes for certificate and reference,
}.toByteArray()

Wie bei iOS mĂŒssen wir diese Funktion in einem Loop aufrufen und dabei den richtigen Block-Offset eingeben, um alle Blöcke nacheinander zu schreiben.

Schlussfolgerung

Die Benutzererfahrung unterscheidet sich zwischen Android und iOS zwar nur geringfĂŒgig, aber das Lesen und Schreiben auf ISO 15693-Tags ist bei Android deutlich komplexer.

Solche Divergenzen beobachten wir oft zwischen den beiden Plattformen: In der Regel bietet das iOS-Framework eine einfache Erfahrung fĂŒr Entwickler, weil es fĂŒr viele Funktionen Standardimplementierungen bereitstellt. Android hingegen lĂ€sst oft mehr Freiheiten bei der Implementierung, was mit zusĂ€tzlichem Aufwand zum Erreichen des gleichen Ergebnisses einhergeht.