diff --git a/chameleon-server/src/main/java/com/chameleonvision/Main.java b/chameleon-server/src/main/java/com/chameleonvision/Main.java index 4cefbc29e..53c8953fa 100644 --- a/chameleon-server/src/main/java/com/chameleonvision/Main.java +++ b/chameleon-server/src/main/java/com/chameleonvision/Main.java @@ -2,6 +2,8 @@ package com.chameleonvision; import com.chameleonvision.config.ConfigManager; import com.chameleonvision.network.NetworkManager; +import com.chameleonvision.scripting.ScriptEventType; +import com.chameleonvision.scripting.ScriptManager; import com.chameleonvision.util.Platform; import com.chameleonvision.util.ShellExec; import com.chameleonvision.util.Utilities; @@ -42,6 +44,8 @@ public class Main { if (!hasReportedConnectionFailure && logMessage.message.contains("timed out")) { System.err.println("NT Connection has failed!"); hasReportedConnectionFailure = true; + } else if (logMessage.message.contains("connected")) { + ScriptManager.queueEvent(ScriptEventType.kNTConnected); } } } @@ -102,6 +106,9 @@ public class Main { } public static void main(String[] args) { + + Runtime.getRuntime().addShutdownHook(new Thread(() -> ScriptManager.queueEvent(ScriptEventType.kProgramExit))); + if (CurrentPlatform.equals(Platform.UNSUPPORTED)) { System.err.printf("Sorry, this platform is not supported. Give these details to the developers.\n%s\n", CurrentPlatform.toString()); return; @@ -133,6 +140,14 @@ public class Main { } ConfigManager.initializeSettings(); + + if (!CurrentPlatform.isWindows()) { + ScriptManager.initialize(); + } else { + System.out.println("Scripts not yet supported on Windows. ScriptEvents will be ignored."); + } + + NetworkManager.initialize(manageNetwork); if (ntServerMode) { @@ -148,6 +163,8 @@ public class Main { // NetworkTableInstance.getDefault().startClient("localhost"); } + ScriptManager.queueEvent(ScriptEventType.kProgramInit); + boolean visionSourcesOk = VisionManager.initializeSources(); if (!visionSourcesOk) { System.out.println("No cameras connected!"); @@ -156,13 +173,13 @@ public class Main { boolean visionProcessesOk = VisionManager.initializeProcesses(); if (!visionProcessesOk) { - System.err.println("shit"); + System.err.println("Failed to start threads!"); return; } VisionManager.startProcesses(); - System.out.printf("Starting Webserver at port %d\n", DEFAULT_PORT); + System.out.printf("Starting Web server at port %d\n", DEFAULT_PORT); Server.main(DEFAULT_PORT); } } diff --git a/chameleon-server/src/main/java/com/chameleonvision/scripting/ScriptCommandType.java b/chameleon-server/src/main/java/com/chameleonvision/scripting/ScriptCommandType.java new file mode 100644 index 000000000..017512065 --- /dev/null +++ b/chameleon-server/src/main/java/com/chameleonvision/scripting/ScriptCommandType.java @@ -0,0 +1,14 @@ +package com.chameleonvision.scripting; + +public enum ScriptCommandType { + kDefault(""), + kBashScript("bash"), + kPythonScript("python"), + kPython3Script("python3"); + + public final String value; + + ScriptCommandType(String value) { + this.value = value; + } +} diff --git a/chameleon-server/src/main/java/com/chameleonvision/scripting/ScriptConfig.java b/chameleon-server/src/main/java/com/chameleonvision/scripting/ScriptConfig.java new file mode 100644 index 000000000..313397841 --- /dev/null +++ b/chameleon-server/src/main/java/com/chameleonvision/scripting/ScriptConfig.java @@ -0,0 +1,23 @@ +package com.chameleonvision.scripting; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ScriptConfig { + public final ScriptEventType eventType; + public final String command; + + public ScriptConfig(ScriptEventType eventType) { + this.eventType = eventType; + this.command = ""; + } + + @JsonCreator + public ScriptConfig( + @JsonProperty("eventType") ScriptEventType eventType, + @JsonProperty("command") String command + ) { + this.eventType = eventType; + this.command = command; + } +} diff --git a/chameleon-server/src/main/java/com/chameleonvision/scripting/ScriptEvent.java b/chameleon-server/src/main/java/com/chameleonvision/scripting/ScriptEvent.java new file mode 100644 index 000000000..c117c73de --- /dev/null +++ b/chameleon-server/src/main/java/com/chameleonvision/scripting/ScriptEvent.java @@ -0,0 +1,35 @@ +package com.chameleonvision.scripting; + +import com.chameleonvision.Debug; +import com.chameleonvision.util.ShellExec; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +public class ScriptEvent { + private static final ShellExec executor = new ShellExec(true, true); + + public final ScriptConfig config; + + public ScriptEvent(ScriptConfig config) { + this.config = config; + } + + public int run() throws IOException { + int retVal = executor.executeBashCommand(config.command); + + String output = executor.getOutput(); + String error = executor.getError(); + + if (!error.isEmpty()) { + System.err.printf("Error when running \"%s\" script: %s\n", config.eventType.name(), error); + } else if (!output.isEmpty()) { + Debug.printInfo(String.format("Output from \"%s\" script: %s\n", config.eventType.name(), output)); + } + Debug.printInfo(String.format("Script for %s ran with command line: \"%s\", exit code: %d, output: %s, error: %s\n", config.eventType.name(), config.command, retVal, output, error)); + return retVal; + } +} diff --git a/chameleon-server/src/main/java/com/chameleonvision/scripting/ScriptEventType.java b/chameleon-server/src/main/java/com/chameleonvision/scripting/ScriptEventType.java new file mode 100644 index 000000000..436676ee3 --- /dev/null +++ b/chameleon-server/src/main/java/com/chameleonvision/scripting/ScriptEventType.java @@ -0,0 +1,21 @@ +package com.chameleonvision.scripting; + +public enum ScriptEventType { + kProgramInit("Program Init"), + kProgramExit("Program Exit"), + kNTConnected("NT Connected"), + kLEDOn("LED On"), + kLEDOff("LED Off"), + kEnterDriverMode("Enter Driver Mode"), + kExitDriverMode("Exit Driver Mode"), + kFoundTarget("Found Target"), + kFoundMultipleTarget("Found Multiple Target"), + kLostTarget("Lost Target"), + kPipelineLag("Pipeline Lag"); + + public final String value; + + ScriptEventType(String value) { + this.value = value; + } +} diff --git a/chameleon-server/src/main/java/com/chameleonvision/scripting/ScriptManager.java b/chameleon-server/src/main/java/com/chameleonvision/scripting/ScriptManager.java new file mode 100644 index 000000000..9385a13c3 --- /dev/null +++ b/chameleon-server/src/main/java/com/chameleonvision/scripting/ScriptManager.java @@ -0,0 +1,126 @@ +package com.chameleonvision.scripting; + +import com.chameleonvision.Debug; +import com.chameleonvision.Main; +import com.chameleonvision.config.ConfigManager; +import com.chameleonvision.util.JacksonHelper; +import com.chameleonvision.util.LoopingRunnable; +import com.chameleonvision.util.Platform; +import com.chameleonvision.util.ProgramDirectoryUtilities; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.LinkedBlockingQueue; + +public class ScriptManager { + + private ScriptManager() {} + + private static final List events = new ArrayList<>(); + private static final LinkedBlockingDeque queuedEvents = new LinkedBlockingDeque<>(25); + + public static void initialize() { + ScriptConfigManager.initialize(); + if (ScriptConfigManager.fileExists()) { + for (ScriptConfig scriptConfig : ScriptConfigManager.loadConfig()) { + ScriptEvent scriptEvent = new ScriptEvent(scriptConfig); + events.add(scriptEvent); + } + + new Thread(new ScriptRunner(10L)).start(); + } else { + System.err.println("Something went wrong initializing scripts! Events will not run."); + } + } + + private static class ScriptRunner extends LoopingRunnable { + + ScriptRunner(Long loopTimeMs) { + super(loopTimeMs); + } + + @Override + protected void process() { + try { + + handleEvent(queuedEvents.takeFirst()); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + private void handleEvent(ScriptEventType eventType) { + var toRun = events.parallelStream().filter(e -> e.config.eventType == eventType).findFirst().orElse(null); + if (toRun != null) { + try { + toRun.run(); + } catch (IOException e) { + System.err.printf("Failed to run script for event: %s, exception below.\n%s\n", eventType.name(), e.getMessage()); + } + } + } + } + + protected static class ScriptConfigManager { + + protected static final Path scriptConfigPath = Paths.get(ConfigManager.SettingsPath.toString(), "scripts.json"); + + private ScriptConfigManager() {} + + static boolean fileExists() { return Files.exists(scriptConfigPath); } + + public static void initialize() { + if (!fileExists()) { + List eventsConfig = new ArrayList<>(); + for (var eventType : ScriptEventType.values()) { + eventsConfig.add(new ScriptConfig(eventType)); + } + + try { + JacksonHelper.serializer(scriptConfigPath, eventsConfig.toArray(new ScriptConfig[0])); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + static List loadConfig() { + try { + var raw = JacksonHelper.deserializer(scriptConfigPath, ScriptConfig[].class); + if (raw != null) { + return List.of(raw); + } + } catch (IOException e) { + e.printStackTrace(); + } + return new ArrayList<>(); + } + + protected static void deleteConfig() { + try { + Files.delete(scriptConfigPath); + } catch (IOException e) { + // + } + } + } + + public static void queueEvent(ScriptEventType eventType) { + if (!Platform.getCurrentPlatform().isWindows()) { + try { + queuedEvents.putLast(eventType); + Debug.printInfo("Queued event: " + eventType.name()); + } catch (InterruptedException e) { + System.err.println("Failed to add event to queue: " + eventType.name()); + } + } + } +} diff --git a/chameleon-server/src/main/java/com/chameleonvision/util/Platform.java b/chameleon-server/src/main/java/com/chameleonvision/util/Platform.java index 4bc4e6712..203880e98 100644 --- a/chameleon-server/src/main/java/com/chameleonvision/util/Platform.java +++ b/chameleon-server/src/main/java/com/chameleonvision/util/Platform.java @@ -23,7 +23,7 @@ public enum Platform { private static final String OS_ARCH = System.getProperty("os.arch"); public static final Platform CurrentPlatform = getCurrentPlatform(); - public boolean isWindows() { + public boolean isWindows() { return this == WINDOWS_64; } @@ -35,6 +35,10 @@ public enum Platform { return this == MACOS_64; } + public static boolean isRaspberryPi() { + return CurrentPlatform.equals(LINUX_RASPBIAN); + } + private static ShellExec shell = new ShellExec(true, false); public boolean isRoot() { diff --git a/chameleon-server/src/main/java/com/chameleonvision/util/ProgramDirectoryUtilities.java b/chameleon-server/src/main/java/com/chameleonvision/util/ProgramDirectoryUtilities.java index b78b45e91..3c415a412 100644 --- a/chameleon-server/src/main/java/com/chameleonvision/util/ProgramDirectoryUtilities.java +++ b/chameleon-server/src/main/java/com/chameleonvision/util/ProgramDirectoryUtilities.java @@ -24,19 +24,16 @@ public class ProgramDirectoryUtilities { if (runningFromJAR()) { + if (Platform.isRaspberryPi()) { + return "/boot/chameleon-vision"; + } return getCurrentJARDirectory(); } else { return System.getProperty("user.dir"); -// return getCurrentProjectDirectory(); } } - private static String getCurrentProjectDirectory() - { - return new File("").getAbsolutePath(); - } - private static String getCurrentJARDirectory() { try diff --git a/chameleon-server/src/main/java/com/chameleonvision/vision/VisionProcess.java b/chameleon-server/src/main/java/com/chameleonvision/vision/VisionProcess.java index 1d3d5e64b..f501e8ee9 100644 --- a/chameleon-server/src/main/java/com/chameleonvision/vision/VisionProcess.java +++ b/chameleon-server/src/main/java/com/chameleonvision/vision/VisionProcess.java @@ -4,6 +4,8 @@ import com.chameleonvision.Debug; import com.chameleonvision.config.CameraCalibrationConfig; import com.chameleonvision.config.CameraConfig; import com.chameleonvision.config.ConfigManager; +import com.chameleonvision.scripting.ScriptEventType; +import com.chameleonvision.scripting.ScriptManager; import com.chameleonvision.config.FullCameraConfiguration; import com.chameleonvision.util.LoopingRunnable; import com.chameleonvision.util.MathHandler; @@ -137,6 +139,7 @@ public class VisionProcess { public void setDriverMode(boolean driverMode) { pipelineManager.setDriverMode(driverMode); + ScriptManager.queueEvent(driverMode ? ScriptEventType.kEnterDriverMode : ScriptEventType.kExitDriverMode); SocketHandler.sendFullSettings(); } diff --git a/chameleon-server/src/test/java/com/chameleonvision/scripting/ScriptingTest.java b/chameleon-server/src/test/java/com/chameleonvision/scripting/ScriptingTest.java new file mode 100644 index 000000000..deda0cd12 --- /dev/null +++ b/chameleon-server/src/test/java/com/chameleonvision/scripting/ScriptingTest.java @@ -0,0 +1,29 @@ +package com.chameleonvision.scripting; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static com.chameleonvision.scripting.ScriptManager.*; + +public class ScriptingTest { + + @Test + public void configTest() { + ScriptConfigManager.deleteConfig(); + + Assertions.assertFalse(ScriptConfigManager.fileExists()); + + ScriptConfigManager.initialize(); + + Assertions.assertTrue(ScriptConfigManager.fileExists()); + + var config = ScriptConfigManager.loadConfig(); + Assertions.assertEquals(config.size(), ScriptEventType.values().length); + System.out.println("Script Config PASSED"); + } + + @Test + public void eventTest() { + ScriptManager.queueEvent(ScriptEventType.kProgramInit); + } +}