GamePUI.java
package ch.ladestation.connectncharge.pui;
import ch.ladestation.connectncharge.controller.ApplicationController;
import ch.ladestation.connectncharge.model.game.gamelogic.*;
import ch.ladestation.connectncharge.services.file.CSVReader;
import ch.ladestation.connectncharge.util.mvcbase.PuiBase;
import com.github.mbelling.ws281x.LedStrip;
import com.github.mbelling.ws281x.LedStripType;
import com.github.mbelling.ws281x.Ws281xLedStrip;
import com.pi4j.context.Context;
import com.pi4j.io.gpio.digital.DigitalInput;
import com.pi4j.io.gpio.digital.PullResistance;
import com.pi4j.io.spi.Spi;
import com.pi4j.io.spi.SpiBus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.helpers.MessageFormatter;
import java.io.IOException;
import java.util.*;
public class GamePUI extends PuiBase<Game, ApplicationController> {
public static final String DEBUG_MSG_REACT_TO_ARR_CHANGE =
"reacting to change of {}; oldValue.length={} newValue.length={}";
public static final String DEBUG_MSG_REACT_TO_CHANGE =
"reacting to change of {}; oldValue={} newValue={}";
/**
* Logger instance
*/
private static final Logger LOG = LoggerFactory.getLogger(GamePUI.class);
private final String hOUSEFLAG = "H";
private LEDAnimator ledAnimator;
private List<MCP23S17> chips;
private List<Edge> edges;
private List<Node> nodes;
private Map<Integer, Map<Integer, Edge>> pinToEdgeLUT;
private Map<Integer, Segment> segmentIdLUT;
private DigitalInput[] interruptPins;
private Spi spiInterface;
private Game modelInstance;
public GamePUI(ApplicationController controller, Context pi4J, LEDAnimator animator) {
super(controller, pi4J);
this.ledAnimator = animator;
setupOwnModelToUiBindings(this.modelInstance);
}
/**
* Will set up and initialise the LED-Strip
*
* @return the {@link LedStrip} object
*/
public static LedStrip setupLEDStrip() {
LedStrip ledStrip = new Ws281xLedStrip(845, 10, 800000, 10, false, LedStripType.WS2811_STRIP_GRB, true);
return ledStrip;
}
@Override
public void initializeParts() {
this.chips = setupGPIOExtensionICs(pi4J);
this.edges = new ArrayList<>();
this.nodes = new ArrayList<>();
this.segmentIdLUT = new HashMap<>();
this.pinToEdgeLUT = new HashMap<>();
instanceSegments();
}
@Override
public void setupUiToActionBindings(ApplicationController controller) {
addInterruptsToPinViews(controller);
}
/**
* this is only used to store the model instance to call
* {@link GamePUI#setupOwnModelToUiBindings(Game)} later, because
* otherwise it is impossible to mock the LED-strip class.
* <p>
* If the mock is passed to the Ctor
* it cannot be assigned to {@code this.ledstrip} in time before super calls
* setupModelToUiBindings(). So instead call it later but store the model until then.
*
* @param model
*/
@Override
public void setupModelToUiBindings(Game model) {
this.modelInstance = model;
}
/**
* This method binds the model reactively to the ledstrips.
*
* @param model
*/
public void setupOwnModelToUiBindings(Game model) {
onChangeOf(model.activatedEdges).execute(((oldValue, newValue) -> {
LOG.debug(DEBUG_MSG_REACT_TO_ARR_CHANGE, "Game.activatedEdges", oldValue.length, newValue.length);
if (oldValue.length < newValue.length) {
Sounder.playActivate();
} else if (oldValue.length > newValue.length) {
Sounder.playDeactivate();
}
ledAnimator.scheduleEdgesToBeAnimated(oldValue, newValue);
}));
onChangeOf(model.terminals).execute(((oldValue, newValue) -> {
LOG.debug(DEBUG_MSG_REACT_TO_ARR_CHANGE, "Game.terminals", oldValue.length, newValue.length);
ledAnimator.simplyToggleMultipleSegments(oldValue, false);
ledAnimator.simplyToggleMultipleSegments(newValue, true);
}));
onChangeOf(model.isEdgeBlinking).execute((oldValue, newValue) -> {
LOG.debug(DEBUG_MSG_REACT_TO_CHANGE, "Game.isEdgeBlinking", oldValue, newValue);
ledAnimator.simplyToggleSegment(model.blinkingEdge, newValue);
});
onChangeOf(model.isTippOn).execute((oldValue, newValue) -> {
LOG.debug(DEBUG_MSG_REACT_TO_CHANGE, "Game.isTippOn", oldValue, newValue);
ledAnimator.simplyToggleSegment(model.tippEdge, newValue);
});
onChangeOf(model.activeHint).execute((oldValue, newValue) -> {
if (newValue != Hint.HINT_EMPTY_HINT) {
Sounder.playNotification();
}
});
onChangeOf(model.isFinished).execute(((oldValue, newValue) -> {
if (Boolean.TRUE.equals(newValue)) {
Sounder.playWin();
}
}));
onChangeOf(model.muted).execute((oV, nV) -> Sounder.changeMuted(nV));
}
/**
* Configures pins of the MCP23S17 ICs to listen for interrupts and
* adds a listener to every single one of them that calls
* handleEdgePressed with the correct chip no. & pin no.
*/
private void addInterruptsToPinViews(ApplicationController controller) {
try {
for (int i = 0; i < chips.size(); i++) {
var pinViews = chips.get(i).getAllPinsAsPulledUpInterruptInput();
for (var pinView : pinViews) {
addEdgePressListenerToPinView(i, pinView, controller);
}
}
} catch (IOException e) {
throw new RuntimeException("Error when trying to configure MCP23S17 pins: " + e.getMessage());
}
}
/**
* given a chip index and a {@link MCP23S17.PinView} object, this method will add a listener to the latter
* that calls the {@link GamePUI#handleEdgePressed} method with the corresponding {@link Edge} instance.
*
* @param indexOfIC the index into the {@link GamePUI#chips} list where the {@link MCP23S17} instance
* that the {@link MCP23S17.PinView} argument belongs to is stored.
* @param pinView the {@link MCP23S17.PinView} object the interrupt originated from.
*/
private void addEdgePressListenerToPinView(int indexOfIC, MCP23S17.PinView pinView,
ApplicationController controller) {
pinView.addListener((state, pin) -> {
LOG.debug("Interrupt triggered for Chip={}, Pin={}. state={}", indexOfIC, pin.getPinNumber(), state);
var edge = lookUpChipAndPinNumberToEdge(indexOfIC, pin.getPinNumber());
if (edge == null) {
var msg = MessageFormatter.arrayFormat("No Edge registered for Chip {}, Pin {}. "
+ "Please revise src/main/resources{} and/or the elctrical connections.",
new Object[] {indexOfIC, pin.getPinNumber(), CSVReader.LEDSEGMENTS_CSV})
.getMessage();
LOG.error(msg);
throw new NoSuchElementException(msg);
}
if (state) {
handleEdgePressed(edge, controller);
}
});
}
/**
* method that gets called every time someone tries to toggle an edge by pushing it down.
* NOTE: it is only called on release of the edge.
*
* @param edge the instance that represents the pressed edge
*/
private void handleEdgePressed(Edge edge, ApplicationController controller) {
LOG.info("edge {} between {} & {} was pressed",
edge.getSegmentIndex(),
edge.getFromNodeId(),
edge.getToNodeId());
controller.edgePressed(edge);
}
/**
* get the spi interface of the MCP23S17 chips
*
* @return the pi4j {@link Spi} object
*/
public Spi getSpiInterface() {
return spiInterface;
}
/**
* Will set up and initialise the MCP23S17 GPIO-Extension ICs
*
* @return two fully configured lists of {@link MCP23S17.PinView} objects.
* that means 2 * 16 extra GPIO Pins set as input, pulled up and interrupt enabled
* @throws IOException when the creation of the {@link MCP23S17} objects or
* gathering of the {@link MCP23S17.PinView} objects fail
*/
private List<MCP23S17> setupGPIOExtensionICs(Context pi4J) {
var interruptPinConfig =
DigitalInput.newConfigBuilder(pi4J).id("interrupt0").name("a MCP interrupt").address(22)
.pull(PullResistance.PULL_UP);
var interruptPinChip0 = pi4J.create(interruptPinConfig);
var interruptPinChip1 = pi4J.create(interruptPinConfig.address(23).id("interrupt1"));
var interruptPinChip2 = pi4J.create(interruptPinConfig.address(24).id("interrupt2"));
var interruptPinChip3 = pi4J.create(interruptPinConfig.address(25).id("interrupt3"));
var interruptPinChip4 = pi4J.create(interruptPinConfig.address(27).id("interrupt4"));
interruptPins = new DigitalInput[] {interruptPinChip0, interruptPinChip1, interruptPinChip2, interruptPinChip3,
interruptPinChip4};
List<MCP23S17> interruptChips;
try {
interruptChips = MCP23S17.multipleNewOnSameBusWithTiedInterrupts(
pi4J, SpiBus.BUS_1, interruptPins, 5, true);
} catch (IOException e) {
throw new RuntimeException("Fatal error when instantiating MCP23S17 chips: " + e.getMessage());
}
spiInterface = interruptChips.get(0).getSpi();
return interruptChips;
}
/**
* Get the pins to which the chips are connected
*
* @return an array of {@link DigitalInput} objects
*/
public DigitalInput[] getInterruptPins() {
return interruptPins;
}
public void instanceSegments() {
var records = CSVReader.readCSV();
int runningTotal = 0;
var retSegments = new ArrayList<Segment>();
for (int i = 1; i < records.size() - 1; i++) {
var record = records.get(i);
int startIndex = runningTotal;
runningTotal += Integer.parseInt(record.get(1));
int endIndex = runningTotal - 1;
if (record.get(2).equals(hOUSEFLAG)) {
int segmentId = Integer.parseInt(record.get(0));
var segment = new Node(segmentId, startIndex, endIndex);
nodes.add(segment);
segmentIdLUT.put(segmentId, segment);
} else {
int segmentId = Integer.parseInt(record.get(0));
int chip = Integer.parseInt(record.get(2));
int pin = Integer.parseInt(record.get(3));
int cost = Integer.parseInt(record.get(4));
int fromNode = Integer.parseInt(record.get(5));
int toNode = Integer.parseInt(record.get(6));
var segment = new Edge(segmentId, startIndex, endIndex, cost, fromNode, toNode);
edges.add(segment);
segmentIdLUT.put(segmentId, segment);
populateLUT(chip, pin, segment);
}
}
linkNodeReferencesInAllEdges();
}
private void linkNodeReferencesInAllEdges() {
for (var edge : edges) {
edge.setFromNode((Node) lookUpSegmentIdToSegment(edge.getFromNodeId()));
edge.setToNode((Node) lookUpSegmentIdToSegment(edge.getToNodeId()));
}
}
private void populateLUT(int chip, int pin, Edge segment) {
pinToEdgeLUT.putIfAbsent(chip, new HashMap<>());
pinToEdgeLUT.computeIfPresent(chip, (key, oldValue) -> {
oldValue.put(pin, segment);
return oldValue;
});
}
public Edge lookUpChipAndPinNumberToEdge(int chipNo, int pinNo) {
return pinToEdgeLUT.get(chipNo).get(pinNo);
}
public Segment lookUpSegmentIdToSegment(int segmentId) {
return segmentIdLUT.get(segmentId);
}
public Edge lookUpEdge(int fromIndex, int toIndex) {
return edges.stream().filter(e -> (e.getFromNodeId() == fromIndex && e.getToNodeId() == toIndex)
|| (e.getFromNodeId() == toIndex && e.getToNodeId() == fromIndex)).findFirst().orElseThrow();
}
public List<Edge> getAllEdges() {
return edges;
}
}