Skip to content

Commit

Permalink
Merge pull request #12 from Pi4J/feat/demo-launcher
Browse files Browse the repository at this point in the history
Feature: Demo Mode for Launcher
  • Loading branch information
tobiassiegrist authored Jun 12, 2021
2 parents b4ad9cd + 05955ac commit ad9d59d
Show file tree
Hide file tree
Showing 13 changed files with 244 additions and 78 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="crowpi-examples [debug]" type="MavenRunConfiguration" factoryName="Maven">
<configuration default="false" name="CrowPi Debug" type="MavenRunConfiguration" factoryName="Maven">
<MavenSettings>
<option name="myGeneralSettings" />
<option name="myRunnerSettings">
Expand All @@ -16,7 +16,7 @@
</option>
<option name="passParentEnv" value="true" />
<option name="runMavenInBackground" value="true" />
<option name="skipTests" value="false" />
<option name="skipTests" value="true" />
<option name="vmOptions" value="" />
</MavenRunnerSettings>
</option>
Expand All @@ -43,4 +43,4 @@
</MavenSettings>
<method v="2" />
</configuration>
</component>
</component>
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Remote Debug" type="Remote">
<configuration default="false" name="CrowPi Remote Debug" type="Remote">
<module name="crowpi-examples" />
<option name="USE_SOCKET_TRANSPORT" value="true" />
<option name="SERVER_MODE" value="false" />
Expand All @@ -13,4 +13,4 @@
</RunnerSettings>
<method v="2" />
</configuration>
</component>
</component>
47 changes: 47 additions & 0 deletions .run/crowpi-run-demo.run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="CrowPi Run (Demo Mode)" type="MavenRunConfiguration" factoryName="Maven">
<MavenSettings>
<option name="myGeneralSettings" />
<option name="myRunnerSettings">
<MavenRunnerSettings>
<option name="delegateBuildToMaven" value="false" />
<option name="environmentProperties">
<map />
</option>
<option name="jreName" value="#USE_PROJECT_JDK" />
<option name="mavenProperties">
<map>
<entry key="crowpi.launcher.args" value="--demo" />
<entry key="crowpi.remote.host" value="Add CrowPi IP here" />
</map>
</option>
<option name="passParentEnv" value="true" />
<option name="runMavenInBackground" value="true" />
<option name="skipTests" value="true" />
<option name="vmOptions" value="" />
</MavenRunnerSettings>
</option>
<option name="myRunnerParameters">
<MavenRunnerParameters>
<option name="profiles">
<set />
</option>
<option name="goals">
<list>
<option value="install" />
</list>
</option>
<option name="pomFileName" value="pom.xml" />
<option name="profilesMap">
<map>
<entry key="remote-run" value="true" />
</map>
</option>
<option name="resolveToWorkspace" value="false" />
<option name="workingDirPath" value="$PROJECT_DIR$" />
</MavenRunnerParameters>
</option>
</MavenSettings>
<method v="2" />
</configuration>
</component>
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="crowpi-examples [install]" type="MavenRunConfiguration" factoryName="Maven">
<configuration default="false" name="CrowPi Run" type="MavenRunConfiguration" factoryName="Maven">
<MavenSettings>
<option name="myGeneralSettings" />
<option name="myRunnerSettings">
Expand All @@ -16,7 +16,7 @@
</option>
<option name="passParentEnv" value="true" />
<option name="runMavenInBackground" value="true" />
<option name="skipTests" value="false" />
<option name="skipTests" value="true" />
<option name="vmOptions" value="" />
</MavenRunnerSettings>
</option>
Expand All @@ -43,4 +43,4 @@
</MavenSettings>
<method v="2" />
</configuration>
</component>
</component>
3 changes: 1 addition & 2 deletions src/main/java/com/pi4j/crowpi/Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ default String getName() {
* @return Human-readable application description
*/
default String getDescription() {
final String classFqdn = this.getClass().getName();
return "Runs application " + classFqdn;
return this.getClass().getName();
}

/**
Expand Down
175 changes: 126 additions & 49 deletions src/main/java/com/pi4j/crowpi/Launcher.java
Original file line number Diff line number Diff line change
@@ -1,29 +1,22 @@
package com.pi4j.crowpi;

import com.pi4j.Pi4J;
import com.pi4j.context.Context;
import com.pi4j.crowpi.applications.*;
import com.pi4j.crowpi.helpers.CrowPiPlatform;
import com.pi4j.library.pigpio.PiGpio;
import com.pi4j.plugin.pigpio.provider.gpio.digital.PiGpioDigitalInputProvider;
import com.pi4j.plugin.pigpio.provider.gpio.digital.PiGpioDigitalOutputProvider;
import com.pi4j.plugin.pigpio.provider.i2c.PiGpioI2CProvider;
import com.pi4j.plugin.pigpio.provider.pwm.PiGpioPwmProvider;
import com.pi4j.plugin.pigpio.provider.serial.PiGpioSerialProvider;
import com.pi4j.plugin.pigpio.provider.spi.PiGpioSpiProvider;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Model.CommandSpec;

import java.util.*;
import java.util.stream.Collectors;

@Command(name = "CrowPi Example Launcher", mixinStandardHelpOptions = true)
@Command(name = "CrowPi Example Launcher", version = "1.0.0", mixinStandardHelpOptions = true)
public final class Launcher implements Runnable {
/**
* This list must contain all applications which should be executable through the launcher.
* Each class instance must implement the Application interface and gets automatically added as a subcommand.
*/
private static final List<Application> APPLICATIONS = new ArrayList<>(Arrays.asList(
public static final List<Application> APPLICATIONS = new ArrayList<>(Arrays.asList(
new ButtonApp(),
new ButtonMatrixApp(),
new BuzzerApp(),
Expand All @@ -46,11 +39,38 @@ public final class Launcher implements Runnable {
new VibrationMotorApp()
));

/**
* Demo mode will keep the launcher running forever, allowing the consecutive execution of several applications.
* This value gets dynamically set to {@code false} when the user selects to exit the launcher in the interactive menu.
*/
@CommandLine.Option(names = {"-d", "--demo"}, description = "Enable demo mode to run multiple applications consecutively")
private boolean demoMode = false;

/**
* PicoCLI command line instance used for parsing
*/
private final CommandLine cmdLine;

/**
* Pi4J context for CrowPi platform
*/
private final Context pi4j;

/**
* List of available applications to be executed
*/
private final List<Application> applications;

/**
* List of dynamically generated application runners
*/
private final List<ApplicationRunner> runners = new ArrayList<>();

/**
* Main application entry point which executes the launcher and exits afterwards.
*
* @param args Command line arguments
*/
public static void main(String[] args) {
final var launcher = new Launcher(APPLICATIONS);
System.exit(launcher.execute(args));
Expand All @@ -66,23 +86,8 @@ public Launcher(List<Application> applications) {
// Initialize PicoCLI instance
this.cmdLine = new CommandLine(this);

// Initialize PiGPIO
final var piGpio = PiGpio.newNativeInstance();

// Initialize Pi4J context by manually specifying the desired platform and providers
// FIXME: This can probably be replaced by `.newAutoContext()` once https://github.com/Pi4J/pi4j-v2/issues/17 has been resolved
this.pi4j = Pi4J.newContextBuilder()
.noAutoDetect()
.add(new CrowPiPlatform())
.add(
PiGpioDigitalInputProvider.newInstance(piGpio),
PiGpioDigitalOutputProvider.newInstance(piGpio),
PiGpioPwmProvider.newInstance(piGpio),
PiGpioI2CProvider.newInstance(piGpio),
PiGpioSerialProvider.newInstance(piGpio),
PiGpioSpiProvider.newInstance(piGpio)
)
.build();
// Initialize Pi4J context
this.pi4j = CrowPiPlatform.buildNewContext();

// Register application runners as subcommands
this.applications = applications;
Expand All @@ -96,38 +101,76 @@ public Launcher(List<Application> applications) {
*/
@Override
public void run() {
// Print informational header that no application has been specified
System.out.println("> No application has been specified, defaulting to manual selection");
System.out.println("> Run this launcher with --help for further information");

// Print list of possible choices
System.out.println("> The following applications can be launched:");
for (int i = 0; i < this.runners.size(); i++) {
final var runner = this.runners.get(i);
final var appName = runner.getApp().getName();
final var appDescription = runner.getApp().getDescription();

System.out.println((i + 1) + ") " + appName + " (" + appDescription + ")");
// Build list of targets only once for performance reasons
final var targets = buildTargets();

// Print informational header based on current operation mode
if (demoMode) {
System.out.println("> Launcher was started in demo mode and will not automatically exit");
} else {
System.out.println("> No application has been specified, defaulting to interactive selection");
System.out.println("> Run this launcher with --help for further information");
}

// Interactively ask the user for a desired target and run it
// This loop will either run only once or forever, depending on the state of `demoMode`
do {
getTargetInteractively(targets).run();
} while (demoMode);
}

/**
* Presents the passed list of targets to the user as a numbered list and waits until a valid choice via stdin has been made.
* If an invalid input occurs, this method will keep retrying until a valid value has been entered.
*
* @param targets List of targets to present as a choice
* @return Selected target by user
*/
private Target getTargetInteractively(List<Target> targets) {
// Print numbered list of available targets starting at 1
System.out.println("> The following launch targets are available:");
for (int i = 0; i < targets.size(); i++) {
System.out.println((i + 1) + ") " + targets.get(i).getLabel());
}

// Read stdin until user has made a valid choice
// We have to use println() here due to buffering in exec-maven-plugin
// Wait for valid choice of user via stdin
final var in = new Scanner(System.in);
var choice = 0;
while (choice < 1 || choice > this.runners.size()) {
System.out.println("> Please choose your desired application by typing the appropriate number:");
int choice = 0;
while (choice < 1 || choice > targets.size()) {
System.out.println("> Please choose your desired launch target by typing its number:");
try {
choice = in.nextInt();
} catch (InputMismatchException ignored) {
in.next();
}
}

// Launch chosen application
final var runner = this.runners.get(choice - 1);
final var appName = runner.getApp().getName();
System.out.println("> Launching application " + appName + "...");
runner.run();
// Return selected choice
return targets.get(choice - 1);
}

/**
* Builds a list of launcher targets based on static entries and available application runners
*
* @return List of targets
*/
private List<Target> buildTargets() {
final var targets = new ArrayList<Target>();

// Append target for exiting launcher
// This can be achieved by ensuring that demo mode is disabled, as the launcher will exit too once the application exits
targets.add(new Target("Exit launcher without running application", () -> demoMode = false, true));

// Append list of application targets
targets.addAll(this.runners.stream()
.map(runner -> {
final var runnerApp = runner.getApp();
final var runnerLabel = runnerApp.getName() + " (" + runnerApp.getDescription() + ")";
return new Target(runnerLabel, runner);
})
.collect(Collectors.toList()));

return targets;
}

/**
Expand Down Expand Up @@ -160,6 +203,40 @@ private void registerApplicationRunners() {
}
}

/**
* Helper class for representing launcher targets which can be interactively chosen if the user does not specify a single app.
*/
private static final class Target implements Runnable {
private final String label;
private final Runnable runnable;
private final boolean isSilent;

public Target(String label, Runnable runnable) {
this(label, runnable, false);
}

public Target(String label, Runnable runnable, boolean isSilent) {
this.label = label;
this.runnable = runnable;
this.isSilent = isSilent;
}

@Override
public void run() {
if (!isSilent) {
System.out.println("> Launching target [" + getLabel() + "]");
}
runnable.run();
if (!isSilent) {
System.out.println("> Target [" + getLabel() + "] has exited");
}
}

public String getLabel() {
return label;
}
}

/**
* Helper class which wraps around an application to implement the Runnable interface.
* This can not be done directly in the interface as it will need to pass the Pi4J context of the parent class.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ public void execute(Context pi4j) {
// 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() {
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/com/pi4j/crowpi/applications/LedMatrixApp.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,8 @@ public void execute(Context pi4j) {
// In this example, "{HEART}" will be replaced with the actual "Symbol.HEART" value
// If such a pattern exists but no symbol is found with that name, it gets ignored and printed as-is.
matrix.print("CrowPi + Pi4J = {HEART}");

// Disable the LED matrix before exiting
matrix.setEnabled(false);
}
}
Loading

0 comments on commit ad9d59d

Please sign in to comment.