diff --git a/src/Iot.Device.Bindings/CompatibilitySuppressions.xml b/src/Iot.Device.Bindings/CompatibilitySuppressions.xml index 4644ab8a2b..fa60bbdc88 100644 --- a/src/Iot.Device.Bindings/CompatibilitySuppressions.xml +++ b/src/Iot.Device.Bindings/CompatibilitySuppressions.xml @@ -1,6 +1,20 @@  + + CP0001 + T:Iot.Device.Pn532.AsTarget.TargetBaudRateInialized + lib/net6.0/Iot.Device.Bindings.dll + lib/net6.0/Iot.Device.Bindings.dll + true + + + CP0001 + T:Iot.Device.Pn532.AsTarget.TargetBaudRateInialized + lib/netstandard2.0/Iot.Device.Bindings.dll + lib/netstandard2.0/Iot.Device.Bindings.dll + true + CP0002 F:Iot.Device.Axp192.BatteryStatus.Overwinered diff --git a/src/devices/Card/CreditCard/ApduCommands.cs b/src/devices/Card/CreditCard/ApduCommands.cs index 8bb9001a99..95b0c29587 100644 --- a/src/devices/Card/CreditCard/ApduCommands.cs +++ b/src/devices/Card/CreditCard/ApduCommands.cs @@ -1,26 +1,59 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Iot.Device.Card.CreditCardProcessing +namespace Iot.Device.Card { /// /// The list of predefined commands to communicate with the card /// - internal class ApduCommands + public class ApduCommands { + /// Application block. public static byte[] ApplicationBlock = { 0x80, 0x1E }; - public static byte[] ApplicaitonUnBlock = { 0x80, 0x18 }; + + /// Application unblock. + public static byte[] ApplicationUnBlock = { 0x80, 0x18 }; + + /// Card block. public static byte[] CardBlock = { 0x80, 0x16 }; + + /// External authenticate. public static byte[] ExternalAuthenticate = { 0x00, 0x82 }; + + /// Generate application cryptogram. public static byte[] GenerateApplicationCryptogram = { 0x80, 0xAE }; + + /// Get challenge. public static byte[] GetChallenge = { 0x00, 0x84 }; + + /// Get data. public static byte[] GetData = { 0x80, 0xCA }; + + /// Get processing options. public static byte[] GetProcessingOptions = { 0x80, 0xA8 }; + + /// Internal authenticate. public static byte[] InternalAuthenticate = { 0x00, 0x88 }; + + /// PIN number change unblock. public static byte[] PersonalIdentificationNumberChangeUnblock = { 0x80, 0x24 }; + + /// Read binary. + public static byte[] ReadBinary = { 0x00, 0xB0 }; + + /// Update binary. + public static byte[] UpdateBinary = { 0x00, 0xD6 }; + + /// Read record. public static byte[] ReadRecord = { 0x00, 0xB2 }; + + /// Select a file. public static byte[] Select = { 0x00, 0xA4 }; + + /// Verify. public static byte[] Verify = { 0x80, 0x20 }; + + /// Get bytes to read. public static byte[] GetBytesToRead = { 0x00, 0xC0 }; } } diff --git a/src/devices/Card/CreditCard/ApduReturnCommands.cs b/src/devices/Card/CreditCard/ApduReturnCommands.cs new file mode 100644 index 0000000000..d051b54e3a --- /dev/null +++ b/src/devices/Card/CreditCard/ApduReturnCommands.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Iot.Device.Card +{ + /// + /// The list of predefined commands to communicate with the card + /// + public class ApduReturnCommands + { + /// Command completed properly. + public static byte[] CommandComplete = { 0x90, 0x00 }; + + /// Tag not found. + public static byte[] TagNotFound = { 0x6A, 0x82 }; + + /// Function not supported. + public static byte[] FunctionNotSupported = { 0x6A, 0x81 }; + + /// Memory Failure. + public static byte[] MemoryFailure = { 0x65, 0x81 }; + + /// Security status not satisfied. + public static byte[] SecurityNotSatisfied = { 0x69, 0x82 }; + + /// Wrong legnth. + public static byte[] WrongLength = { 0x6C, 0x00 }; + + /// End of file before being able to send all. + public static byte[] EndOfFileBefore = { 0x62, 0x82 }; + } +} diff --git a/src/devices/Card/EmulatedTag/CardStatus.cs b/src/devices/Card/EmulatedTag/CardStatus.cs new file mode 100644 index 0000000000..85727620ad --- /dev/null +++ b/src/devices/Card/EmulatedTag/CardStatus.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace EmulatedTag +{ + /// + /// The state of the card. + /// + public enum CardStatus + { + /// Card is in released state. + Released = 0, + + /// Car is in activated state. + Activated, + + /// Card is on delected state. + Deselected + } +} diff --git a/src/devices/Card/EmulatedTag/EmulatedNdefTag.cs b/src/devices/Card/EmulatedTag/EmulatedNdefTag.cs new file mode 100644 index 0000000000..86255619a6 --- /dev/null +++ b/src/devices/Card/EmulatedTag/EmulatedNdefTag.cs @@ -0,0 +1,530 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Threading; +using EmulatedTag; +using Iot.Device.Common; +using Iot.Device.Ndef; +using Iot.Device.Pn532; +using Iot.Device.Pn532.AsTarget; +using Microsoft.Extensions.Logging; + +namespace Iot.Device.Card +{ + /// + /// Emulated Tag card. so far, implementation only for PN532. + /// See https://nfc-forum.org/uploads/specifications/97-NFCForum-TS-T4T-1.2.pdf. + /// + public class EmulatedNdefTag + { + // APDU commands are using 5 elements in this order + // Then the data are added + private const byte Cla = 0x00; + private const byte Ins = 0x01; + private const byte P1 = 0x02; + private const byte P2 = 0x03; + private const byte Lc = 0x04; + private const ushort AbsoluteNdefMaxLength = 0xFFFE; + private const byte NdefFileId = 0x04; + private const byte CapacityContainerHiId = 0xE1; + private const byte CapacityContainerId = 0x03; + private static readonly byte[] NdefTagApplicationNameV2 = new byte[] { 0xD2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01 }; + private static readonly byte[] NdefTagApplicationNameV1 = new byte[] { 0xD2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x00 }; + private readonly List _receviedBytes = new List(); + private readonly Pn532.Pn532 _pn532; + private readonly ILogger _logger; + + private byte[] _ndefId = new byte[3]; + private ushort _maximumNdefLength = AbsoluteNdefMaxLength; + private bool _readOnly = false; + private TagType _tagType = TagType.NoTagSelected; + private CardStatus _cardStatus = CardStatus.Released; + + // Capability container, page 18 + private byte[] _capabilityContainer = new byte[] + { + //// Length of the CC file + 0x00, 0x0F, + //// Mapping version is 2.0, 0x30 is v3 + 0x20, + //// Maximum R-APDU data size is 200 bytes + 0x00, 0xC8, + //// Maximum C-APDU data size is 200 bytes + 0x00, 0xC8, + //// Tag, file identifier 4 = NDEF file + 0x04, + //// TLV size of the file identifier + 0x06, + //// File identifier, using 0xE1 like for the capability container selection to make things easier + //// More valid range exists + CapacityContainerHiId, NdefFileId, + //// Max NDEF Length will be replaced by the real value + (byte)(AbsoluteNdefMaxLength >> 8), (byte)(AbsoluteNdefMaxLength & 0xFF), + //// NDEF file read access for all + 0x00, + //// NDEF file write access for all by default + 0x00 + }; + + /// + /// Event sent when a new NDEF message is received. + /// + public event EventHandler? NdefReceived; + + /// + /// Event sent when the satus of the card has changed. + /// + public event EventHandler? CardStatusChanged; + + /// + /// Gets or sets the read only flag. Default is false. + /// + public bool ReadOnly + { + get => _readOnly; + set + { + _readOnly = value; + _capabilityContainer[14] = (byte)(_readOnly ? 0xFF : 0x00); + } + } + + /// + /// Gets or sets the maximum NDEF length. Default is 0xFFFE. + /// + public ushort MaximumNdefLength + { + get => _maximumNdefLength; + set + { + if (value > AbsoluteNdefMaxLength) + { + throw new ArgumentException($"{nameof(MaximumNdefLength)} must be maximum {AbsoluteNdefMaxLength:X2}"); + } + + _maximumNdefLength = value; + _capabilityContainer[11] = (byte)(_maximumNdefLength >> 8); + _capabilityContainer[12] = (byte)(_maximumNdefLength & 0xFF); + } + } + + /// + /// Gets or sets the NDEF ID. It must be 3 bytes long. + /// + public byte[] NdefId + { + get => _ndefId; + + set + { + if (value == null || value.Length != 3) + { + throw new System.ArgumentException("NdefId must be 3 bytes long"); + } + + _ndefId = value; + } + } + + /// + /// Gets or sets the NDEF message. + /// + public NdefMessage NdefMessage { get; set; } = new NdefMessage(); + + /// + /// Constructor for EmulatedTag. + /// + /// A PN532 card reader + /// The NDEF ID. It must be 3 bytes long. + public EmulatedNdefTag(Pn532.Pn532 pn532, byte[] nfId = default!) + { + _pn532 = pn532 ?? throw new System.ArgumentNullException(nameof(pn532)); + if (nfId != null) + { + NdefId = nfId; + } + + _logger = _pn532.GetCurrentClassLogger(); + } + + /// + /// Initialize the PN532 as a target. + /// + /// A cancellation token to stop the listen operation. + /// A tuple containing the initialization details and the data sent by the initiator. + public (TargetModeInitialized? ModeInitialized, byte[] Data) Initialize(CancellationToken token = default) + { + byte[]? retData = null!; + TargetModeInitialized? modeInitialized = null!; + while (!token.IsCancellationRequested) + { + (modeInitialized, retData) = _pn532.InitAsTarget( + TargetModeInitialization.PiccOnly, + new TargetMifareParameters() + { + NfcId3 = NdefId, + Atqa = new byte[] { 0x04, 0x00 }, + Sak = 0x20 + }, + new TargetFeliCaParameters(), + new TargetPiccParameters()); + if (modeInitialized is object) + { + break; + } + + // Give time to PN532 to process + token.WaitHandle.WaitOne(100, true); + } + + if (modeInitialized is null) + { + return (null, Array.Empty()); + } + + return (modeInitialized, retData!); + } + + /// + /// Initializes the reader as a target and Listen for a card. + /// + /// A cancellation token to stop the listen operation. + /// True is success and operation was done properly. + public ErrorCode InitializeAndListen(CancellationToken token = default) + { + ErrorCode err = ErrorCode.Unknown; + while (!token.IsCancellationRequested) + { + var (modeInitialized, data) = Initialize(token); + if (modeInitialized is null || data is null || data.Length < 2) + { + return ErrorCode.Unknown; + } + + err = Listen(token); + } + + return err; + } + + /// + /// Initializes the reader as a target and Listen for a card. + /// + /// A cancellation token to stop the listen operation. + /// True is success and operation was done properly. + public ErrorCode Listen(CancellationToken token = default) + { + // Now reads the data + Span read = stackalloc byte[512]; + byte[] data; + int ret = -1; + DateTimeOffset dt = DateTimeOffset.UtcNow; + ErrorCode err = ErrorCode.None; + TargetStatus targetStatus; + CardStatus newStatus = _cardStatus; + _receviedBytes.Clear(); + while ((ret < 0 || !token.IsCancellationRequested)) + { + CheckCardStatus(); + + if (_cardStatus == CardStatus.Released) + { + return ErrorCode.None; + } + + if (_cardStatus == CardStatus.Activated) + { + // This must be a close loop using all the CPU not to miss the message + ret = _pn532.ReadDataAsTarget(read); + if (ret >= 0) + { + // For example: 00-00-A4-04-00-0E-32-50-41-59-2E-53-59-53-2E-44-44-46-30-31-00 + _logger.LogDebug($"EMUL RCV- Status: {(ErrorCode)read[0]}, Data: {BitConverter.ToString(read.Slice(1, ret - 1).ToArray())}"); + + if ((ErrorCode)read[0] != ErrorCode.None) + { + CheckCardStatus(); + return (ErrorCode)read[0]; + } + + data = read.Slice(1, ret - 1).ToArray(); + + // We need to ensure we have a proper data length + if (data.Length < Lc) + { + // We send anyway a complete just in case + SendResponse(ApduReturnCommands.CommandComplete); + continue; + } + + if (data[Cla] == ApduCommands.Select[Cla] && (data[Ins] == ApduCommands.Select[Ins])) + { + // 00 A4 04 00 07 D2760000850101 00 + // 00 A4 00 0C 02 E103 + err = ProcessSelect(data); + + } + else if (data[Cla] == ApduCommands.ReadBinary[Cla] && (data[Ins] == ApduCommands.ReadBinary[Ins])) + { + err = ProcessBinary(data); + } + else if (data[Cla] == ApduCommands.UpdateBinary[Cla] && (data[Ins] == ApduCommands.UpdateBinary[Ins])) + { + err = ProcessWriting(data); + } + + if (err != ErrorCode.None) + { + CheckCardStatus(); + return err; + } + + dt = DateTimeOffset.UtcNow; + } + else + { + // It can take up to 1.078 second to get the data + if (dt.AddMilliseconds(1100) < DateTimeOffset.UtcNow) + { + if (err != ErrorCode.None) + { + CheckCardStatus(); + return err; + } + + SendResponse(ApduReturnCommands.FunctionNotSupported); + return ErrorCode.Unknown; + } + } + } + } + + return ErrorCode.None; + + void CheckCardStatus() + { + targetStatus = _pn532.GetStatusAsTarget(); + if (targetStatus is null) + { + return; + } + else + { + newStatus = targetStatus.IsReleased ? CardStatus.Released : (targetStatus.IsActivated ? CardStatus.Activated : CardStatus.Deselected); + if (newStatus != _cardStatus) + { + _cardStatus = newStatus; + CardStatusChanged?.Invoke(this, _cardStatus); + } + } + } + } + + private ErrorCode ProcessSelect(ReadOnlySpan data) + { + byte nameOrId = data[P1]; + + if (nameOrId == 0x04) + { + // 00 A4 04 00 07 D2760000850101 00 + // Application name + byte len = data[Lc]; + if ((len != NdefTagApplicationNameV2.Length) && (len != NdefTagApplicationNameV1.Length)) + { + SendResponse(ApduReturnCommands.FunctionNotSupported); + return ErrorCode.None; + } + + bool foundNdefFile = false; + if (data.Slice(Lc + 1, len).SequenceEqual(NdefTagApplicationNameV2)) + { + foundNdefFile = true; + } + else if (data.Slice(Lc + 1, len).SequenceEqual(NdefTagApplicationNameV1)) + { + foundNdefFile = true; + } + + if (!foundNdefFile) + { + SendResponse(ApduReturnCommands.FunctionNotSupported); + return ErrorCode.None; + } + + SendResponse(ApduReturnCommands.CommandComplete); + return ErrorCode.None; + } + else if (nameOrId == 0x00) + { + // 00 A4 00 0C 02 E103 + // Application ID, second parameter should be 0x0C + if (data[P2] != 0x0C) + { + SendResponse(ApduReturnCommands.CommandComplete); + } + else + { + // in theory, this can be encoded up to 3 bytes, at this stage, it can only by 1 byte + var len = data[Lc]; + if (len == 0x02 && (data[Lc + 1] == CapacityContainerHiId) && (data[Lc + 2] == CapacityContainerId || (data[Lc + 2] == NdefFileId))) + { + SendResponse(ApduReturnCommands.CommandComplete); + if (data[Lc + 2] == CapacityContainerId) + { + _tagType = TagType.CompatibilityContainer; + } + else + { + _tagType = TagType.NdefMessage; + } + } + else + { + SendResponse(ApduReturnCommands.TagNotFound); + } + } + } + else + { + return ErrorCode.Unknown; + } + + return ErrorCode.None; + } + + private ErrorCode ProcessBinary(ReadOnlySpan data) + { + if (_tagType == TagType.NoTagSelected) + { + SendResponse(ApduReturnCommands.TagNotFound); + return ErrorCode.None; + } + + int len = (data[P1] << 8) + data[P2]; + if (len > MaximumNdefLength) + { + SendResponse(ApduReturnCommands.EndOfFileBefore); + return ErrorCode.None; + } + + if (_tagType == TagType.CompatibilityContainer) + { + var size = data[Lc] + len > _capabilityContainer.Length ? _capabilityContainer.Length - len : data[Lc]; + Span response = stackalloc byte[2 + data[Lc]]; + ApduReturnCommands.CommandComplete.CopyTo(response.Slice(response.Length - 2)); + _capabilityContainer.AsSpan().Slice(len, size).CopyTo(response); + SendResponse(response); + return ErrorCode.None; + } + else + { + if (NdefMessage == null) + { + // 67h 00h Wrong length; no further indication. + // 6Ch XXh Wrong Le field; SW2 encodes the exact number of available data bytes. + SendResponse(ApduReturnCommands.WrongLength); + return ErrorCode.None; + } + + Span ndef = stackalloc byte[NdefMessage.Length + 2]; + var size = data[Lc] + len > ndef.Length ? ndef.Length - len : data[Lc]; + NdefMessage.Serialize(ndef.Slice(2)); + ndef[0] = (byte)(NdefMessage.Length >> 8); + ndef[1] = (byte)(NdefMessage.Length & 0xFF); + Span response = stackalloc byte[2 + data[Lc]]; + ApduReturnCommands.CommandComplete.CopyTo(response.Slice(response.Length - 2)); + ndef.Slice(len, size).CopyTo(response); + SendResponse(response); + return ErrorCode.None; + } + } + + private ErrorCode ProcessWriting(ReadOnlySpan data) + { + if (ReadOnly) + { + SendResponse(ApduReturnCommands.SecurityNotSatisfied); + return ErrorCode.None; + } + + // So far this implementation do not support offset. We will assume all the elements can be written in one write + // We do support also only v2.0 and not the v3.0 + // --- + // Case of single writes: + // 00-D6-00-00-17-00-00-D1-01-11-54-02-65-6E-C3-87-61-20-6D-61-72-63-68-65-20-74-6F-70 + // 00-D6-00-00-02-00-15 + // Another example: + // 00-D6-00-00-31-00-00-D1-01-2B-54-02-65-6E-2E-4E-45-54-20-49-6F-54-20-61-6E-64-2E-4E-45-54-20-6E-61-6E-6F-46-72-61-6D-65-77-6F-72-6B-20-61-72-65-20-67-72-65-61-74 + // 00-D6-00-00-02-00-2F + // --- + // Case of multiple writes: + // 00-D6-00-00-38-00-00-91-01-3A-54-02-65-6E-57-6A-73-68-68-73-68-73-68-73-73-62-73-62-73-68-73-68-20-73-6A-73-6A-73-75-73-20-73-75-73-75-77-69-69-61-F0-9F-8D-B0-F0-9F-98-82-F0-9F-8D-B7 + // 00-D6-00-38-38-F0-9F-98-9B-F0-9F-A7-80-11-01-7C-55-00-73-6D-73-3A-33-33-36-36-34-34-30-34-36-37-34-26-62-6F-64-79-3D-53-62-73-68-73-75-73-25-32-30-73-6E-73-6E-73-6A-6A-73-25-46-30-25 + // 00-D6-00-70-38-39-46-25-39-38-25-41-44-25-46-30-25-39-46-25-38-44-25-42-39-25-46-30-25-39-46-25-39-38-25-38-39-25-46-30-25-39-46-25-38-44-25-42-39-25-46-30-25-39-46-25-39-38-25-38-32 + // 00-D6-00-A8-35-25-46-30-25-39-46-25-39-39-25-38-46-25-46-30-25-39-46-25-39-39-25-38-46-51-01-19-55-02-6C-69-6E-6B-65-64-69-6E-2E-63-6F-6D-2F-69-6E-2F-6C-61-75-72-65-6C-6C-65 + // Final write gives the full size of the NDEF + // 00-D6-00-00-02-00-DB + int len = data[Lc]; + if (len == 2) + { + ErrorCode err = ErrorCode.None; + // This is the case of a confirmation and greedy collection + var size = (data[Lc + 1] << 8) + data[Lc + 2]; + _receviedBytes.RemoveRange(0, _receviedBytes.Count - size); + var msg = new NdefMessage(_receviedBytes.ToArray()); + try + { + foreach (NdefRecord record in msg.Records) + { + if (record.Payload is null) + { + continue; + } + + NdefMessage.Records.Add(record); + } + + NdefReceived?.Invoke(this, msg); + } + catch (Exception ex) + { + // If we are getting 3.0 messages, we will be in this situation + _pn532.GetCurrentClassLogger().LogError($"Exception processing NDEF: {ex}"); + err = ErrorCode.Unknown; + } + finally + { + _receviedBytes.Clear(); + SendResponse(ApduReturnCommands.CommandComplete); + } + + return err; + } + else if (len < 2) + { + _receviedBytes.Clear(); + SendResponse(ApduReturnCommands.FunctionNotSupported); + return ErrorCode.None; + } + + // Add all what is revceived we ignore the offsets as they are always sent in order + _receviedBytes.AddRange(data.Slice(Lc + 1, len).ToArray()); + + SendResponse(ApduReturnCommands.CommandComplete); + return ErrorCode.None; + } + + private bool SendResponse(ReadOnlySpan response) + { + if (response.Length < 2) + { + return false; + } + + _pn532.GetCurrentClassLogger().LogDebug($"RESPONSE: {BitConverter.ToString(response.ToArray())}"); + return _pn532.WriteDataAsTarget(response); + } + } +} diff --git a/src/devices/Card/EmulatedTag/EmulatedTag.csproj b/src/devices/Card/EmulatedTag/EmulatedTag.csproj new file mode 100644 index 0000000000..6c2671c810 --- /dev/null +++ b/src/devices/Card/EmulatedTag/EmulatedTag.csproj @@ -0,0 +1,14 @@ + + + + $(DefaultBindingTfms) + enable + + + + + + + + + diff --git a/src/devices/Card/EmulatedTag/README.md b/src/devices/Card/EmulatedTag/README.md new file mode 100644 index 0000000000..ec6f20a76e --- /dev/null +++ b/src/devices/Card/EmulatedTag/README.md @@ -0,0 +1,92 @@ +# Emulated card NDEF Tag + +This class supports NDEF card emulation. So far, the only reader supported is the PN532. + +## Documentation + +You can find useful documentation on: + +* [NFC Forum](https://nfc-forum.org/uploads/specifications/97-NFCForum-TS-T4T-1.2.pdf) for the high level communication protocol and payloads. +* [ST25TA64K](https://www.st.com/resource/en/datasheet/st25ta64k.pdf) as a good implementation detail from the card perspective. + +## Usage + +When you have created a PN52, you can use directly: + +```csharp +EmulatedNdefTag ndef = new(pn532, new byte[] { 0x12, 0x34, 0x45 }); +ndef.CardStatusChanged += NdefCardStatusChanged; +ndef.NdefReceived += NdefNdefReceived; +ndef.InitializeAndListen(cts.Token); +``` + +The constructor allow to adjust the last 3 bytes of the ID. By default, to avoid copying cards fully, the first byte is always fixed to 0x08. + +The card will automatically place itself in the listen mode and will listen up to a cancellation token is set to cancel. + +This implies in this mode that once the card is deselected and read already, placing it in listen more again can trigger the listener to select it again. + +So you can use the `CardStatusChanged` event to adjust the needed behavior. Both `Initialize` and `Listen` functions can be used in a more granular way. + +Here is an example of writing 3 tags on the emulated tag from a [phone application](https://apps.apple.com/app/nfc-tools/id1252962749): + +![write the 3 ndef messages](tag_write.png) + +Once the application write it to the card, you will get from this code: + +```csharp +void NdefNdefReceived(object? sender, NdefMessage e) +{ + Console.WriteLine("New NDEF received!"); + foreach (var record in e.Records) + { + Console.WriteLine($"Record length: {record.Length}"); + if (TextRecord.IsTextRecord(record)) + { + var text = new TextRecord(record); + Console.WriteLine($" Text: {text.Text}"); + } + else if (UriRecord.IsUriRecord(record)) + { + var uri = new UriRecord(record); + Console.WriteLine($" Uri: {uri.Uri}"); + } + } +} + +void NdefCardStatusChanged(object? sender, EmulatedTag.CardStatus e) +{ + Console.WriteLine($"Status of the emulated card changed to {e}"); +} +``` + +The following outcome: + +```text +Status of the emulated card changed to Activated +New NDEF received! +Record length: 48 + Text: I ?? .NET IoT and .NET nanoFramework +Record length: 26 + Uri: github.com/dotnet/iot +Record length: 29 + Uri: github.com/nanoFramework +Status of the emulated card changed to Released +``` + +And reading back what's on the emulated card with the phone application: + +![display the 3 ndef messages](tag_read.png) + +You can also get access to the `NdefMessage` property: + +```csharp +ndef.NdefMessage.Records.Add(new TextRecord("I love NET IoT and .NET nanoFramework!", "en-us", Encoding.UTF8)); +ndef.NdefMessage.Records.Add(new UriRecord(UriType.Https, "github.com/dotnet/iot")); +``` + +So when the card reader will read the card, it will be able to access the NDEF message you have saved in this property. + +## Future improvements + +So far, only PN532 can be setup as a card. Once more readers in this repository will have this ability, an interface will have to be put in place so more readers can be used for this purpose. diff --git a/src/devices/Card/EmulatedTag/TagType.cs b/src/devices/Card/EmulatedTag/TagType.cs new file mode 100644 index 0000000000..004a31232b --- /dev/null +++ b/src/devices/Card/EmulatedTag/TagType.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Iot.Device.Card +{ + internal enum TagType + { + NoTagSelected, + CompatibilityContainer, + NdefMessage, + } +} diff --git a/src/devices/Card/EmulatedTag/category.txt b/src/devices/Card/EmulatedTag/category.txt new file mode 100644 index 0000000000..315208448b --- /dev/null +++ b/src/devices/Card/EmulatedTag/category.txt @@ -0,0 +1,2 @@ +card +rfid \ No newline at end of file diff --git a/src/devices/Card/EmulatedTag/tag_read.png b/src/devices/Card/EmulatedTag/tag_read.png new file mode 100644 index 0000000000..2a1d7e7993 Binary files /dev/null and b/src/devices/Card/EmulatedTag/tag_read.png differ diff --git a/src/devices/Card/EmulatedTag/tag_write.png b/src/devices/Card/EmulatedTag/tag_write.png new file mode 100644 index 0000000000..506a00bec8 Binary files /dev/null and b/src/devices/Card/EmulatedTag/tag_write.png differ diff --git a/src/devices/Card/Ndef/NdefMessage.cs b/src/devices/Card/Ndef/NdefMessage.cs index 29311928ce..247ce1112f 100644 --- a/src/devices/Card/Ndef/NdefMessage.cs +++ b/src/devices/Card/Ndef/NdefMessage.cs @@ -12,6 +12,8 @@ namespace Iot.Device.Ndef /// public class NdefMessage { + private static readonly byte[] _emptyMessage = new byte[] { 0x00, 0x03, 0xD0, 0x00, 0x00 }; + /// /// Associated with the GeneralPurposeByteConsitions, it tells if a sector is read/write and a valid /// NDEF sector @@ -112,7 +114,18 @@ public NdefMessage(ReadOnlySpan message) /// /// Get the length of the message /// - public int Length => Records.Select(m => m.Length).Sum(); + public int Length + { + get + { + if (Records.Count == 0) + { + return _emptyMessage.Length; + } + + return Records.Select(m => m.Length).Sum(); + } + } /// /// Serialize the message in a span of bytes @@ -127,6 +140,8 @@ public void Serialize(Span messageSerialized) if (Records.Count == 0) { + // Empty record is 00 03 D0 00 00 + _emptyMessage.CopyTo(messageSerialized); return; } diff --git a/src/devices/Pn532/AsTarget/TargetBaudRateInialized.cs b/src/devices/Pn532/AsTarget/TargetBaudRateInitialized.cs similarity index 93% rename from src/devices/Pn532/AsTarget/TargetBaudRateInialized.cs rename to src/devices/Pn532/AsTarget/TargetBaudRateInitialized.cs index b98a98d51d..c29586938b 100644 --- a/src/devices/Pn532/AsTarget/TargetBaudRateInialized.cs +++ b/src/devices/Pn532/AsTarget/TargetBaudRateInitialized.cs @@ -7,7 +7,7 @@ namespace Iot.Device.Pn532.AsTarget /// When PN532 is acting as a target, the baud rate /// it is engaged to /// - public enum TargetBaudRateInialized + public enum TargetBaudRateInitialized { /// /// 106k bps diff --git a/src/devices/Pn532/AsTarget/TargetFeliCaParameters.cs b/src/devices/Pn532/AsTarget/TargetFeliCaParameters.cs index 8b56b83059..b3acfe9bd2 100644 --- a/src/devices/Pn532/AsTarget/TargetFeliCaParameters.cs +++ b/src/devices/Pn532/AsTarget/TargetFeliCaParameters.cs @@ -14,7 +14,7 @@ public class TargetFeliCaParameters { // First 2 bytes must be 0x01 0xFE private byte[] _nfcId2 = new byte[8] { 0x01, 0xFE, 0xA1, 0xB2, 0xC3, 0xD4, 0xE5, 0xF6 }; - private byte[] _pad = new byte[8]; + private byte[] _pad = new byte[8] { 0x03, 0x00, 0x4B, 0x02, 0x4F, 0x49, 0x8A, 0x00 }; // those are typical values private byte[] _systemCode = new byte[2] { 0xFF, 0xFF }; diff --git a/src/devices/Pn532/AsTarget/TargetModeInitialized.cs b/src/devices/Pn532/AsTarget/TargetModeInitialized.cs index 7c0d7139fd..b930c6a9ac 100644 --- a/src/devices/Pn532/AsTarget/TargetModeInitialized.cs +++ b/src/devices/Pn532/AsTarget/TargetModeInitialized.cs @@ -11,7 +11,7 @@ public class TargetModeInitialized /// /// The target baud rate between the PN532 and the reader /// - public TargetBaudRateInialized TargetBaudRate { get; set; } + public TargetBaudRateInitialized TargetBaudRate { get; set; } /// /// True if we have a PICC emulation diff --git a/src/devices/Pn532/AsTarget/TargetState.cs b/src/devices/Pn532/AsTarget/TargetState.cs new file mode 100644 index 0000000000..859ba75405 --- /dev/null +++ b/src/devices/Pn532/AsTarget/TargetState.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Iot.Device.Pn532.AsTarget +{ + /// + /// Status state for the PN532 when setup as a target. + /// + public enum TargetState + { + /// + /// TG_IDLE / TG_RELEASED: the PN532 (acting as NFCIP-1 target) waits for an initiator or has been released by its initiator. + /// + NfcipReleased = 0x00, + + /// + /// TG_ACTIVATED: the PN532 is activated as NFCIP-1 target. + /// + NfcipActivated = 0x01, + + /// + /// TG_DESELECTED: the PN532 (acting as NFCIP-1 target) has been de-selected by its initiator. + /// + NfcipDeselected = 0x02, + + /// + /// PICC_RELEASED: the PN532 (acting as ISO/IEC14443-4 PICC) has been released by its PCD (no more RF field is detected). + /// + PiccReleased = 0x80, + + /// + /// PICC_ACTIVATED: the PN532 is activated as ISO/IEC14443-4 PICC. + /// + PiccActivated = 0x81, + + /// + /// PICC_DESELECTED: the PN532 (acting as ISO/IEC14443-4 PICC) has been de-selected by its PDC + /// + PiccDeselected = 0x82, + } +} diff --git a/src/devices/Pn532/AsTarget/TargetStatus.cs b/src/devices/Pn532/AsTarget/TargetStatus.cs new file mode 100644 index 0000000000..699d9bb09d --- /dev/null +++ b/src/devices/Pn532/AsTarget/TargetStatus.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Iot.Device.Pn532.AsTarget +{ + /// + /// Status state for the PN532 when setup as a target. + /// + public class TargetStatus + { + internal TargetStatus(byte state, byte rbit) + { + State = (TargetState)state; + SpeedInitiator = (TargetBaudRateInitialized)(rbit & 0b0011_0000); + SpeedTarget = (TargetBaudRateInitialized)((rbit >> 4) & 0b0000_0111); + } + + /// + /// Gets or sets the state of the target. + /// + public TargetState State { get; set; } + + /// + /// Gets or sets the initiator baud rate. + /// + public TargetBaudRateInitialized SpeedInitiator { get; set; } + + /// + /// Gets or sets the target baud rate. + /// + public TargetBaudRateInitialized SpeedTarget { get; set; } + + /// + /// Gets the value indicating whether the target is released. + /// + public bool IsReleased => State == TargetState.NfcipReleased || State == TargetState.PiccReleased; + + /// + /// Gets the value indicating whether the target is activated. + /// + public bool IsActivated => State == TargetState.NfcipActivated || State == TargetState.PiccActivated; + + /// + /// Gets the value indicating whether the target is deselected. + /// + public bool IsDeselected => State == TargetState.NfcipDeselected || State == TargetState.PiccDeselected; + } +} diff --git a/src/devices/Pn532/Pn532.cs b/src/devices/Pn532/Pn532.cs index 5314cd5645..f63f3c9fa3 100644 --- a/src/devices/Pn532/Pn532.cs +++ b/src/devices/Pn532/Pn532.cs @@ -898,13 +898,18 @@ public override bool ReselectTarget(byte targetNumber) #region PN532 as Target /// - /// Set the PN532 as a target, so as a card + /// Set the PN532 as a target, so as a card. /// + /// The target mode to select. + /// The Mifare card definition. + /// The Felica card definition. + /// The PICC card definition. + /// A tuple containing the initialization details and the data sent by the initiator. public (TargetModeInitialized? ModeInialized, byte[]? Initiator) InitAsTarget(TargetModeInitialization mode, TargetMifareParameters mifare, TargetFeliCaParameters feliCa, TargetPiccParameters picc) { // First make sure we have the right mode in the parameters for the PICC only case - if (mode == TargetModeInitialization.PiccOnly) + if (mode.HasFlag(TargetModeInitialization.PiccOnly)) { _logger.LogDebug($"{nameof(InitAsTarget)} - changing mode for Picc only"); ParametersFlags |= ParametersFlags.ISO14443_4_PICC; @@ -939,7 +944,7 @@ public override bool ReselectTarget(byte targetNumber) modeInitialized.IsDep = (receivedData[0] & 0b0000_0100) == 0b0000_0100; modeInitialized.IsISO14443_4Picc = (receivedData[0] & 0b0000_1000) == 0b0000_1000; modeInitialized.TargetFramingType = (TargetFramingType)(receivedData[0] & 0b0000_0011); - modeInitialized.TargetBaudRate = (TargetBaudRateInialized)(receivedData[0] & 0b0111_0000); + modeInitialized.TargetBaudRate = (TargetBaudRateInitialized)(receivedData[0] & 0b0111_0000); return (modeInitialized, receivedData.Slice(1, ret - 1).ToArray()); } @@ -960,10 +965,10 @@ public int ReadDataAsTarget(Span receivedData) } ret = ReadResponse(CommandSet.TgGetData, receivedData); - _logger.LogDebug($"{nameof(InitAsTarget)}, success: {ret}"); + _logger.LogDebug($"{nameof(ReadDataAsTarget)}, success: {ret}"); if (ret > 0) { - _logger.LogDebug($"{nameof(WriteDataAsTarget)} - error: {(ErrorCode)receivedData[0]}, received array: {BitConverter.ToString(receivedData.Slice(1, ret - 1).ToArray())}"); + _logger.LogDebug($"{nameof(ReadDataAsTarget)} - error: {(ErrorCode)receivedData[0]}, received array: {BitConverter.ToString(receivedData.Slice(1, ret - 1).ToArray())}"); } return ret; @@ -984,7 +989,7 @@ public bool WriteDataAsTarget(ReadOnlySpan dataToSend) Span receivedData = stackalloc byte[1]; ret = ReadResponse(CommandSet.TgSetData, receivedData); - _logger.LogDebug($"{nameof(InitAsTarget)}, success: {ret}"); + _logger.LogDebug($"{nameof(WriteDataAsTarget)}, success: {ret}"); if (ret > 0) { _logger.LogDebug($"{nameof(WriteDataAsTarget)} - error: {(ErrorCode)receivedData[0]}"); @@ -994,6 +999,29 @@ public bool WriteDataAsTarget(ReadOnlySpan dataToSend) return false; } + /// + /// Gets the status of the PN532 when it is a target. + /// + /// A target status object and null in case of problem. + public TargetStatus GetStatusAsTarget() + { + var ret = WriteCommand(CommandSet.TgGetTargetStatus); + if (ret < 0) + { + return null!; + } + + Span receivedData = stackalloc byte[2]; + ret = ReadResponse(CommandSet.TgGetTargetStatus, receivedData); + _logger.LogDebug($"{nameof(GetStatusAsTarget)}, success: {ret}"); + if (ret > 0) + { + return new TargetStatus(receivedData[0], receivedData[1]); + } + + return null!; + } + #endregion #region RFConfiguration diff --git a/src/devices/Pn532/Pn532.sln b/src/devices/Pn532/Pn532.sln index c375cf445f..ae9aac31d4 100644 --- a/src/devices/Pn532/Pn532.sln +++ b/src/devices/Pn532/Pn532.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.31410.357 +# Visual Studio Version 17 +VisualStudioVersion = 17.7.34221.43 MinimumVisualStudioVersion = 15.0.26124.0 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{EB7F3487-80F6-4406-956D-9EA1067C88EF}" EndProject @@ -21,6 +21,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ultralight", "..\Card\Ultra EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ndef", "..\Card\Ndef\Ndef.csproj", "{65BBA5BE-4207-400D-A1B9-FAD7AE621CA6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EmulatedTag", "..\Card\EmulatedTag\EmulatedTag.csproj", "{EF014952-D851-4202-801B-4BF97C0475B3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -115,6 +117,18 @@ Global {65BBA5BE-4207-400D-A1B9-FAD7AE621CA6}.Release|x64.Build.0 = Release|Any CPU {65BBA5BE-4207-400D-A1B9-FAD7AE621CA6}.Release|x86.ActiveCfg = Release|Any CPU {65BBA5BE-4207-400D-A1B9-FAD7AE621CA6}.Release|x86.Build.0 = Release|Any CPU + {EF014952-D851-4202-801B-4BF97C0475B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EF014952-D851-4202-801B-4BF97C0475B3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EF014952-D851-4202-801B-4BF97C0475B3}.Debug|x64.ActiveCfg = Debug|Any CPU + {EF014952-D851-4202-801B-4BF97C0475B3}.Debug|x64.Build.0 = Debug|Any CPU + {EF014952-D851-4202-801B-4BF97C0475B3}.Debug|x86.ActiveCfg = Debug|Any CPU + {EF014952-D851-4202-801B-4BF97C0475B3}.Debug|x86.Build.0 = Debug|Any CPU + {EF014952-D851-4202-801B-4BF97C0475B3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EF014952-D851-4202-801B-4BF97C0475B3}.Release|Any CPU.Build.0 = Release|Any CPU + {EF014952-D851-4202-801B-4BF97C0475B3}.Release|x64.ActiveCfg = Release|Any CPU + {EF014952-D851-4202-801B-4BF97C0475B3}.Release|x64.Build.0 = Release|Any CPU + {EF014952-D851-4202-801B-4BF97C0475B3}.Release|x86.ActiveCfg = Release|Any CPU + {EF014952-D851-4202-801B-4BF97C0475B3}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -126,6 +140,7 @@ Global {A4130820-1E4C-4825-823E-2784C613F5D7} = {6F2CD363-FCFC-430C-806B-82DE583E5D61} {DD9C2DCB-E6E6-4DAE-A2C4-0E04C3900233} = {6F2CD363-FCFC-430C-806B-82DE583E5D61} {65BBA5BE-4207-400D-A1B9-FAD7AE621CA6} = {6F2CD363-FCFC-430C-806B-82DE583E5D61} + {EF014952-D851-4202-801B-4BF97C0475B3} = {6F2CD363-FCFC-430C-806B-82DE583E5D61} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6D153C0E-5B8C-4118-8B24-8701E83CCFEB} diff --git a/src/devices/Pn532/samples/Pn532sample.csproj b/src/devices/Pn532/samples/Pn532sample.csproj index fac5d1cb4c..0dadfa8513 100644 --- a/src/devices/Pn532/samples/Pn532sample.csproj +++ b/src/devices/Pn532/samples/Pn532sample.csproj @@ -8,6 +8,7 @@ + diff --git a/src/devices/Pn532/samples/Program.cs b/src/devices/Pn532/samples/Program.cs index 3de8daef88..43e9dafb62 100644 --- a/src/devices/Pn532/samples/Program.cs +++ b/src/devices/Pn532/samples/Program.cs @@ -17,6 +17,7 @@ using Iot.Device.Common; using Iot.Device.Ndef; using Iot.Device.Pn532; +using Iot.Device.Pn532.AsTarget; using Iot.Device.Pn532.ListPassive; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Console; @@ -94,7 +95,9 @@ (MifareReadNdef, nameof(MifareReadNdef)), (MifareWriteNdef, nameof(MifareWriteNdef)), (TestGPIO, nameof(TestGPIO)), - (ReadCreditCard, nameof(ReadCreditCard)) + (ReadCreditCard, nameof(ReadCreditCard)), + (AsATarget, nameof(AsATarget)), + (EmulateNdefTag, nameof(EmulateNdefTag)) }; while (true) @@ -730,3 +733,97 @@ void ProcessUltralight(Pn532 pn532) Console.WriteLine("Error writing NDEF data on card"); } } + +void AsATarget(Pn532 pn532) +{ + byte[]? retData = null!; + TargetModeInitialized? modeInitialized = null!; + while ((!Console.KeyAvailable)) + { + (modeInitialized, retData) = pn532.InitAsTarget( + TargetModeInitialization.PiccOnly | TargetModeInitialization.PassiveOnly, + new TargetMifareParameters() + { + NfcId3 = new byte[] { 0x01, 0x02, 0x03 }, + Atqa = new byte[] { 0x04, 0x00 }, + Sak = 0x20 + }, + new TargetFeliCaParameters() + { + NfcId2 = new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + Pad = new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + SystemCode = new byte[] { 0x00, 0x00 } + }, + new TargetPiccParameters()); + if (modeInitialized is object) + { + break; + } + + // Give time to PN532 to process + Thread.Sleep(300); + } + + if (modeInitialized is null) + { + return; + } + + Console.WriteLine($"PN532 as a target: ISDep: {modeInitialized.IsDep}, IsPicc {modeInitialized.IsISO14443_4Picc}, {modeInitialized.TargetBaudRate}, {modeInitialized.TargetFramingType}"); + Console.WriteLine($"Initiator: {BitConverter.ToString(retData!)}"); + // 25-D4-00-E8-11-6A-0A-69-1C-46-5D-2D-7C-00-00-00-32-46-66-6D-01-01-12-02-02-07-FF-03-02-00-13-04-01-64-07-01-03 + // 11-D4-00-01-FE-A2-A3-A4-A5-A6-A7-00-00-00-00-00-30 + // E0-80 + // In the case of E0-80, the reader is seen as a Type 4A Tag and it's part of the activation. See https://www.st.com/resource/en/datasheet/st25ta64k.pdf section 5.9.2 + // the command is E0 and the param is 80 + Span read = stackalloc byte[512]; + int ret = -1; + while (ret < 0) + { + ret = pn532.ReadDataAsTarget(read); + } + + // For example: 00-00-A4-04-00-0E-32-50-41-59-2E-53-59-53-2E-44-44-46-30-31-00 + Console.WriteLine($"Status: {(ErrorCode)read[0]}, Data: {BitConverter.ToString(read.Slice(1, ret - 1).ToArray())}"); +} + +void EmulateNdefTag(Pn532 pn532) +{ + CancellationTokenSource cts = new(); + Console.CancelKeyPress += (s, e) => + { + e.Cancel = true; + cts.Cancel(); + }; + + EmulatedNdefTag ndef = new(pn532, new byte[] { 0x12, 0x34, 0x45 }); + ndef.CardStatusChanged += NdefCardStatusChanged; + ndef.NdefReceived += NdefNdefReceived; + ndef.NdefMessage.Records.Add(new TextRecord("I love NET IoT and .NET nanoFramework!", "en-us", Encoding.UTF8)); + ndef.NdefMessage.Records.Add(new UriRecord(UriType.Https, "github.com/dotnet/iot")); + ndef.InitializeAndListen(cts.Token); +} + +void NdefNdefReceived(object? sender, NdefMessage e) +{ + Console.WriteLine("New NDEF received!"); + foreach (var record in e.Records) + { + Console.WriteLine($"Record length: {record.Length}"); + if (TextRecord.IsTextRecord(record)) + { + var text = new TextRecord(record); + Console.WriteLine($" Text: {text.Text}"); + } + else if (UriRecord.IsUriRecord(record)) + { + var uri = new UriRecord(record); + Console.WriteLine($" Uri: {uri.Uri}"); + } + } +} + +void NdefCardStatusChanged(object? sender, EmulatedTag.CardStatus e) +{ + Console.WriteLine($"Status of the emulated card changed to {e}"); +}