CS_LEVEL_25_SOLUTION - OnlyCook/abitur-elite-code GitHub Wiki
Level 25 – Musterlösung: Gepäckaufgabe & Hardware (Teil 2)
Lösung
public class BarcodeScanner
{
private Serial serial;
public BarcodeScanner(Serial serial)
{
this.serial = serial;
this.serial.Open();
}
public bool IsReady()
{
return serial.DataAvailable() > 0;
}
public string ReadBarcode()
{
while (serial.Read() != 0x02);
char[] data = new char[7];
for (int i = 0; i < 7; i++)
{
data[i] = (char)serial.Read();
}
int xor = CalcChecksum(data);
if (serial.Read() != xor) return "";
if (serial.Read() != 0x03) return "";
return new string(data);
}
private char CalcChecksum(char[] data)
{
int xor = 0;
for (int i = 0; i < 7; i++)
{
xor ^= data[i];
}
return (char)xor;
}
}
public class ScannerController
{
private GepaeckSchleuse schleuse;
private BarcodeScanner scanner;
private FlughafenVerwaltung verwaltung;
public ScannerController(string comPort, FlughafenVerwaltung verwaltung, GepaeckSchleuse schleuse)
{
this.schleuse = schleuse;
this.verwaltung = verwaltung;
Serial serial = new Serial(comPort, 9600, 8, 1, 0);
this.scanner = new BarcodeScanner(serial);
}
public void Run()
{
while (true)
{
if (scanner.IsReady())
{
string barcode = scanner.ReadBarcode();
if (barcode != "")
{
string[] parts = barcode.Split('_');
bool result = verwaltung.CheckBoarding(Int32.Parse(parts[0]), Double.Parse(parts[1]));
if (result == true)
{
schleuse.Unlock();
System.Threading.Thread.Sleep(5000);
schleuse.Lock();
}
else
{
verwaltung.GetLogs().Add(new Log("Abgewiesen", DateTime.Now));
}
}
}
}
}
}
Erklärung
Überblick
Dieses Level kombiniert die Hardware-Schnittstellen-Arbeit aus Sektion 4 mit dem Flughafen-Szenario aus Sektion 6. Der BarcodeScanner liest ein serielles Protokoll – ähnlich wie der RFIDReader in Level 17, aber mit einem anderen Datenformat. Der ScannerController bindet den Scanner in den Ablauf ein und entscheidet, ob ein Passagier durchgelassen wird.
BarcodeScanner – Konstruktor und IsReady()
public BarcodeScanner(Serial serial)
{
this.serial = serial;
this.serial.Open();
}
public bool IsReady()
{
return serial.DataAvailable() > 0;
}
serial.Open() im Konstruktor kennt du bereits – die serielle Schnittstelle muss explizit geöffnet werden, bevor man darauf lesen kann. Das ist Abitur-Standard.
Für IsReady() müsste man hier Lücken füllen, denn es wird nicht explizit beschrieben, was diese Methode tun soll. Jedoch kennst du aus Level 17 die IsCardAvailable()-Methode, welche genau der selben Logik folgt.
ReadBarcode() – das serielle Protokoll lesen (Level 17 sehr ähnelnd)
Das Protokoll hat folgendes Format: STX | 7 Datenbytes | Prüfbyte | ETX, wobei STX = 0x02 und ETX = 0x03.
Synchronisation mit leerem while
while (serial.Read() != 0x02);
Der Strichpunkt am Ende macht den Schleifenkörper leer – die Schleife liest so lange Bytes und verwirft sie, bis das Startbyte 0x02 (STX) gefunden wird. Das schützt vor dem Fall, dass der Empfang mitten in einem Paket beginnt oder Rauschen auf der Leitung ist. Erst wenn das STX-Byte da ist, weiß man: jetzt kommen die echten Daten.
7 Datenbytes einlesen
char[] data = new char[7];
for (int i = 0; i < 7; i++)
{
data[i] = (char)serial.Read();
}
serial.Read() gibt ein int zurück – der Cast zu char macht daraus ein lesbares Zeichen. Das Array hat genau 7 Stellen, denn das Barcode-Format ist fest: z. B. 14_23.5 (Passagier-ID, Unterstrich, Gewicht).
Prüfsumme und ETX verifizieren
int xor = CalcChecksum(data);
if (serial.Read() != xor) return "";
if (serial.Read() != 0x03) return "";
return new string(data);
Nach den Datenbytes kommt das Prüfbyte. Stimmt es nicht mit der berechneten Prüfsumme überein, ist das Paket korrumpiert – es wird "" zurückgegeben, also eine leere Zeichenkette als Fehlersignal. Danach wird noch das ETX-Byte (0x03) erwartet. Erst wenn beides passt, wird der Barcode-String aus dem char-Array zusammengesetzt und zurückgegeben.
new string(data) erzeugt aus einem char[] direkt einen string – eine praktische Kurzform.
CalcChecksum() – XOR über alle Datenbytes
private char CalcChecksum(char[] data)
{
int xor = 0;
for (int i = 0; i < 7; i++)
{
xor ^= data[i];
}
return (char)xor;
}
^= ist der XOR-Zuweisungsoperator: xor ^= data[i] ist kurz für xor = xor ^ data[i]. XOR hat die Eigenschaft, dass es bei gleichen Eingaben immer 0 ergibt und sich selbst rückgängig macht – daher ist es ideal als einfache Prüfsumme. Das Ergebnis wird am Ende wieder zu char gecastet, damit es mit dem Prüfbyte aus dem Datenstrom (das ebenfalls als char/int gelesen wird) verglichen werden kann.
ScannerController – Konstruktor und Komposition
public ScannerController(string comPort, FlughafenVerwaltung verwaltung, GepaeckSchleuse schleuse)
{
this.schleuse = schleuse;
this.verwaltung = verwaltung;
Serial serial = new Serial(comPort, 9600, 8, 1, 0);
this.scanner = new BarcodeScanner(serial);
}
Der Controller erstellt den Serial-Port intern und reicht ihn direkt an den BarcodeScanner weiter. serial ist dabei nur eine lokale Hilfsvariable – sie wird nicht als Feld gespeichert, weil der Controller selbst nie direkt auf den Port zugreift. Er delegiert das komplett an den Scanner.
Run() – der Steuerungsablauf
public void Run()
{
while (true)
{
if (scanner.IsReady())
{
string barcode = scanner.ReadBarcode();
if (barcode != "")
{
string[] parts = barcode.Split('_');
bool result = verwaltung.CheckBoarding(Int32.Parse(parts[0]), Double.Parse(parts[1]));
if (result == true)
{
schleuse.Unlock();
System.Threading.Thread.Sleep(5000);
schleuse.Lock();
}
else
{
verwaltung.GetLogs().Add(new Log("Abgewiesen", DateTime.Now));
}
}
}
}
}
Barcode parsen
string[] parts = barcode.Split('_');
bool result = verwaltung.CheckBoarding(Int32.Parse(parts[0]), Double.Parse(parts[1]));
Der Barcode hat das Format "14_23.5". Split('_') zerlegt ihn an dem Unterstrich – parts[0] enthält dann "14", parts[1] enthält "23.5". Int32.Parse() wandelt den ID-String in eine Ganzzahl um, Double.Parse() wandelt das Gewicht in eine Kommazahl um. Diese Werte werden direkt an CheckBoarding weitergegeben.
Schleuse und Pausierung
schleuse.Unlock();
System.Threading.Thread.Sleep(5000);
schleuse.Lock();
Thread.Sleep(5000) pausiert das Programm für 5000 Millisekunden (also 5 Sekunden), damit der Passagier tatsächlich durch die Schleuse gehen kann, bevor sie sich wieder schließt. In der App wird hier ein kleinerer Wert verwendet, damit du nicht jedes Mal warten musst – der korrekte Wert in einer echten Implementierung wäre 5000.
Log-Eintrag bei Abweisung
verwaltung.GetLogs().Add(new Log("Abgewiesen", DateTime.Now));
Das ist das bekannte Muster der verketteten Methodenaufrufe: GetLogs() liefert die Liste, auf der direkt .Add(...) aufgerufen wird. DateTime.Now gibt den aktuellen Zeitstempel mit. Das Objekt new Log(...) wird dabei direkt übergeben, ohne es in einer Variable zu speichern – das ist möglich, weil es nach dem Add-Aufruf nicht mehr gebraucht wird.