diff --git a/AfRApay.FTM/AfRApay.FTM.csproj b/AfRApay.FTM/AfRApay.FTM.csproj
index d439800..a3e97da 100644
--- a/AfRApay.FTM/AfRApay.FTM.csproj
+++ b/AfRApay.FTM/AfRApay.FTM.csproj
@@ -7,4 +7,10 @@
enable
+
+
+
+
+
+
diff --git a/AfRApay.FTM/Program.cs b/AfRApay.FTM/Program.cs
index 83fa4f4..c766294 100644
--- a/AfRApay.FTM/Program.cs
+++ b/AfRApay.FTM/Program.cs
@@ -1,2 +1,79 @@
-// See https://aka.ms/new-console-template for more information
-Console.WriteLine("Hello, World!");
+using System.CommandLine;
+using PCSC;
+using PCSC.Monitoring;
+using PCSC.Iso7816;
+
+var rootCommand = new RootCommand("Fancy Test Machine for AfRApay");
+
+var listReadersOption = new Option("--list-readers", "List card readers and exit");
+rootCommand.Add(listReadersOption);
+
+rootCommand.SetHandler((listReaders) => {
+ using (var context = ContextFactory.Instance.Establish(SCardScope.System)) {
+ // We need a card reader or this won't work!
+ var readerNames = context.GetReaders();
+ if (readerNames.Length == 0) {
+ Console.Error.WriteLine("Error: no card reader detected");
+ Environment.Exit(1);
+ }
+
+ // If --list-readers is passed, list readers and exit.
+ if (listReaders) {
+ Console.Error.WriteLine("---------- Connected Readers ----------");
+ foreach (var name in readerNames) {
+ Console.WriteLine(name);
+ }
+ Console.Error.WriteLine("---------------------------------------");
+ Environment.Exit(0);
+ }
+
+ // Listen for events on all connected readers.
+ using (var monitor = MonitorFactory.Instance.Create(SCardScope.System)) {
+ monitor.Initialized += (_, args) => Console.WriteLine("[ Reader Initialized: {0} ]", args.ReaderName);
+ monitor.MonitorException += (_, args) => {
+ Console.Error.WriteLine("! ERROR: {0}", args);
+ Environment.Exit(1);
+ };
+ monitor.StatusChanged += (_, args) => Console.WriteLine("~ {0} -> {1}", args.LastState, args.NewState);
+ monitor.CardInserted += (_, args) => {
+ Console.WriteLine("> TAP: {0}", Convert.ToHexString(args.Atr));
+ var reader = new IsoReader(context, args.ReaderName, SCardShareMode.Shared, SCardProtocol.Any);
+ HandleTap(reader);
+ };
+ monitor.CardRemoved += (_, args) => {
+ Console.WriteLine("< OFF");
+ Console.WriteLine(); // Write a blank line between card taps for readability.
+ };
+
+ Console.WriteLine("[ Starting... ]");
+ monitor.Start(readerNames);
+ while (true) {
+ Console.Read();
+ }
+ }
+ }
+}, listReadersOption);
+
+return await rootCommand.InvokeAsync(args);
+
+// Queries a card for data when one is tapped.
+static void HandleTap(IsoReader reader) {
+ // Send a PCSC pseudo-APDU to query the ISO 14443 UID.
+ var rsp = reader.Transmit(new CommandApdu(IsoCase.Case2Short, SCardProtocol.Any) {
+ CLA = 0xFF,
+ Instruction = InstructionCode.GetData,
+ P1 = 0x00,
+ P2 = 0x00,
+ });
+ if (!IsSucc(rsp)) {
+ Console.Error.WriteLine("--> Card Error: SW1={0} SW2={1}", (SW1Code)rsp.SW1, rsp.SW2);
+ return;
+ }
+ var uid = rsp.GetData();
+ Console.WriteLine(" UID: {0}", Convert.ToHexString(uid));
+}
+
+// Was the command successful?
+static bool IsSucc(Response rsp) {
+ return rsp.SW1 == (byte)SW1Code.Normal && rsp.SW2 == 0x00;
+}
diff --git a/shell.nix b/shell.nix
index 5cb47bd..20998be 100644
--- a/shell.nix
+++ b/shell.nix
@@ -3,8 +3,12 @@ pkgs.mkShell {
name = "afrapay";
packages = with pkgs; [
dotnet-sdk_7
+ pcsclite
# ESP32 tooling (for MateCard)
platformio
];
+
+ # AfRApay.FTM tries to dlopen the pcsclite library at runtime.
+ LD_LIBRARY_PATH = [ "${pkgs.pcsclite.out}/lib" ];
}