adafruit / circuitpython

CircuitPython - a Python implementation for teaching coding with microcontrollers
https://circuitpython.org
MIT License
3.96k stars 1.16k forks source link

NFC Tag (NFCT) Support for nRF52 Devices #3379

Open nitz opened 3 years ago

nitz commented 3 years ago

Hello! Long time listener, first time caller.

I'm looking to add support to CircuitPython for the NFC tag functionality of the nRF52 devices. I've been using the functionality for a short while now in a personal project so I feel like I've got a fairly good understanding of how to bring it over to CP, at least in the sense of type 2 tags. Before I started, I wanted to flesh out a few details and ask some questions so that I can contribute in the best way possible.

Before I get to my questions, I want to touch on the current state of NFCT in the nRF5/nrfx SDK:

Current NFCT State

It looks like it was discussed and somewhat proposed to add some NFCT functionality to micropython, but that effort seems to have stalled, due to the use of the closed-sourced (but permissively licenced) T2T/T4T libraries. It doesn't sound like Nordic/ (or Telit, as the license references) intends to open source them any time soon.

The closed source nature of a library like that certainly raises valid concerns, especially if you're going to be using it to transfer any sort of sensitive information. It's for this reason I wanted to bring it up and see what you all thought of it before starting. If that seems like a non or acceptable issue, then leveraging those libraries are certainly going to be the quickest and most efficient way to implement NFCT functionality.

My NFCT Experience

Since I was looking to implement out of band behavior for the NFCT peripheral, my personal project didn't make use of those libraries. I took a different approach, reading different datasheets of NFC tag devices, and implementing behavior based on how I read their behavior to present. As I was dealing with type 2 tags, in particular, I was using mostly the NXP NTAG213/215/216 and MIFARE Ultralight C datasheets. As I haven't ponied up the money (yet) for the NFC Forum Type 2 Tag Specification or the ISO 14443A Specification, the 14443-A and Type 2 Tag information I've used has been limited to what I was able to reverse engineer using physical NTAG215s, and googling around. The same goes for the NDEF and TLV, though lady ada helped out there too, from seemingly a time before the NFC Forum charged for that specification as well.

All of that is to say, I'm fairly confident I can implement at least a Type 2 Tag style emulation, forgoing the use of the Telit blob if need be, but it is mildly "blind", as I don't own the specifications themselves.

Implementation Questions

Moving on to the actual nuts and bolts of actually implementing this functionality: As I mentioned before, I've not yet contributed to CircuitPython, but am eager to go about this the correct way. I see several paths forward, and want to make sure I choose the one that is the most useful for the project as a whole, as well as those who may want to use the functionality. So please point me at the way you think I should move forward here.

My belief is that the NFCT functionality should theoretically exist in two parts: one part as the actual HAL to the peripheral, and should perhaps live in something like it's own driver in circuitpython/ports/nrf/common-hal/, at least for the nRF device.

In parallel to the HAL, the NFC Tag emulation itself should exist higher in CP (something along the lines of shared-module or shared-bindings (though I don't quite understand the difference between those). This would allow future, non-nRF devices to share the common API for interfacing as an NFC Tag. This would also prompt the question of where the actual NFC Tag emulation driver would exist: if we decided to make use of the Nordic provided blobs (at least for now), it's clear they are hardware specific, though they don't fit the definition (at least in my view) of something that would belong in the HAL. Which leads into my next thought:

Now it also seems reasonable that the NFC Tag functionality could exist as a standalone driver like the ones you include in the bundles. However with my quick browsing through some of those, they seem to be pure python implementations, so I'm not sure where a (C/C++ based) HAL would live there.

Closing Thoughts

The NFCT functionality of the nRF platform is something I've definitely seen folks ask for, and think the potential for it could be wonderful. Especially with devices like makerdiary's upcoming CircuitPython based M60 Keyboard, I think now is a fantastic time to add this soft of functionality to CircuitPython. I hope this starts a good discussion and we can get the ball rolling! Thanks for reading all of my drivel!

tannewt commented 3 years ago

Are you reading tags or acting like a tag?

I think you are on the right track with shared-bindings and common-hal. It'd be great to have an nfcio in shared-bindings to use across ports.

The license doesn't worry me as long as its use is kept to the nrf port because of the license exception for it.

The #circuitpython channel on the Adafruit Discord is the best place to get help.

nitz commented 3 years ago

Acting like a tag. The peripheral in the nRF52 platform is actually called "NFCT", since it only behaves as the NFC Tag side. Updated the title to more clearly call attention to this! So in other words, this effort wouldn't overlap with parts like the PN532 pretty much at all (for now, at least.)

I agree: an nfcio interface makes a lot of sense to me, especially as I hope the NFC tag (and reader, for that matter) continue to catch on. I'll stub up a proposed interface there to get a little feedback on what looks like it would be acceptable in terms of the actual API, as well as the type of functionality that should be exposed. From there, that'll give me a good idea of how to finish laying out the layers in common-hal.

Excellent on the license front. Ideally it's just a means to ends at this point too: if other platforms do start picking up NFC Tag peripherals, the library (at least from my use of it) should be droppable as we implement similar functionality!

nitz commented 3 years ago

Since I have the most experience with it, it makes sense for me to tackle Type 2 Tags first, but the nRF libraries do have Type 4 Tag support too, so I'm designing with that in mind.

One thing I'm definitely curious about is memory management. Because the API that renders the NDEF messages requires the message to be defined with the number of records it has, I was planning on collecting up the records in a python object, then rendering them as the raw buffer that is used as the actual payload when it's set. I worry that might be a little double dipping. I'll start with a theoretical example, then explain from there.

Hypothetical Use Example

import nfcio

tag_payload_length = 128 # something large enough for our payload. needs to be the size of the records, plus the overhead of the TLV structures, etc.
tag = nfcio.Type2Tag(tag_payload_length)

hello_world = nfcio.NDEFMessage()
hello_world.add_uri_record("https://adafruit.com/")
hello_world.add_text_record("Hello, circuitpython!", "en")
hello_world.add_text_record("Hola, circuito de pitón!", "es")

tag.set_payload(hello_world)
# could cleanup hello_world now, as the tag is holding the rendered payload,
# if not, that memory will still be in use

tag.start_emulation()

# ... and so on

The nfcio Module (shared-bindings)

These are what I'm thinking to start with as the actual python-visible classes and functions. I'm using busio as template of sorts, so please point out things I might have missed or handled in a non-standard way!

class NDEFAppRecordType(Enum):
    ANDROID = 0
    WINDOWS_PHONE = 1

class NDEFMessage: # A class representing an NDEF message, made of NDEF Records
    records = []
    def __init__(self) # create a NDEF message.
    def clear_records(self) # clears any associated records with this message
    def add_uri_record(self, uri: str) # add a URI record. 
    def add_text_record(self, text: str, language_code: str = None) # add a text record with optional IANA language_code, defaulting to 'en'
    def add_launch_app_record(self, appID: str, platform_type: NDEFAppRecordType) # add a record to launch an application
    def build_payload(self, buffer: WriteableBuffer) # used by the tag to construct the message payload based on records set
    def deinit(self) # frees up memory used by the message
    def __exit__(self) # as above

class Tag: # a common base class for common NFC Tag functionality.
    payload_buffer = ... # will be bytearray of payload_buffer_size bytes)
    def __init__(self, payload_buffer_size: int) # creates a buffer of payload_buffer_size that the payload will be held in
    def deinit(self) # stops tag emulation and frees up the hardware
    def __exit__(self) # as above, but automagically.

class Type2Tag(Tag):
    def __init__(self, payload_buffer_size: int)
    def set_payload(self, payload: NDEFMessage) # renders the message into the payload buffer
    def set_raw_payload(self, payload: ReadableBuffer) # sets raw bytes into the payload, for advanced use.
    def start_emulation(self) # starts type 2 tag emulation
    def stop_emulation(self) # stops type 2 tag emulation

class Type4Tag(Tag):
    # not yet implemented

I'm also not sure if class inheritance is the way you prefer to share common functionality, but I do want to plan for the future for Type 4 Tags, which is why I've split the classes up as such. I also don't know how you feel about enum classes like I used for the app launch record type, didn't quite see something to mimic there.

shared-module / common-hal

// elsewhere:
// typedef struct { /* ... */ } nfcio_tag_obj_t;
// typedef struct { /* ... */ } nfcio_ndef_message_obj_t;

common_hal_nfcio_ndef_construct(nfcio_ndef_message_obj_t *self);
common_hal_nfcio_ndef_deinit(nfcio_ndef_message_obj_t *self);
common_hal_nfcio_ndef_deinited(nfcio_ndef_message_obj_t *self);
common_hal_nfcio_ndef_clear_records(nfcio_ndef_message_obj_t *self);
common_hal_nfcio_ndef_add_uri_record(nfcio_ndef_message_obj_t *self, mp_obj_t uri);
common_hal_nfcio_ndef_add_text_record(nfcio_ndef_message_obj_t *self, mp_obj_t text, mp_obj_t language_code);
common_hal_nfcio_ndef_add_launch_app_record(nfcio_ndef_message_obj_t *self, mp_obj_t app_id, mp_int_t platform_type);
common_hal_nfcio_ndef_build_payload(nfcio_ndef_message_obj_t *self, uint8_t* payload_buffer, size_t payload_buffer_len);

common_hal_nfcio_tag_construct(nfcio_tag_obj_t *self, mp_int_t payload_buffer_size);
common_hal_nfcio_tag_deinit(nfcio_tag_obj_t *self);
common_hal_nfcio_tag_deeinited(nfcio_tag_obj_t *self);

common_hal_nfcio_tag_type_2_tag_construct(nfcio_tag_obj_t *self, mp_int_t payload_buffer_size);
common_hal_nfcio_tag_type_2_tag_set_raw_payload(nfcio_tag_obj_t *self, const uint8_t* payload);
common_hal_nfcio_tag_type_2_tag_start_emulation(nfcio_tag_obj_t *self);
common_hal_nfcio_tag_type_2_tag_stop_emulation(nfcio_tag_obj_t *self);

I'm not quite sure how to set up the inheritance in C just yet, but I figure that's a future me problem!

Please feel free to point out where I've not followed naming conventions or if you see names that would be more appropriate, etc. I think I'm grasping the difference between shared-module/common-hal and shared-bindings now, but let me know if I'm making any mistakes there too.

tannewt commented 3 years ago

This is a great start! Take a look at the design guide here: https://circuitpython.readthedocs.io/en/latest/docs/design_guide.html It's a random collection of things that could be helpful to know.

The reason I point it out is because this API could actually be split into two. Native APIs should be minimal and just encompass the core of the functionality. For example, NDEFMessage could be done in Python much easier than C.

One existing API to compare against is the BLE Advertising API. It is an Adapter class with stop and start advertising functions. I have a couple questions about NFC: 1) What is the difference between a type 2 and type 4 tag? Do they both have byte payloads? 2) Can the device act as multiple tags at once? 3) Can the device act as NFC concurrently with Bluetooth?

For the Python side, take a look at the BLE Advertising examples here: https://github.com/adafruit/Adafruit_CircuitPython_BLE/blob/master/examples/ble_current_time_service.py#L11-L15 Generally, I like that the message object be useful both to transmit and to read after a scan. That would make your example:

hello_world = nfcio.NDEFMessage()
hello_world.uri = "https://adafruit.com/"
hello_world.text["en"] = "Hello, circuitpython!"
hello_world.text["es"] = "Hola, circuito de pitón!"

This then allows you to do:

hello_world = nfcio.NDEFMessage()
print(hello_world.uri)
for language in hello_world.text:
    print(hello_world.text[language])

This would change a bit if you can have multiples of any of these things because then you'll want to act like a list.

tannewt commented 3 years ago

Also, no need to layout common-hal if you have a Python API. They should map from one to the other clearly.

nitz commented 3 years ago

Awesome, the design guide is exactly the sort of thing I was looking for!

I definitely wanted NDEFMessage to be python only, so the fact that it's not only possible but encouraged sounds excellent to me!

For your questions:

  1. First I guess it makes sense to touch on what makes them similar: They do both have byte payloads (typically NDEF messages according to the spec I think.) They both are the "target" device in an NFC interaction. The "reader/writer" would be something like your phone, or an PN532 chip. The target merely responds to commands that the reader/writer sends.

The Type 2 Tag (T2T) is based on the ISO14443A. They're read and re-writable, but they have a very limited memory footprint. Data is split into 4-byte "blocks" (Also called "pages", depending who you ask.) Those blocks make up sectors in sets of 256 (1KB). T2Ts respond to a SECTOR SELECT command that allows the reader/writer to choose which sector of memory (if greater than 1KB) it wants to work with. Since SECTOR SELECT only takes a single byte, that puts a theoretical cap on memory size at 254 KB (seems like 0xFF is RFU.) In practice, T2T memory sizes are much smaller. For example, NXP's NTAG21X line carries capacities from 144 to 888 bytes of user memory. Other than that, they're relatively straight forward in how they communicate, responding to read/write requests and so on. Of note, the T2T library in the nRF only supports "read" operations at the moment.

The Type 4 Tag (T4T) is based on ISO14443A and B. While it still operates in a similar read/re-writable transaction method, it's expected to build a "file system" of sorts. The reader/writer can select and read (or write) different things after putting the tag in the related states. As well, the T4T supports higher bitrates than the 106 kbps of the T2T. I haven't spent as much time with T4Ts as I have T2Ts, just tinkered slightly to see how the nRF API differed (as it supports a read-write payload and a static (e.g. read only) payload mode. It's also my understanding that there seems to be two common modes the actual payload behaves in, either the "ISO-DEP" mode, as well as the "NDEF" mode. I'm not quite sure if that's a software only difference or if it implies protocol differences as well.

  1. From my understanding, no. The ISO14443B only uses a 10% signal modulation between high and low, where as the ISO14443A uses 100%. I believe the initial synchronization portion of a data exchange does behave the same, but differs quickly when a device is expecting to be talking to a T4T instead of a T2T.

  2. At least in the case of the nRF platform, yes. The NFCT uses a 13.56 MHz frequency for NFC communications, and has the pins completely separate from the RF antenna. For example, on the nRF52840, pin H23 is the single-ended RF antenna pin for Bluetooth/Zigbee/Thread/Proprietary, and pins J24 (P0.11) & T2 (P0.11) are the NFC antenna pins. (Of which both are required for operation, is my understanding.)


Okay! Back to the Python side! I definitely can appreciate how you've got the BLE advertising set up. It's very possible to mimic that with the NFCT API. The hardware does raise events as it goes through the NFC transaction, so it's likely something could be done similar. The downside is that the NFC transaction is fleeting, rather than more long lasting like a bluetooth connection/pairing. The NFC transaction is dictated entirely by the "reader/writer" and can end from any step. The nRF52 hardware raises the NFC events as interrupts, which might be enough to catch most states in a similar loop like you've shown me in the BLE example there, but there'd be no guaranteed way of preventing the transaction from moving to the next step before user code has had a chance to run. (Unless there's some sort of callback back to user code, but that would definitely muddy the API)

Regarding using the NDEFMessage as the read & write object: definitely, 100%. The T2T will only be read only (while we lean on the nRF blob), but the T4T in read/write mode expects to be able to write to the payload buffer for sure, so parsing it back out into the NDEFMessage object makes perfect sense.

The caveat is that An NDEF message can have 0 or more records of any types. Perhaps an NDEFRecord object that could describe what it is, and provide interfaces to get it's data as the applicable type? (I see that being an intuitive thing, but I don't know how unexpected Python users typically find it for "duck punched" objects like that, with the need to check a type before trying to access properties that may be there? Or what the expected behavior would be when they try to access a property that isn't valid; e.g. the uri on a text record.)

tannewt commented 3 years ago
2. From my understanding, no.

Ok, so this means we may want to think of it as a singleton like the BLE Adapter class.

I think we can handle writing by reading the buffer back. No need to do any fancy callback stuff.

The caveat is that An NDEF message can have 0 or more records of any types. Perhaps an NDEFRecord object that could describe what it is, and provide interfaces to get it's data as the applicable type? (I see that being an intuitive thing, but I don't know how unexpected Python users typically find it for "duck punched" objects like that, with the need to check a type before trying to access properties that may be there? Or what the expected behavior would be when they try to access a property that isn't valid; e.g. the uri on a text record.)

How many record types are there? My bias would be to have one property for each type. Is there an overall type for the payload?

nitz commented 3 years ago

A singleton makes most sense to me. I suppose it's theoretical that a device could implement more than one NFC Tag, but that seems way out of scope for what CP looks to cover. I'm good with using the buffers as well for communication. Wouldn't be as "fully featured", but that's the kinda thing someone would probably be using their own stack for anyways.

For record types, I think the practical answer for now should be 5: Text, Uri, Application Launch, Connection handover [more on that much later], and "raw data". My reasoning for that is: well, those cover most common use cases and are what the NDEF code in the nRF SDK implements 😉.

A more thorough answer though...

My understanding of NDEF comes a lot from adafruit's tutorial, which to be fair says it's last update on December 2012, but based on other things I've read googling around it seems pretty upd to date.

There's sorta two layers to types in records. A type that describes uh... the type. And then a second subset of those types.

So records are split up into "chunks", where each chunk has a handful of flags, lengths, and a 'type name format' field (TNF).

The TNF field in the Record basically says It's one of: "empty", "well-known" (more on that in a moment), "mime type", "absolute uri", "external (more in a moment)", "unknown", or "unchanged". Unchanged is just used for every record chunk past the first. Empty, mime, and URI are exactly what you'd imagine. My experience has been that "unknown" is often used for arbitrary data, but I'm not sure if that's standard or not.

For the other two I skipped:

My more complicated answer in that case is 22, plus raw data. But most could be left for a rainy day.

I'll also go on record to say I'm 100% good with each record type having its own class. I think it keeps things clean and provides excellent discoverability. If code size is an issue, perhaps the "core" types could be in the base, and the other things like money transfers, or wireless charging types could just be a small add-on library, since the folx looking to use those would already be specifically hunting them.

tannewt commented 3 years ago

Ok, I think all of this pertains to a Python library (or more than one). We can iterate on those much faster so we can continue it later on those libraries.

For the core, do you have a better idea of what the API would look like?

nitz commented 3 years ago

I think so, I definitely understand the style you're going for, and will certainly look towards the BLE adapter for inspiration. I'm thinking I'm going to write from the "top down", so the python layer first and then fill in what it needs to be usable. I'll open a draft PR when I feel like the python layer looks sound so we can make tweaks starting there, before ugly accidentally stumbles all the way down the hill!

Excited to get started!

tannewt commented 3 years ago

@nitz Sounds perfect! Thank you!