Bei einer Button-Matrix handelt es sich einfach gesagt um ein Gitter von Knöpfen, also zum Beispiel 4 × 4 Knöpfe wie beim CrowPi. In der simpelsten Form könnte jeder Knopf einzeln mit einem GPIO Pin verbunden werden, jedoch stösst man auf diese Art und Weise schnell an die maximale Kapazität von GPIO Pins eines Raspberry Pi.
Bei einer anderen Methode, welche auch von der Button-Matrix auf dem CrowPi genutzt wird, wird die Kombination der beiden Achsen für das
Auslesen der einzelnen Knöpfe verwendet. In der Komponente werden hierbei sogenannte Selector
und Button
Pins definiert, die
dann zusammen das effiziente Anbinden einer Button-Matrix ermöglichen.
Der Selector
Pin legt hierbei jeweils fest, welche Spalte (vertikal) ausgelesen wird. Nachdem der Pin der gewünschten Spalte
angesteuert wurde, können nun alle Zeilen (horizontal) dieser Spalte mithilfe der einzelnen Button
Pins ausgelesen werden. Um den zweiten Knopf
von links in der ersten Reihe einzulesen, muss zuerst der zweite Selector
Pin aktiviert werden (= Spalte 2), um anschliessend den ersten
Button
Pin (= Zeile 1) auszulesen. Somit werden für ein 4 × 4 Gitter nur 8 statt 12 GPIO Pins benötigt.
Dies bedeutet jedoch auch, dass sich nun nicht mehr einfach so der Zustand eines Pins überwachen lässt, um Events für einen Knopf zu
erhalten. Stattdessen muss nun eine Polling-Methode verwendet werden, sprich es müssen immer wieder Spalte für Spalte alle Knöpfe abgefragt
werden. Diese Komplexität entfällt jedoch bei Nutzung der Komponente, da diese automatisch einen Poller
im Hintergrund startet.
Die Buttons auf dem CrowPi sind auf der Platine beschriftet und von links nach rechts, oben nach unten von 1 bis 16 durchnummeriert. Die
Komponente nimmt bei allen Methoden jeweils diese Nummer entgegen, sprich 7
entspricht dem dritten Knopf von links in der zweiten Zeile.
Für diese Komponente müssen alle DIP-Switches vom linken Block aktiviert werden, da sich die Button-Matrix sonst nicht oder nur teilweise nutzen lässt. Die Stellung der DIP Switches sollte anschliessend so aussehen:
Nachfolgend wird die Verwendung der Klasse
com.pi4j.crowpi.components.ButtonMatrixComponent
Javadoc
beschrieben.
Konstruktor | Bemerkung |
---|---|
ButtonMatrixComponent(com.pi4j.context.Context pi4j) | Initialisiert eine Button-Matrix mit den Standardeinstellungen für den CrowPi. |
ButtonMatrixComponent(com.pi4j.context.Context pi4j, int[] selectorPins, int[] buttonPins, int[] stateMappings, long pollerPeriodMs) | Initialisiert eine Button-Matrix mit frei definierbaren Selector / Button Pins, einem eigenen Mapping sowie einer benutzerdefinierten Polling-Dauer. |
Methode | Bemerkung |
---|---|
void startPoller(long pollerPeriodMs) | Startet den Poller (siehe Funktionsweise) mit dem angegebenen Intervall. Diese Methode wird automatisch vom Konstruktor aufgerufen und muss normalerweise nicht verwendet werden. |
void stopPoller() | Stoppt den Poller sofort und aktualisiert hiermit den Zustand der Buttons nicht mehr. |
int readBlocking() | Wartet endlos darauf dass ein Knopf gedrückt und wieder losgelassen wird, um anschliessend dessen Nummer zurückzugeben. Im Fehlerfall wird der Wert -1 zurückgegeben. |
int readBlocking(long timeoutMs) | Wartet bis zu timeoutMs Millisekunden darauf, dass ein Knopf gedrückt und wieder losgelassen wird. Verhält sich ansonsten gleich wie int readBlocking() . |
int[] getPressedButtons() | Gibt die Nummern aller Knöpfe zurück, die zurzeit aktiv gedrückt werden. Falls keine Knöpfe gedrückt werden, so wird eine leere Liste zurückgegeben. |
boolean isDown(int number) | Gibt true zurück falls der Knopf mit der angegebenen Nummer zurzeit gedrückt wird. |
boolean isUp(int number) | Gibt true zurück falls der Knopf mit der angegebenen Nummer zurzeit nicht gedrückt wird. |
ButtonState getState(int number) | Gibt den aktuellen Zustand vom Knopf mit der angegebenen Nummer zurück. |
void onDown(int number, SimpleEventHandler handler) | Setzt den Event Handler, welcher beim Drücken des angegebenen Knopfs aufgerufen werden soll. null deaktiviert diesen Event Listener. |
void onUp(int number, SimpleEventHandler handler) | Setzt den Event Handler, welcher beim Loslassen des angegebenen Knopfs aufgerufen werden soll. null deaktiviert diesen Event Listener. |
com.pi4j.crowpi.components.ButtonComponent
Javadoc
enthält alle möglichen Zustände, die von einem
Knopf zurückgegeben werden können. Es wird hierbei absichtlich die Enumeration von der einfacheren ButtonComponent
mitverwendet, um eine
möglichst ähnliche Nutzung zu ermöglichen.Die folgende Beispielapplikation ist etwas komplexer und stellt ein vollständiges Spiel auf Basis der Button-Matrix dar. Es gleicht dem
sogenannten Memory Game oder auch dem deutschen Spiel „Ich packe in meinen Koffer“. Zuerst werden mit der Hilfsmethode
determinePlayers()
alle Spielernamen gesammelt, welche am Spiel teilnehmen sollen. Die Methode sorgt hierbei dafür, dass mindestens zwei
Spieler existieren, da dies eine Voraussetzung vom Spiel darstellt. Diese Spieler werden schliesslich in players
gespeichert.
Anschliessend wird eine leere Liste namens history
erzeugt, die jeweils alle vorherigen Knöpfe bzw. deren Nummer beinhaltet. Nun ist
das Spiel vollständig initialisiert und eine while
-Schleife läuft bis nur noch ein aktiver Spieler existiert und alle anderen Spieler
aufgrund eines falschen Zugs verloren haben.
Innerhalb dieser Schleife wird mit einem Iterator jeweils der nächste Spieler ermittelt. Sobald der Iterator, welcher in der originalen Reihenfolge über alle Spieler iteriert, am Ende angekommen ist, wird dieser neu erstellt und startet damit wieder am Anfang. So kommt ein Spieler nach dem anderen zum Zug und dies lässt sich endlos wiederholen.
Nun erfolgen ein paar Ausgaben auf der Kommandozeile und danach eine for
-Schleife, welche den Spieler auffordert, jeden vorherigen Zug der
in history
gespeichert wurde, zu wiederholen. Wird hierbei ein Fehler erkannt, so wird die Schleife vorzeitig verlassen und das Flag
hasFailed
auf true
gesetzt. Falls dem Spieler keine Fehler passieren, endet die Schleife normal und der Wert von hasFailed
bleibt auf
false
.
Ist hasFailed
nach der Schleife nun gesetzt, so wird der Spieler über seinen Fehler informiert, aus der aktiven Spielerliste gelöscht und
mit continue
der nächste Spielzug forciert. War hingegen alles richtig, so kann der Spieler nun einen neuen Knopf wählen und der Zug des
nächsten Spielers beginnt.
Wenn irgendwann nur noch ein aktiver Spieler verbleibt, so wird eine Gewinnmeldung und die totale Punktzahl, welche der Anzahl an wiederholten Elementen entspricht, ausgegeben. Bevor die Applikation endet, wird noch der Poller gestoppt um die Button Matrix nicht länger abzufragen und somit Ressourcen freizugeben.
src/main/java/com/pi4j/crowpi/applications/ButtonMatrixApp.java
package com.pi4j.crowpi.applications;
import com.pi4j.context.Context;
import com.pi4j.crowpi.Application;
import com.pi4j.crowpi.components.ButtonMatrixComponent;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
/**
* This example is actually also known as "memory game" or "Ich packe meinen Koffer mit ..." in German. During startup, it will first ask
* for at least two players which will play against each other. Once all players have been entered, the actual game starts. During each turn
* the player has to repeat all the previously pressed buttons in the same order and then pick a new button. The active player loses if an
* incorrect button is pressed while having to repeat the previous ones. Otherwise, the game will move to the next player. The last player
* remaining automatically wins the game.
*/
public class ButtonMatrixApp implements Application {
@Override
public void execute(Context pi4j) {
// Initialize button matrix component
final var buttonMatrix = new ButtonMatrixComponent(pi4j);
// Initialize game state
final List<String> players = determinePlayers();
final List<Integer> history = new ArrayList<>();
// Repeat the game loop until there is only a single active player left
// This means the game will go on until everyone but one has lost
var playerIterator = players.iterator();
while (players.size() >= 2) {
// Determine active player by fetching the next active player from the list
// An iterator basically loops over all entries and returns one after another each time .next() is called
// When we reach the end of the iterator (hasNext() returns false), we create a new iterator to start from the beginning
if (!playerIterator.hasNext()) {
playerIterator = players.iterator();
}
final var activePlayer = playerIterator.next();
// Print a message which informs about the currently active player whose turn it is
System.out.println();
System.out.println(">>> NEXT TURN: Good luck, " + activePlayer);
// Print a message about the total number of moves which have to be repeated
if (history.size() > 0) {
System.out.println("Please repeat all previous " + history.size() + " button presses in order...");
}
// Make the player repeat the whole history and abort if incorrect
boolean hasFailed = false;
for (int i = 0; i < history.size(); i++) {
// Wait for the player to press a button
final int number = buttonMatrix.readBlocking();
// Compare the button with the history at the currently checked position "i"
// If incorrect, break out of this loop with the failed flag set to true
if (number != history.get(i)) {
hasFailed = true;
break;
}
// Inform the user about the remaining moves if there are any
final int movesLeft = history.size() - i - 1;
if (movesLeft > 0) {
System.out.println("Correct! " + (history.size() - i - 1) + " more numbers to go...");
} else {
System.out.println("Well done, you've repeated all buttons correctly!");
}
}
// If the player has failed, print a message, remove the player and continue with the next turn
if (hasFailed) {
System.out.println("Incorrect! You've lost and are out, " + activePlayer + "!");
playerIterator.remove();
continue;
}
// Let the player choose a new button which shall be added to the history
System.out.println("Press any button of your choice to add it to the list...");
final int number = buttonMatrix.readBlocking();
// Add the chosen number to the history and print it on the CLI
history.add(number);
System.out.println("Turn completed, button " + number + " has been added.");
}
// Print the winner, which will be the last and single element in the list
System.out.println("Congratulations, " + players.get(0) + ", you have won!");
System.out.println("Your score: " + history.size() + " points");
// Stop the button matrix poller now that the application has ended
buttonMatrix.stopPoller();
}
private List<String> determinePlayers() {
// Initialize empty list of players
final var players = new ArrayList<String>();
// Initialize buffered reader for reading from command line
final var reader = new BufferedReader(new InputStreamReader(System.in));
// Gather player names from command line
while (true) {
// Print prompt to ask for the player names
if (players.size() >= 2) {
System.out.print("Please enter another player name or nothing to start the game: ");
} else {
System.out.print("At least 2 players are needed. Please enter a player name: ");
}
// Read the next player name from command line
final String player;
try {
player = reader.readLine();
} catch (IOException e) {
continue;
}
// Determine if we are ready to start the game or need more players
if (players.size() >= 2 && player.isEmpty()) {
break;
} else if (!player.isEmpty()) {
players.add(player);
}
}
return players;
}
}