Skip to main content

Working with NFC in Flutter

· 11 min read

Enter our career fair project

For the latest career fair at MatFyz, we wanted to build something interesting: from one side, it should be more than just "here is our merch, come work with us"; from another side, it should showcase the products we were working on at Mews.

Since one of our products is a kiosk for self-service check-ins at hotels, we decided to emulate a hotel check-in experience with guests entering their details into a registration card, cutting their "key" with an emulated key cutter, and later taking part in a lottery with randomly selecting winning room numbers.

What is a key cutter?

If you google "key cutter" images, it will show you something like this:

This is definitely not the device hotel guests are supposed to use to check into their rooms.

A hotel key cutter is basically an RFID-card encoder: an electronic device that looks like a payment terminal. During "traditional" check-in, a receptionist takes an empty RFID-card and uses this device to write some information on the card. Later, the room door lock reads this information and opens the door for you. This process of "cutting" the key card is usually not visible, often cards are prepared in advance before check-in.

When using the self-service kiosk, you can do this on your own: following instructions on the screen, you take a new RFID-card from the pile and place it on the key cutter:

There can be more complex devices (e.g., with automatic card dispensers), but we will emulate exactly this type, which is pretty popular.

How did we go about it?

This article will be about this "key cutting" process. At first, we wanted to set up a real key cutter, but there are a lot of technical and legal problems associated with them, so we decided to create a simple mobile app that will write a room number on an NFC-card. Later, a user can just come up with this card so that we can read their room number and match it with the winning numbers.

Let's talk about this overall process of key cutting and how it works in production for a real hotel kiosk:

In our setup, Kiosk doesn't communicate directly with Key Cutter. Instead, when the guest presses the "Cut key" button, Kiosk issues a command to our Web Server, and Server creates a command in the special queue. In the hotel, there is a separate desktop application (Connector) that listens to this queue and communicates with the hardware Key Cutter. When the key is cut, Key Cutter notifies Connector, and Connector sends the confirmation back to Server. After that, Server notifies Kiosk via Websocket that the key is ready.

It sounds like a complex setup (and it is), but it allows us to completely decouple Kiosk from Key Cutters – and since Kiosk is not the only client that deals with Key Cutters (and other peripherals), we keep all the knowledge on how to work with peripherals in one place.

In our "emulated" version, both the Key Cutter and Connector roles are fulfilled by the mobile app. The good thing is that Kiosk already uses our public API, the same one that is used by the Connector desktop app. That means that we already have the API implementation in Dart and can reuse it for our "key cutter".

So the emulated workflow looks like this:

In fact, we're just merging two entities, Key Cutter and Connector, into one mobile app. Kiosk still communicates with Server, but all the logic of receiving the key cutting command and "cutting" the key now happens in the custom app.

Now let’s discuss this app in detail. For that, we will need to know more about NFC.

What is NFC?

Let's start with another question: "What is RFID?"

Beginning NFC by Tom Igoe, Don Coleman, Brian Jepson

Imagine you’re sitting on your porch at night. You turn on the porch light, and you can see your neighbor as he passes close to your house because the light reflects off him back to your eyes. That’s passive RFID. [...]

Now imagine you turn on your porch light, and your neighbor in his home sees it and flicks on his porch light so that you can see him waving hello from his porch. That’s active RFID. [...]

RFID is a lot like those two porches. You and your neighbor know each other’s faces, but you don’t really learn a lot about each other that way. You don’t exchange any meaningful messages. RFID is not a communications technology; rather, it’s designed for identification. RFID tags can hold a small amount of data, and you can read and write to them from RFID readers, but the amount of data we’re talking about is trivial, a thousand bytes or less.

RFID and NFC are often conflated, but they’re not the same thing. Though NFC readers can read from and write to some RFID tags, NFC has more capabilities than RFID, and enables a greater range of uses. You can think of NFC as an extension of RFID, building on a few of the many RFID standards to create a wider data exchange platform.

So, what is NFC?

Near Field Communication (NFC) is a contact-less communication technology based on a radio frequency (RF) field using a base frequency of 13.56 MHz. It is designed to exchange data between two devices through a simple touch gesture.

RFIDNFC
Uses radio frequencyUses radio frequency
Identification technologyCommunication technology
No single standardStandardized

Different standards

We decided to go with the nfc_manager plugin. It's a wrapper around platform specific NFC (or rather RFID) capabilities.

It provides the following platform-tag-classes:

TypeAndroidiOS
Ndef
FeliCa
Iso7816
Iso15693
MiFare
NfcA
NfcB
NfcF
NfcV
IsoDep
MifareClassic
MifareUltralight
NdefFormatable

As you can see, the only type that is available on both platforms is NDEF. And that is expectable, as this is the standardized format for NFC Forum Tags.

NFC Forum Tags are contact-less memory cards hosting a so called NDEF message (NDEF is standing for NFC Data Exchange Format) defined by an NFC Forum Specification. NFC Forum has currently defined five different NFC Forum Tag types to allow the usage of many different existing memory card implementations as NFC Forum Tags. These different NFC Forum Tag types differ by the underlying communication protocol and data structure to store NDEF messages but the resulting overall behavior of NFC Forum Tags is identical.

That means if we want to go cross-platform, we should use NDEF-compatible cards. This can be a problem on its own – when we were looking for the cards for our project, we failed to find them.

At least we managed to find Mifare Desfire EV1 which can be formatted to NDEF format, but it can only be done with Android. So it's either formatting the cards in advance and then using any mobile phone for writing NDEF messages, or using Android for writing the data (later both iPhone and Android can be used for reading the data).

We decided to choose the second way, as Android has some other advantages for serving as an improvised key cutter.

iOS vs Android – UX, custom behavior

If you have an NDEF-formatted card, you can read and write using both Android and iPhone. But we didn't need a "B2C"-style app, we were going to create a key cutter emulator. And for this, Android is a better choice:

  • iOS supports NDEF-formatted cards, while Android can also operate on so-called NDEF-formattable cards, formatting them and writing NDEF messages (after that it can be read by iOS as well);
  • iOS displays a system dialog when initiating an NFC session. Android allows you to do the scanning in the background, providing a clearer experience;
  • there's one advantage in iOS devices though, at least among the devices I've tested. The NFC-reader is more sensitive and can read/write data when you touch the front side of the device.

Implementation

The "key cutting" logic is pretty simple: we're just connecting to the server and waiting for the command. After the command is received, we fetch additional data (since the command itself contains only the room number, we need to fetch the reservation details) and validate them (it should point to the correct active reservation). If the data are valid, we start an NFC writing session: enter discovering tag mode, and write the data (room number in our case) after the tag is discovered (aka NFC-card bumped to the phone). After that, we return to the "waiting" state, and the process repeats.

This state diagram can be easily mapped to the BLoC code, so let's just take a look at the NFC writing part:

Future<void> _writeNumber(String number) {
final completer = Completer<void>();

NfcManager.instance.startSession(
onDiscovered: (tag) async {
final ndef = Ndef.from(tag);
final formattable = NdefFormatable.from(tag);

final message = NdefMessage([NdefRecord.createText(number)]);
if (ndef != null) {
await ndef.write(message);
} else if (formattable != null) {
await formattable.format(message);
}
await NfcManager.instance.stopSession();
completer.complete();
},
onError: (error) async => completer.completeError(error),
);

return completer.future;
}

As you see, we're starting an NFC session, and passing it a callback as onDiscovered parameter – it will be triggered once an NFC trigger detects a tag nearby. As I mentioned earlier, in Android we can work with either NDEF or NDEF formattable tags, so if the tag of the correct type was bumped, we just write the prepared message there (which contains just the the room number in our case) and close the session.

The code for the reading part of the app is pretty straightforward as well. I will show the complete source code for the NfcReaderBloc, but the most interesting part is the _onTagDiscovered method:

typedef _Event = NfcReaderEvent;
typedef _State = NfcReaderState;
typedef _EventHandler = EventHandler<_Event, _State>;
typedef _Emitter = Emitter<_State>;

class NfcReaderBloc extends Bloc<_Event, _State> {
NfcReaderBloc() : super(const Waiting()) {
on<_Event>(_eventHandler, transformer: droppable());

NfcManager.instance.startSession(
onDiscovered: (tag) async => add(TagDiscovered(tag)),
onError: (e) async => add(Failed(e)),
);
}

_EventHandler get _eventHandler => (event, emit) => event.map(
tagDiscovered: (event) => _onTagDiscovered(event, emit),
failed: (event) => _onFailed(event, emit),
);

Future<void> _onTagDiscovered(TagDiscovered event, _Emitter emit) async {
try {
final ndef = Ndef.from(event.tag);
if (ndef == null) throw Exception('No NDEF tag');

final data = await ndef.read();

emit(Success(utf8.decode(data.records.first.payload).substring(3)));
} on Object {
emit(const Failure());
}

await Future<void>.delayed(const Duration(seconds: 3));
emit(const Waiting());
}

Future<void> _onFailed(Failed _, _Emitter emit) async {
emit(const Failure());
await Future<void>.delayed(const Duration(seconds: 3));
emit(const Waiting());
}


Future<void> close() {
NfcManager.instance.stopSession();

return super.close();
}
}


class NfcReaderEvent with _$NfcReaderEvent {
const factory NfcReaderEvent.tagDiscovered(NfcTag tag) = TagDiscovered;
const factory NfcReaderEvent.failed(NfcError e) = Failed;
}


class NfcReaderState with _$NfcReaderState {
const factory NfcReaderState.waiting() = Waiting;
const factory NfcReaderState.success(String number) = Success;
const factory NfcReaderState.failure() = Failure;
}

As you see, when the tag is discovered, we just read the first record there (assuming that we read the NDEF formatted tag) and emit it as a new state. The only trick here is to skip the first three bytes of the payload. This is needed because, according to the NDEF specification, the first byte in the payload is reserved to store the length of the language code (N), then goes N bytes to store the language code itself, and then the actual record content (our message). In our case, we only use EN language (two bytes for the code), so we can hard-code to skip the three reserved bytes.

Demo


The result was pretty good and we received a lot of positive feedback, so we decided to reuse this idea for WebExpo 2022, but that's a different story.