6.3. Umsetzung¶
6.3.1. Hardware vorbereiten und testen¶
Bauteile¶
1 x Raspberry Pi 3 Model B
1 x PN532 NFC/RFID-Modul
Vorbereitung des Raspberry Pi¶
Die I2C-Schnittstelle wird über den Befehl:
raspi-config
aktiviert.Der SSH-Server wird mit:
echo 'This enables SSH on boot' | sudo tee /boot/ssh
aktiviert.I2C-Utility-Binärdateien werden mit:
sudo apt install i2c-tools
installiert.Das NFC-Modul wird für I2C konfiguriert, indem ein SMB-Header auf der PCB umgelegt wird.
Verkabelung des PN532¶
Raspberry Pi GPIO I2C-Belegung:
Das NFC-Modul besitzt mehrere Schnittstellen zur Verbindung. Für I2C werden die GPIO-Pins mit der 4-Pin-Schnittstelle auf der NFC-Modul-PCB verbunden:
NFC-Modul Pin -> Raspberry Pi GPIO physischer Pin - GND -> 6 - VCC -> 4 - SDA -> 3 - SCL -> 5
Überprüfung der I2C-Geräte¶
root@raspberrypi:~# i2cdetect -y 1
Es wird angezeigt, dass ein Gerät auf der Adresse 0x24 vorhanden ist, was dem NFC-Modul entspricht.
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- 24 -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
6.3.2. Installation von NFC-Tools¶
sudo apt install libnfc6 libnfc-bin libnfc-examples
/etc/nfc/libnfc.conf
wird bearbeitet, um die direkte Ansprache von I2C-Geräten zu ermöglichen:
device.name = "PN532 über I2C"
device.connfstring = "pn532_i2c:/dev/i2c-1"
6.3.3. Auflisten verbundener NFC-Lesegeräte¶
pi@raspberrypi:~ $ nfc-scan-device -v
nfc-scan-device uses libnfc 1.7.1
1 NFC device(s) found:
- pn532_i2c:/dev/i2c-1:
pn532_i2c:/dev/i2c-1
chip: PN532 v1.6
initator mode modulations: ISO/IEC 14443A (106 kbps), FeliCa (424 kbps, 212 kbps), ISO/IEC 14443-4B (106 kbps), Innovision Jewel (106 kbps), D.E.P. (424 kbps, 212 kbps, 106 kbps)
target mode modulations: ISO/IEC 14443A (106 kbps), FeliCa (424 kbps, 212 kbps), D.E.P. (424 kbps, 212 kbps, 106 kbps)
6.3.5. Entwicklung der CLI App mit Golang¶
Anforderungen¶
Die Anwendung soll folgende Features unterstützen:
Songs abspielen: Die Anwendung soll in der Lage sein, Songs abzuspielen, die auf dem Raspberry Pi gespeichert sind und mit einem NFC-Tag assoziert sind.
Songs verwalten: Die Anwendung soll in der Lage sein, Songs hinzuzufügen oder zu entfernen und die Zuordnung zu NFC-Tags zu verwalten.
Auf NFC-Tags reagieren: Die Anwendung soll auf das Scannen eines NFC-Tags reagieren und den entsprechenden Song abspielen.
Design¶
Der Ablauf vom Starten des Programms bis zum Abspielen eines Songs ist in folgendem Flussdiagramm dargestellt:

Zuerst wird das NFC Gerät initialisiert, dann wird auf das Scannen eines NFC-Tags gewartet. Wenn ein Tag gescannt wird, wird die ID des Tags ausgelesen und mit der Liste der Songs verglichen. Wenn ein Song mit der ID des Tags übereinstimmt, wird der Song abgespielt.
Im folgenden Sequenzdiagramm ist sichtbar, welche Akteure an der Kommunikation beteiligt sind und wie die Kommunikation abläuft:

Implementierung¶
Um die Funktionalität umzusetzen mussten mehrere Teilprobleme gelöst werden, für die eigene Packages erstellt wurden: - Controller: Konfiguriert die CLI Kommandos und verwaltet Songs und NFC-Interaktionen - Repository: Verwaltet die Datenbank - View: Spielt Songs ab - Chip: Verwaltet die NFC-Interaktionen
Controller¶
Der Controller ist für die Konfiguration der CLI Kommandos und nimmt damit Benutzereingaben entgegen. Er verwendet die Packages Repository und View, um die Datenbank zu verwalten und Songs abzuspielen.
Der Controller koordiniert damit die Zusammenarbeit der Packages und ist die zentrale Stelle für die Interaktion mit dem Benutzer. Er wird in der main Funktion initialisiert und gestartet.
func main() {
control := controller.NewController()
control.Start()
}
Im Controller werden über die CLI Library die Kommandos konfiguriert und die entsprechenden Funktionen aufgerufen.
Dieses Beispiel für das Hinzufügen eines Songs zeigt, wie erst mithilfe der CLI Library ein Kommando erstellt wird und dann die entsprechende Funktion des Repositories aufgerufen wird, um einen neuen Song in die Datenbank zu speichern.
app := &cli.App{
Name: "AALBox",
Usage: "Manage and play songs",
Commands: []*cli.Command{
{
Name: "add",
Usage: "Add a new song to the database",
Action: func(c *cli.Context) error {
tagId := c.Args().Get(0)
path := c.Args().Get(1)
if err := control.SongRepo.AddSong(tagId, path); err != nil {
fmt.Println("Error adding song:", err)
return err
}
fmt.Println("Song added successfully!")
return nil
},
},
},
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
Die weiteren Kommandos zur Verwaltung von Songs und NFC-Tags wurden analog implementiert und werden hier nicht weiter ausgeführt.
Um einen Song abzuspielen, wird auch analog des oberen Beispiels ein play Kommando hinzugefügt und
die Funktion PlaySong
des View Packages aufgerufen.
Komplexer ist die Implementierung des NFC-Listeners, der auf das Scannen eines NFC-Tags wartet und dann die ID des Tags ausliest, um den entsprechenden Song in der Datenbank zu finden und abzuspielen.
Der NFC-Listener wird in einer eigenen Go-Routine gestartet, um die Hauptfunktion nicht zu blockieren.
Hierzu wird ein Channel erstellt, über den die ID des Tags an den Controller übergeben wird. Die Go-Routine startet dann den NFC-Listener und schreibt die ID des Tags in den Channel, sobald ein Tag gescannt wird.
In der Main-Funktion wird auf neuen Input aus dem Channel in einer for-Schleife gewartet und dann der entsprechende Song abgespielt, falls ein Song mit der ID des Tags in der Datenbank gefunden wurde.
func(c *cli.Context) error {
fmt.Printf("Using NFC library version: %s\n", nfc.Version())
devices, err := nfc.ListDevices()
if err != nil {
log.Fatalf("Failed to list NFC devices: %v", err)
}
if len(devices) == 0 {
log.Fatal("No NFC devices found.")
}
fmt.Println("Available NFC devices:")
for _, device := range devices {
fmt.Println("-", device)
}
rfidChannel := make(chan string)
quitChannel := make(chan os.Signal, 1)
// Create an abstraction of the Reader, DeviceConnection string is empty -> the library to autodetect reader
rfidReader := chip.NewTagReader("", rfidChannel, 19)
// Listen for an RFID/NFC tag in another goroutine
go rfidReader.ListenForTags()
// Ensure the sounds folder exists in the user's home directory and move the file if necessary
homeSoundsDir := filepath.Join(os.Getenv("HOME"), "sounds")
if _, err := os.Stat(homeSoundsDir); os.IsNotExist(err) {
if err := os.Mkdir(homeSoundsDir, os.ModePerm); err != nil {
log.Fatalf("Failed to create directory in home: %s", err)
}
}
// Define the source and destination paths for the MP3 file
defaultSrcPath := "../sounds/wat-wer-bist-du-denn.mp3"
defaultDestPath := filepath.Join(homeSoundsDir, "wat-wer-bist-du-denn.mp3")
// Check if the file already exists in the destination; if not, copy it
if _, err := os.Stat(defaultDestPath); os.IsNotExist(err) {
if err := copyFile(defaultSrcPath, defaultDestPath); err != nil {
log.Printf("Failed to move file: %s", err)
}
}
for {
select {
case tagId := <-rfidReader.TagChannel:
fmt.Println("This is your id:", tagId)
songPath := control.SongRepo.GetSongPath(tagId)
if songPath != "" {
go view.PlaySong(songPath)
} else {
// Play the default song from the home directory if no specific song is found
go view.PlaySong(defaultDestPath)
fmt.Println("No song associated with this tag, playing default sound.")
}
case <-quitChannel:
rfidReader.Cleanup()
default:
time.Sleep(time.Millisecond * 10)
}
}
},
Repository¶
Das Repository ist für die Verwaltung der Datenbank zuständig und bietet Funktionen zum Hinzufügen, Entfernen und Suchen von Songs.
Das Design basiert auf dem Repository Design-Pattern. Das Repository-Pattern schafft eine Abstraktionsebene zwischen der Datenbank und der Controller-Logik und kapselt die Datenbank-Interaktionen. Anstatt direkt auf die Datenbank zuzugreifen, verwendet der Controller das Repository, um Songs hinzuzufügen, zu entfernen und zu suchen und arbeitet dabei mit einem Song Struct, das die Daten eines Songs repräsentiert.
Das Repository bietet folgende Funktionen:
AddSong(tagId string, path string) error: Fügt einen neuen Song mit der übergebenen Tag-ID und dem Pfad zur Datenbank hinzu.
RemoveSong(tagId string) error: Entfernt den Song mit der übergebenen Tag-ID aus der Datenbank.
GetSongPath(tagId string) string: Sucht den Song mit der übergebenen Tag-ID in der Datenbank und gibt den Pfad zurück.
Die Funktionen wiederum verwenden die pg Library, um mit der Datenbank zu interagieren und senden klassisch SQL-Queries ab, wie am folgenden Beispiel für die GetSongPath Funktion zu sehen ist:
// GetSongPath retrieves the song path associated with a given word
func (r *Repository) GetSongPath(tagId string) string {
var songPath string
err := r.Connection.QueryRow("SELECT song_path FROM songs WHERE tag_id = $1", tagId).Scan(&songPath)
if err != nil {
return ""
}
return songPath
}
View¶
Das View Package ist für das Abspielen von Songs zuständig und bietet eine Funktion zum Abspielen eines Songs. Zum Abspielen von Songs wird das Command mpg123 verwendet, das auf dem Raspberry Pi installiert sein muss.
Das View Package bietet folgende Funktion: - PlaySong(path string): Spielt den Song mit dem übergebenen Pfad ab. - StopSong(): Stoppt den aktuell abgespielten Song.
// PlaySong plays the song located at the provided songPath.
// If a song is already playing, it stops the current song and starts the new one.
// It uses the mpg123 command to play the song.
func PlaySong(songPath string) {
StopSong()
mu.Lock()
currentCmd = exec.Command("mpg123", songPath)
currentCmd.Stdout = os.Stdout
currentCmd.Stderr = os.Stderr
mu.Unlock()
err := currentCmd.Start()
if err != nil {
fmt.Println("Error playing song:", err)
return
}
// Wait for the command to finish
if err := currentCmd.Wait(); err != nil {
fmt.Println("Error playing song:", err)
}
}
Chip¶
Das Chip Package verwendet die GPIO und NFC Libraries, um die NFC-Interaktionen zu verwalten.
Das Chip Package bietet folgende Funktionen:
NewTagReader(deviceConnection string, tagChannel chan string, resetPin int): Erstellt einen neuen TagReader, der auf das Scannen eines NFC-Tags wartet und die ID des Tags in den übergebenen Channel schreibt.
ListenForTags(): Startet den NFC-Listener, der auf das Scannen eines NFC-Tags wartet und die ID des Tags in den Channel schreibt.
Cleanup(): Beendet den NFC-Listener und schließt die Verbindung zum NFC-Modul.
Reset(): Setzt das NFC-Modul zurück.
extractUID(target nfc.Targetg) string: Helper-Funktion, um die ID des Tags auszulesen.
Die ListenForTags Funktion verwendet die NFC Library, um auf das Scannen eines NFC-Tags zu warten und die ID des Tags auszulesen. Die ID wird dann über den TagChannel an den Controller übergeben.
// ListenForTags initializes the reader and then continuously listens for NFC tags.
// When a tag is detected, its UID is sent to the TagChannel.
func (reader *TagReader) ListenForTags() {
//Initialize the reader
reader.init()
//Listen for all the modulations specified
var modulations = []nfc.Modulation{
{Type: nfc.ISO14443a, BaudRate: nfc.Nbr106},
{Type: nfc.ISO14443b, BaudRate: nfc.Nbr106},
{Type: nfc.Felica, BaudRate: nfc.Nbr212},
{Type: nfc.Felica, BaudRate: nfc.Nbr424},
{Type: nfc.Jewel, BaudRate: nfc.Nbr106},
{Type: nfc.ISO14443biClass, BaudRate: nfc.Nbr106},
}
for {
// Poll for 300ms
tagCount, target, err := reader.reader.InitiatorPollTarget(modulations, 1, 300*time.Millisecond)
if err != nil {
log.Println("Error polling the reader:", err)
continue
}
// Check if any tag was detected
if tagCount > 0 {
UID := extractUID(target)
// Send the UID of the tag to controller goroutine
if UID != "" {
reader.TagChannel <- UID
}
}
time.Sleep(1 * time.Second)
}
}
ListenForTags wird vom Controller in einer Go-Routine gestartet, um die Hauptfunktion nicht zu blockieren.