Project3 - NfcReaderMaui ======================== Ein .NET MAUI-Projekt zur Verwaltung von NFC-Karten mit Guthabenverwaltung. Überblick --------- Die App liest NFC-Karten (Smartcards), speichert deren UID und verwaltet ein fiktives Guthabenmodell. Benutzer können Ein- und Auszahlungen tätigen, Karten sperren sowie einen Admin-Modus aktivieren. Projektstruktur --------------- :: NfcReaderMaui/ ├── NfcReaderMaui/ │ ├── Platforms/ │ ├── Properties/ │ ├── Resources/ │ ├── App.xaml │ ├── App.xaml.cs │ ├── AppShell.xaml │ ├── AppShell.xaml.cs │ ├── MainPage.xaml │ ├── MainPage.xaml.cs │ ├── MauiProgram.cs │ ├── NfcReaderMaui.csproj ├── NfcReaderMaui.sln Entwicklungsumgebung -------------------- **IDE** - Visual Studio 2022 oder neuer (empfohlen mit .NET MAUI Workload installiert) **.NET MAUI** (Multi-platform App UI) - Ermöglicht die Erstellung plattformübergreifender Apps (Windows, Android, macOS) - Eingebauter XAML-Support für UI - Datenbindung & MVU/MVVM-Kompatibilität (optional genutzt) Wahl der Programmiersprache: C# ------------------------------- Für die Umsetzung unseres Projekts haben wir uns bewusst für die Programmiersprache **C#** entschieden. Diese Entscheidung basiert auf einer Kombination aus technischer Eignung, vorhandener Bibliotheksunterstützung sowie moderner Sprachfeatures, die eine effiziente und wartbare Softwareentwicklung ermöglichen. Integration von PC/SC-Smartcard-Funktionalität ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Ein wesentlicher Faktor für die Wahl von C# ist die Verfügbarkeit der leistungsfähigen und aktiv gepflegten Bibliothek `PCSC.NET `_. Diese stellt eine objektorientierte und klar strukturierte Schnittstelle zur Kommunikation mit Smartcards über das PC/SC-Subsystem bereit. Die Bibliothek ermöglicht unter anderem: - direkten Zugriff auf Kartenleser und Kartenstatus - ereignisgesteuerte Verarbeitung (z. B. Karten einlegen/entfernen) - plattformübergreifende Unterstützung (Windows, Linux, macOS über .NET Core/Mono) Im Vergleich zu Java entfallen dadurch aufwendige JNI- oder JNA-Wrapper und die Implementierung wird erheblich vereinfacht. Ereignisverarbeitung durch Lambda-Ausdrücke ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C# bietet durch **Lambda-Ausdrücke** eine elegante und kompakte Möglichkeit, Ereignisse (Events) zu behandeln. Dies ist insbesondere bei der Registrierung von Ereignis-Handlern für Kartenleser von Vorteil. Ein typisches Beispiel: .. code-block:: csharp monitor.CardInserted += (sender, args) => { // Verarbeitungslogik bei Karten-Einfügung }; Im Gegensatz dazu erfordert Java häufig separate Klassen oder anonyme Implementierungen von ``EventListener``-Schnittstellen. Java-Listener wie ``ActionListener`` sind dabei primär auf GUI-Interaktionen ausgelegt und eignen sich weniger gut für systemnahe Ereignisse wie Kartenleserzugriffe. In vielen Fällen ist dort ein manuelles Polling erforderlich, was zusätzliche Synchronisationslogik und potenzielle Fehlerquellen mit sich bringt. C# hingegen bietet durch seine native Ereignisarchitektur mit ``delegate``-basierter Event-Verarbeitung eine klarere und robustere Lösung. Starke Typisierung mit Komfort: das ``var``-Schlüsselwort ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Ein weiteres Merkmal, das zur Wahl von C# beigetragen hat, ist das ``var``-Schlüsselwort. Dieses erlaubt eine implizite Typinferenz bei gleichzeitiger starker Typisierung: .. code-block:: csharp var reader = new CardReader(); Im Gegensatz zu dynamisch typisierten Sprachen wie Python erfolgt die Typableitung hier **zur Compile-Zeit** – der Datentyp bleibt also statisch und wird vom Compiler anhand der Zuweisung eindeutig bestimmt. Dies ermöglicht einen kompakteren Code ohne Verlust an Typensicherheit. In Java hingegen war bis Version 10 stets eine explizite Typangabe erforderlich. Erst seit Java 10 existiert ebenfalls ein ``var``, dessen Einsatz jedoch auf lokale Variablen beschränkt ist und nicht in allen Projekten oder Entwicklungsumgebungen unterstützt wird. Fazit ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C# bietet im Kontext dieses Projekts eine Vielzahl von Vorteilen: - Direkte Integration leistungsfähiger Bibliotheken wie PCSC.NET - Moderne Sprachfeatures wie Lambda-Ausdrücke und Typinferenz - Einfache und sichere Event-Verarbeitung durch native ``delegate``-Mechanismen - Klare, wartbare Syntax mit starker Typprüfung - Hohe Entwicklerproduktivität bei gleichzeitig robuster Laufzeitstabilität Insbesondere für Projekte mit Hardwareanbindung, asynchroner Ereignisverarbeitung und hoher Codequalität stellt C# eine äußerst geeignete und zukunftssichere Wahl dar. Bibliotheken ------------- **NuGet-Pakete / Libraries**: - `PCSC` (https://www.nuget.org/packages/PCSC): Für den Zugriff auf Smartcard-Geräte unter Windows - `PCSC.Iso7816`: Für APDU-Kommandos zur Kommunikation mit NFC-Karten - `PCSC.Monitoring`: Überwacht Leser-Events wie das Ein- oder Ausstecken von Karten - `System.Diagnostics` – Debugging-Unterstützung. .NET MAUI Überblick ------------------- .NET MAUI (Multi-platform App UI) ist Microsofts Framework für Cross-Platform-Entwicklung (Android, iOS, macOS, Windows) mit einem gemeinsamen C#-Codebase. **In dieser App genutzt:** - `Pages:` Die Hauptseite (`MainPage`) ist eine `ContentPage`. Sie stellt die UI bereit, um Karten zu lesen und Transaktionen zu tätigen. - `Shell-Navigation:` Die `AppShell`-Klasse nutzt MAUI Shell zur einfachen Navigation. - `Binding:` Datenbindung (z. B. `Cards` als `ObservableCollection`) ist ein Kernfeature von MAUI und sorgt dafür, dass UI-Elemente automatisch aktualisiert werden, wenn sich die zugrunde liegenden Daten ändern. - `Cross-Platform APIs:` Die App nutzt `MainThread.BeginInvokeOnMainThread()`, um sicherzustellen, dass UI-Änderungen im richtigen Thread erfolgen, was für alle Plattformen konsistent funktioniert. - `Fonts & Ressourcen:` Über `MauiProgram.cs` werden Fonts hinzugefügt, die dann appweit verwendet werden können. Technische Details ------------------ - `Kommunikation`: Die Kartenkommunikation erfolgt im ISO 7816 Case 2 Short-Format. - `UID-Erkennung`: Die UID wird über einen spezifischen APDU-Befehl ausgelesen. - `Sicherheit`: Karten können gesperrt werden, um unautorisierten Zugriff zu verhindern. - `Fehlerbehandlung`: Bei unerwarteten Ausnahmen wird eine Fehlermeldung ausgegeben und das Programm wartet auf Tastendruck. Klassendiagramm ------------------ .. image:: ./img/project3/Project3_class_diagramm.png :alt: Klassendiagramm Ablauf ------- .. image:: ./img/project3/Project3_sequenz_diagramm.png :alt: Sequenz-Diagramm Beim Start der App wird über die `PCSC.ContextFactory` ein Kontext zur Kartenleseumgebung aufgebaut. Anschließend wird der erste verfügbare Leser ermittelt. Wenn kein Leser gefunden wird, erfolgt keine weitere Initialisierung. **Wichtiger Hinweis:** Diese App setzt einen **PCSC-kompatiblen Kartenleser** (z. B. USB-NFC-Reader) voraus. Auf Plattformen wie Android ist die Nutzung von Standard-NFC-Hardware **nicht automatisch unterstützt**, sondern erfordert einen Treiber, der PC/SC implementiert (dies betrifft vor allem Desktop-Betriebssysteme wie Windows). Nach erfolgreicher Initialisierung wird ein Kartenmonitor (`SCardMonitor`) erstellt. Dieser lauscht auf zwei Ereignisse: - `CardInserted`: Wird ausgelöst, wenn eine Karte eingelegt wird. - `CardRemoved`: Wird ausgelöst, wenn eine Karte entfernt wird. Wird eine Karte erkannt, wird die Funktion `UseCard` aufgerufen. Diese Funktion stellt eine Verbindung zur Karte her und sendet einen APDU-Befehl, um die UID der Karte auszulesen. Struktur für UID: - `CLA` = 0xFF **Proprietär (herstellerspezifisch; normalisierte Kommandos sind 0x00)** - `INS` = 0xCA **„Get Data“ (dieses Kommando fordert Daten von der Karte an)** - `P1/P2` = 0x00 **Parameter** - `Le` = 0x00 **Länge der erwarteten Antwort (0 = alle verfügbaren Daten)** Die UID wird aus der Antwort extrahiert und auf bereits bekannte Karten geprüft: - `Bekannte Karte` Der aktuelle Kontostand wird ausgegeben. Falls die Karte gesperrt ist, wird eine Warnung angezeigt. - `Neue Karte` Die Karte wird mit einem Standardguthaben von 500 angelegt und zur `Memory`-Datenbank (ein Dictionary mit UID als Schlüssel) hinzugefügt. Karteninformationen werden in einer einfachen `Card`-Klasse gespeichert. Diese repräsentiert eine einzelne Karte, die durch ihre UID identifiziert wird. Sie enthält Felder zum Kontostand und Sperrstatus. Attribute: - `Uid` (*str*): Die eindeutige ID der Karte. - `Balance` (*int*): Der Kontostand der Karte. Initialwert ist 500. - `IsBlocked` (*bool*): Gibt an, ob die Karte gesperrt ist. Im Programm werden folgende globale Variablen verwendet: - `Memory` Ein Dictionary, das die UID der Karten als Schlüssel und ``Card``-Objekte als Wert speichert. Es dient als temporärer Speicher für alle im System verwendeten Karten. - `currentCard`: Zeigt auf die aktuell verwendete Karte. Wenn keine Karte eingelegt ist, hat dieser Wert den Wert ``null``. Benutzte Fehlerbehandlungen --------------------------- Die Fehlerbehandlung erfolgt vor allem im NFC-Handling: - **try-catch Block in `InitCardReader()`:** .. code-block:: csharp try { // Initialisierung des Kartenlesers } catch (Exception ex) { Console.WriteLine($"Fehler beim Initialisieren des Kartenlesers: {ex.Message}"); } **Bemerkung:** Der Fehler wird hier nur in der Konsole ausgegeben. Das ist für Debugging hilfreich, für die Produktion aber nicht optimal, da der Nutzer keinen Hinweis auf einen Fehler erhält. - **Reader nicht verfügbar beim Start:** Beim Start der App wird geprüft, ob ein NFC-Leser verfügbar ist: .. code-block:: csharp var reader = context.GetReaders().FirstOrDefault(); if (reader == null) return; **Bemerkung:** Falls kein Leser gefunden wird, **bricht die Initialisierung ab** und es wird keine weitere Verbindung aufgebaut. Die App bleibt dadurch stabil, liefert jedoch aktuell keine UI-Fehlermeldung (nur ein Konsolen-Log). - **Reader getrennt während Laufzeit:** Ein ``SCardMonitor`` überwacht kontinuierlich den Kartenleser. Wenn eine Karte entfernt wird, reagiert die App wie folgt: .. code-block:: csharp monitor.CardRemoved += (s, args) => { MainThread.BeginInvokeOnMainThread(() => { currentCard = null; Karte.IsVisible = true; Karte.Text = "Bitte Karte auflegen"; AdminButton.IsVisible = true; TransButtons.IsVisible = false; EingabeFeld.IsVisible = false; }); }; **Bemerkung:** Dies sorgt dafür, dass die UI **sofort zurückgesetzt** wird, um den Nutzer aufzufordern, eine neue Karte aufzulegen. - **Ungültige Benutzereingaben:** Bei Geldtransaktionen prüft die App die Benutzereingabe auf Korrektheit. Zum Beispiel: .. code-block:: csharp if (int.TryParse(result, out amount) && amount > 0) { await HandleAmount(amount); } else { await DisplayAlert("Ungültiger Betrag", "Bitte gib eine gültige Zahl ein.", "OK"); } **Bemerkung:** Zudem wird beim Abheben überprüft, ob genügend Guthaben vorhanden ist: .. code-block:: csharp if (currentCard.Balance >= amount) currentCard.Balance -= amount; else await DisplayAlert("Fehler", "Nicht genug Guthaben!", "OK"); **Bemerkung:** Durch diese Prüfungen wird sichergestellt, dass **ungültige oder problematische Eingaben den App-Ablauf nicht stören**, und der Benutzer erhält bei Fehlern sofort ein Feedback. - **Zusätzliche Stabilität:** Zur Absicherung gegen Threading-Probleme verwendet die App konsequent: .. code-block:: csharp MainThread.BeginInvokeOnMainThread(() => { ... }); Das garantiert, dass alle UI-Änderungen **im Hauptthread** stattfinden, was für plattformübergreifende Apps entscheidend ist, um Abstürze durch Threading-Probleme zu vermeiden. Besondere C#-Mechanismen ------------------------ In der App kommen einige typische **C#-Features** zum Einsatz: - **Lambda-Ausdrücke für Event-Handler:** Beispiel für die Registrierung von Event-Handlern für den NFC-Monitor: .. code-block:: csharp monitor.CardInserted += (s, args) => { MainThread.BeginInvokeOnMainThread(() => { UseCard(screader, args); // Weitere UI-Updates... }); }; Diese Schreibweise macht den Code kürzer und vermeidet separate Methoden. - **ObservableCollection:** Für die Bindung von Karten-Objekten in der UI: .. code-block:: csharp public ObservableCollection Cards { get; set; } = []; Diese Sammlung wird automatisch mit der UI synchronisiert. - **INotifyPropertyChanged:** Die `Card`-Klasse implementiert dieses Interface, um Property-Änderungen zu melden: .. code-block:: csharp public class Card : INotifyPropertyChanged { // ... public event PropertyChangedEventHandler PropertyChanged; } Damit bleibt die UI bei Änderungen (z. B. Sperrstatus) aktuell. - **Nullable-Referenztypen:** Das Projekt nutzt Nullable-Annotationen wie `Card? currentCard`, was die Absicht klarer macht und Compiler-Warnungen bei möglichen Null-Referenzen erzeugt. - **Pattern Matching:** Beim Sperren/Entsperren einer Karte: .. code-block:: csharp var a = sender as Button; var item = a?.BindingContext as Card; UI der App ----------- Start der Anwendung ^^^^^^^^^^^^^^^^^^^^^^^^^^ Beim Start der Anwendung erscheint auf dem Bildschirm die Meldung: **„Bitte Karte auflegen“** Ein initialisierter Monitor überwacht den verbundenen NFC-Reader und erkennt, wenn eine Karte aufgelegt wird. Kartenerkennung und Benutzeroberfläche ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Sobald eine Karte erkannt wird: - Wird die Benutzeroberfläche aktualisiert - Die erkannte Karte wird als Objekt im System hinterlegt Die Benutzeroberfläche bietet nun die Möglichkeit, Transaktionen durchzuführen. Dabei kann der Benutzer zwischen vordefinierten Transaktionen und benutzerdefinierten Eingaben wählen. Kartenentfernung ^^^^^^^^^^^^^^^^ Die aufgelegte Karte kann auf zwei Arten entfernt werden: 1. **Manuell:** Die Karte wird einfach vom NFC-Reader genommen. .. code-block:: csharp monitor.CardRemoved += (s, args) => { MainThread.BeginInvokeOnMainThread(() => { currentCard = null; Karte.IsVisible = true; Karte.Text = "Bitte Karte auflegen"; AdminButton.IsVisible = true; TransButtons.IsVisible = false; EingabeFeld.IsVisible = false; }); }; 2. **Per Auswurf-Knopf:** Über den Knopf oben links wird die Karte ausgeworfen. .. code-block:: csharp public void DiscardCard(object sender, EventArgs e) { currentCard = null; Karte.IsVisible = true; Karte.Text = "Bitte Karte entfernen"; AdminButton.IsVisible = true; TransButtons.IsVisible = false; EingabeFeld.IsVisible = false; } Wird die Karte über den Knopf ausgeworfen, muss sie manuell vom Reader entfernt werden, bevor eine neue Karte erkannt werden kann. Die Benutzeroberfläche weist in diesem Fall entsprechend darauf hin. Admin-Terminal ^^^^^^^^^^^^^^ Der Benutzer hat die Möglichkeit, sich mit einem Passwort in das Admin-Terminal einzuloggen. Nach erfolgreicher Authentifizierung kann der Administrator: - Karten überwachen, die sich aktuell oder kürzlich auf dem Lesegerät befanden - Den Kontostand einer Karte einsehen - Den letzten Kontaktzeitpunkt der Karte abrufen - Karten sperren lassen, um zukünftige Transaktionen zu verhindern .. image:: ./img/project3/karte_auflegen.png :alt: Startseite .. image:: ./img/project3/karte_aktion.png :alt: Karte aufgelegt .. image:: ./img/project3/karte_abheben.png :alt: Betrag abheben .. image:: ./img/project3/karte_entfernen.png :alt: terminal .. image:: ./img/project3/karte_abheben_betrag.png :alt: Bestimmten Betrag abheben .. image:: ./img/project3/karte_abheben_fehler.png :alt: Fehler .. image:: ./img/project3/admin_access.png :alt: Admin-Modus .. image:: ./img/project3/admin_terminal.png :alt: Terminal Quellen ------- - [C#](https://www.geeksforgeeks.org/csharp-programming-language/?) - [Maui](https://learn.microsoft.com/de-de/dotnet/maui/?view=net-maui-9.0) - [PCSC Library](https://github.com/danm-de/pcsc-sharp) - [PC/SC Spezifikation](https://www.pcscworkgroup.com/specifications/) - [ACR122 Reader](https://www.acs.com.hk/en/products/3/acr122u-usb-nfc-reader/)