diff --git a/.travis.yml b/.travis.yml index 18929a1..4f24ffb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,10 @@ language: java jdk: openjdk9 +cache: + directories: + - $HOME/.m2 + before_install: - echo $GPG_SECRET_KEYS | base64 --decode | $GPG_EXECUTABLE --import - echo $GPG_OWNERTRUST | base64 --decode | $GPG_EXECUTABLE --import-ownertrust @@ -15,19 +19,21 @@ branches: stages: - name: test - name: deploy - if: branch = master AND type != pull_request + if: (branch = master OR branch =~ /^v.*/) AND type != pull_request jobs: include: - stage: test - install: "" + install: skip after_success: bash <(curl -s https://codecov.io/bash) - stage: deploy - install: "" - test: "" + script: skip + install: skip deploy: - provider: script script: mvn --settings .maven.xml site -DskipTests=true -B && git add dokka + on: + all_branches: true - provider: pages skip-cleanup: true github-token: $GITHUB_TOKEN @@ -35,12 +41,12 @@ jobs: verbose: true local_dir: ./dokka on: - branch: master + all_branches: true - stage: deploy - install: - - "" + script: skip + install: skip deploy: - provider: script script: mvn deploy --settings .maven.xml -DskipTests=true -B on: - branch: master + all_branches: true \ No newline at end of file diff --git a/pom.xml b/pom.xml index 8c1fdbd..9c6e10e 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.animatedledstrip animatedledstrip-server - 0.3 + 0.4-SNAPSHOT jar ${project.groupId}:${project.artifactId} @@ -69,7 +69,7 @@ io.github.animatedledstrip animatedledstrip-core - 0.3 + 0.4-SNAPSHOT org.jetbrains.kotlin diff --git a/src/main/java/animatedledstrip/cmdline/CommandLine.kt b/src/main/java/animatedledstrip/cmdline/CommandLine.kt new file mode 100644 index 0000000..944071f --- /dev/null +++ b/src/main/java/animatedledstrip/cmdline/CommandLine.kt @@ -0,0 +1,74 @@ +package animatedledstrip.cmdline + +import kotlinx.coroutines.* +import org.pmw.tinylog.Configurator +import org.pmw.tinylog.Level +import java.io.EOFException +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import java.net.InetSocketAddress +import java.net.Socket +import java.net.SocketException +import kotlin.system.exitProcess + +class CommandLine { + + private val socket = Socket() + + private var endCmdLine = false + + private var readerJob: Job? = null + + init { + Configurator.defaultConfig().level(Level.OFF).activate() + } + + fun loop() { + println("Welcome to the AnimatedLEDStrip Server console") + while (!endCmdLine) { + try { + socket.connect(InetSocketAddress("localhost", 1118), 5000) + } catch (e: Exception) { + continue + } + println("Connected") + try { + val socOut = ObjectOutputStream(socket.getOutputStream()) + val socIn = ObjectInputStream(socket.getInputStream()) + readerJob = GlobalScope.launch { + withContext(Dispatchers.IO) { + try { + while (!endCmdLine) { + println(socIn.readObject() as String? ?: "ERROR") + } + } catch (e: SocketException) { + println("Connection lost: $e") + } catch (e: EOFException) { + println("Connection lost: $e") + } + } + } + + input@ while (!endCmdLine) { + val str = readLine() ?: continue + when (str.toUpperCase()) { + "" -> continue@input + "EXIT" -> exitProcess(0) + "Q", "QUIT" -> { + socOut.writeObject(str) + exitProcess(0) + } + else -> socOut.writeObject(str) + } + } + + } catch (e: SocketException) { + println("Connection lost: $e") + readerJob?.cancel() + } catch (e: EOFException) { + println("Connection lost: $e") + readerJob?.cancel() + } + } + } +} \ No newline at end of file diff --git a/src/main/java/animatedledstrip/server/AnimatedLEDStripServer.kt b/src/main/java/animatedledstrip/server/AnimatedLEDStripServer.kt index 9285fc5..3393f01 100644 --- a/src/main/java/animatedledstrip/server/AnimatedLEDStripServer.kt +++ b/src/main/java/animatedledstrip/server/AnimatedLEDStripServer.kt @@ -31,100 +31,115 @@ import animatedledstrip.colors.ccpresets.CCBlue import animatedledstrip.leds.AnimatedLEDStrip import animatedledstrip.leds.emulated.EmulatedAnimatedLEDStrip import animatedledstrip.utils.delayBlocking -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.newSingleThreadContext import org.apache.commons.cli.DefaultParser -import org.apache.commons.cli.Options import org.pmw.tinylog.Configurator import org.pmw.tinylog.Level import org.pmw.tinylog.Logger +import java.io.File import java.io.FileInputStream import java.io.FileNotFoundException import java.util.* import kotlin.reflect.KClass import kotlin.reflect.full.primaryConstructor -class AnimatedLEDStripServer ( +class AnimatedLEDStripServer( args: Array, ledClass: KClass ) { + /** + * Is the server running + */ internal var running = false - private val options = Options().apply { - addOption("d", "Enable debugging") - addOption("t", "Enable trace debugging") - addOption("v", "Enable verbose log statements") - addOption("q", "Disable log outputs") - addOption("E", "Emulate LED strip but do NOT launch emulator") - addOption("f", true, "Specify properties file") - addOption("i", "Enable image debugging") - addOption("T", "Run test") - } + /* Command line options and properties file */ private val cmdline = DefaultParser().parse(options, args) - var defaultPropertyFileName = "led.config" + private var propertyFileName = cmdline.getOptionValue("f") ?: "led.config" + + private var outputFileName: String? = cmdline.getOptionValue("o") + + /* Set logging levels based on command line */ + init { + val loggingPattern = + if (cmdline.hasOption("v")) "{date:yyyy-MM-dd HH:mm:ss} [{thread}] {class}.{method}()\n{level}: {message}" + else "{{level}:|min-size=8} {message}" + + val loggingLevel = + when { + cmdline.hasOption("t") -> Level.TRACE + cmdline.hasOption("d") -> Level.DEBUG + cmdline.hasOption("q") -> Level.OFF + else -> Level.INFO + } - var defaultOutputFileName: String? = null + Configurator.defaultConfig().formatPattern(loggingPattern).level(loggingLevel).addWriter(SocketWriter()) + .activate() - private val properties = Properties().apply { + + } + + private val properties: Properties? = Properties().apply { try { - load(FileInputStream(cmdline.getOptionValue("f") ?: defaultPropertyFileName)) + load(FileInputStream(propertyFileName)) } catch (e: FileNotFoundException) { - Logger.warn("File ${cmdline.getOptionValue("f") ?: defaultPropertyFileName} not found") + Logger.warn { "File $propertyFileName not found" } } } - val ports = mutableListOf().apply { - Logger.debug(properties.getProperty("ports")) - properties.getProperty("ports")?.split(' ')?.forEach { - Logger.debug(it) + /* Arguments for creating the AnimatedLEDStrip instance */ + + private val emulated: Boolean = cmdline.hasOption("e") || cmdline.hasOption("E") + + private val numLEDs: Int = properties?.getProperty("numLEDs", "240")?.toInt() ?: 240 + + private val pin: Int = properties?.getProperty("pin", "12")?.toInt() ?: 12 + + private val imageDebuggingEnabled: Boolean = cmdline.hasOption("i") + + private val ports = mutableListOf().apply { + properties?.getProperty("ports")?.split(' ')?.forEach { requireNotNull(it.toIntOrNull()) this.add(it.toInt()) } + if (!emulated) this += 1118 // local port } - private val leds = when (cmdline.hasOption("e") || cmdline.hasOption("E")) { + private val rendersBeforeSave = + properties?.getProperty("renders")?.toIntOrNull() ?: cmdline.getOptionValue("r")?.toIntOrNull() ?: 1000 + + private val leds = when (emulated) { false -> ledClass.primaryConstructor!!.call( - properties.getProperty("numLEDs", "240").toInt(), - properties.getProperty("pin", "12").toInt(), - cmdline.hasOption("i"), - cmdline.getOptionValue("o") ?: defaultOutputFileName + numLEDs, + pin, + imageDebuggingEnabled, + outputFileName, + rendersBeforeSave ) true -> EmulatedAnimatedLEDStrip( - properties.getProperty("numLEDs", "240").toInt(), - imageDebugging = cmdline.hasOption("i"), - fileName = cmdline.getOptionValue("o") ?: defaultOutputFileName + numLEDs, + imageDebugging = imageDebuggingEnabled, + fileName = outputFileName ) } - internal val animationHandler = AnimationHandler(leds) + private val persistAnimations = + properties?.getProperty("persist", "false")?.toBoolean() ?: cmdline.hasOption("P") + + internal val animationHandler = AnimationHandler(leds, persistAnimations = persistAnimations) var testAnimation: AnimationData = AnimationData().animation(Animation.COLOR).color(CCBlue) - init { - val pattern = - if (cmdline.hasOption("v")) "{date:yyyy-MM-dd HH:mm:ss} [{thread}] {class}.{method}()\n{level}: {message}" - else "{{level}:|min-size=8} {message}" - - val level = - when { - cmdline.hasOption("t") -> Level.TRACE - cmdline.hasOption("d") -> Level.DEBUG - cmdline.hasOption("q") -> Level.OFF - else -> Level.INFO - } - - Configurator.defaultConfig().formatPattern(pattern).level(level).activate() - } - + /* Start and stop methods */ fun start(): AnimatedLEDStripServer { + val dir = File(".animations") + if (!dir.isDirectory) + dir.mkdirs() + running = true - startLocalTerminalReader() - Logger.debug("Ports = $ports") + Logger.debug { "Ports: $ports" } ports.forEach { SocketConnections.add(it, server = this).open() } @@ -132,12 +147,6 @@ class AnimatedLEDStripServer ( return this } - fun waitUntilStop() { - while (running) { - delayBlocking(1) - } - } - fun stop() { leds.setStripColor(0) delayBlocking(500) @@ -146,20 +155,51 @@ class AnimatedLEDStripServer ( running = false } - @Suppress("EXPERIMENTAL_API_USAGE") - val localThread = newSingleThreadContext("Local Terminal") - - private fun startLocalTerminalReader() { - GlobalScope.launch(localThread) { - while (this@AnimatedLEDStripServer.running) { - Logger.trace("Local terminal waiting for input") - val strIn = readLine() - Logger.trace("Read line") - if (strIn?.toUpperCase() == "Q") { - Logger.trace("'Q' received, shutting down server") - stop() + internal fun parseTextCommand(command: String) { + val line = command.toUpperCase().split(" ") + return when (line[0]) { + "QUIT", "Q" -> { + Logger.info { "Shutting down server" } + stop() + } + "DEBUG" -> { + setLoggingLevel(Level.DEBUG) + Logger.debug("Set logging level to debug") + } + "TRACE" -> { + setLoggingLevel(Level.TRACE) + Logger.trace("Set logging level to trace") + } + "INFO" -> { + setLoggingLevel(Level.INFO) + Logger.info("Set logging level to info") + } + "CLEAR" -> { + animationHandler.addAnimation(AnimationData().animation(Animation.COLOR)) + } + "SHOW" -> { + if (line.size > 1) Logger.info { + "${line[1]}: ${animationHandler.continuousAnimations[line[1]]?.params ?: "NOT FOUND"}" } + else Logger.info { "Running Animations: ${animationHandler.continuousAnimations.keys}" } + } + "END" -> { + if (line.size > 1) { + if (line[1].toUpperCase() == "ALL") { + val animations = animationHandler.continuousAnimations + animations.forEach { + animationHandler.endAnimation(it.value) + } + } else for (i in 1 until line.size) + animationHandler.endAnimation(animationHandler.continuousAnimations[line[i]]) + } else Logger.warn { "Animation ID must be specified" } } + else -> Logger.warn { "$command is not a valid command" } } } + + private fun setLoggingLevel(level: Level) { + Configurator.currentConfig().level(level).activate() + } + } \ No newline at end of file diff --git a/src/main/java/animatedledstrip/server/AnimationHandler.kt b/src/main/java/animatedledstrip/server/AnimationHandler.kt index 0e87af3..158bba4 100644 --- a/src/main/java/animatedledstrip/server/AnimationHandler.kt +++ b/src/main/java/animatedledstrip/server/AnimationHandler.kt @@ -31,6 +31,10 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.newFixedThreadPoolContext import org.pmw.tinylog.Logger +import java.io.File +import java.io.FileInputStream +import java.io.InvalidClassException +import java.io.ObjectInputStream import java.lang.Math.random @@ -38,10 +42,14 @@ import java.lang.Math.random * An object that creates ContinuousRunAnimation instances for animations and * keeps track of currently running animations. */ -internal class AnimationHandler(private val leds: AnimatedLEDStrip) { +internal class AnimationHandler( + private val leds: AnimatedLEDStrip, + threadCount: Int = 100, + internal val persistAnimations: Boolean = false +) { @Suppress("EXPERIMENTAL_API_USAGE") - val animationThreadPool = newFixedThreadPoolContext(100, "AnimationThreads") + val animationThreadPool = newFixedThreadPoolContext(threadCount, "AnimationThreads") /** * Map tracking what continuous animations are currently running @@ -52,6 +60,24 @@ internal class AnimationHandler(private val leds: AnimatedLEDStrip) { val continuousAnimations = mutableMapOf() + init { + GlobalScope.launch { + File(".animations/").walk().forEach { + if (!it.isDirectory && it.name.endsWith(".anim")) try { + ObjectInputStream(FileInputStream(it)).apply { + val obj = readObject() as AnimationData + addAnimation(obj, obj.id) + close() + } + } catch (e: ClassCastException) { + it.delete() + } catch (e: InvalidClassException) { + it.delete() + } + } + } + } + /** * Adds a new animation. * @@ -65,46 +91,46 @@ internal class AnimationHandler(private val leds: AnimatedLEDStrip) { * * @param params An AnimationData instance containing data about the animation to be run */ - fun addAnimation(params: AnimationData) { - - /* Special "Animation" type that the GUI sends to end an animation */ - if (params.animation == Animation.ENDANIMATION) { - Logger.debug("Ending an animation") - continuousAnimations[params.id]?.endAnimation() // End animation - ?: throw Exception("Animation ${params.id} not running") - continuousAnimations.remove(params.id) // Remove it from the continuousAnimations map - return - } - - Logger.trace("Launching new thread for new animation") - GlobalScope.launch(animationThreadPool) { - Logger.debug(params) + fun addAnimation(params: AnimationData, animId: String? = null) { + /* Special "Animation" type that the client sends to end an animation */ + if (params.animation == Animation.ENDANIMATION) + endAnimation(params) + else when (params.animation::class.java.fields[params.animation.ordinal].annotations.find { it is NonRepetitive } is NonRepetitive) { - /* Animations that are only run once because they change the color of the strip */ + /* Animations that are only run once because they change the color of the strip */ true -> { - Logger.trace("Calling Single Run Animation") - leds.run(params) - Logger.trace("Single Run Animation on ${Thread.currentThread().name} complete") + singleRunAnimation(params) } - /* Animations that can be run repeatedly */ + /* Animations that can be run repeatedly */ false -> { if (params.continuous) { - Logger.trace("Calling Continuous Animation") - val id = random().toString().removePrefix("0.") + val id = animId ?: (random() * 100000000).toInt().toString() continuousAnimations[id] = - ContinuousRunAnimation(id, params, leds) + ContinuousRunAnimation(id, params, leds, this) Logger.trace(continuousAnimations) - continuousAnimations[id]!!.startAnimation() - Logger.debug("$id complete") + continuousAnimations[id]!!.runAnimation() } else { - Logger.trace("Calling Single Run Animation") - leds.run(params) - Logger.trace("Single Run Animation on ${Thread.currentThread().name} complete") + singleRunAnimation(params) } } } + } + + private fun singleRunAnimation(params: AnimationData) { + GlobalScope.launch(animationThreadPool) { + leds.run(params) } } + + fun endAnimation(params: AnimationData?) { + continuousAnimations[params?.id ?: "NONE"]?.endAnimation() // End animation + ?: run { Logger.warn { "Animation ${params?.id} not running" }; return } + } + + fun endAnimation(animation: ContinuousRunAnimation?) { + if (animation == null) return + else endAnimation(animation.params) + } } diff --git a/src/main/java/animatedledstrip/server/ContinuousRunAnimation.kt b/src/main/java/animatedledstrip/server/ContinuousRunAnimation.kt index b5b91cc..1fbd4c8 100644 --- a/src/main/java/animatedledstrip/server/ContinuousRunAnimation.kt +++ b/src/main/java/animatedledstrip/server/ContinuousRunAnimation.kt @@ -23,9 +23,16 @@ package animatedledstrip.server */ +import animatedledstrip.animationutils.Animation import animatedledstrip.animationutils.AnimationData import animatedledstrip.leds.AnimatedLEDStrip +import kotlinx.coroutines.* import org.pmw.tinylog.Logger +import java.io.File +import java.io.FileOutputStream +import java.io.ObjectOutputStream +import java.nio.file.Files +import java.nio.file.Paths /** * Class for running an animation that repeats until stopped. @@ -34,43 +41,72 @@ import org.pmw.tinylog.Logger * @param params An AnimationData instance containing data about the animation * to be run */ -class ContinuousRunAnimation(private val id: String, private val params: AnimationData, private val leds: AnimatedLEDStrip) { +internal class ContinuousRunAnimation( + private val id: String, + val params: AnimationData, + private val leds: AnimatedLEDStrip, + private val handler: AnimationHandler +) { /** - * Variable controlling while loops in animation functions. + * Variable controlling if the animation will repeat */ private var continueAnimation = true + private var job: Job? = null - init { - sendAnimation() // Send animation to GUI - } + private val fileName = "$id.anim" /** - * Determine which animation is being called and call the corresponding function. + * Run the animation in a new thread */ - fun startAnimation() { - Logger.trace("params: $params") - while (continueAnimation) leds.run(params) + fun runAnimation() { + job = GlobalScope.launch(handler.animationThreadPool) { + if (handler.persistAnimations) launch { saveAnimationToDisk() } + sendStartAnimation() + while (continueAnimation) leds.run(params) + sendEndAnimation() + handler.continuousAnimations.remove(id) + } } /** - * Stop animation by setting the loop guard to false. + * Stop animation by setting the loop guard to false */ fun endAnimation() { - Logger.debug("Animation $id ending") - continueAnimation = false + Logger.debug { "Animation $id ending" } + if (continueAnimation) continueAnimation = false + else job?.cancel() + if (File(".animations/$fileName").exists()) + Files.delete(Paths.get(".animations/$fileName")) } /** - * Send animation data to GUI. + * Send message to client(s) that animation has started */ - fun sendAnimation(connection: SocketConnections.Connection? = null) { - Logger.trace("Sending animation to GUI") + fun sendStartAnimation(connection: SocketConnections.Connection? = null) { SocketConnections.sendAnimation(params, id, connection) } + /** + * Send message to client(s) that animation has ended + * + * @param connection + */ + fun sendEndAnimation(connection: SocketConnections.Connection? = null) { + SocketConnections.sendAnimation(params.copy(animation = Animation.ENDANIMATION), id, connection) + } + + private suspend fun saveAnimationToDisk() { + withContext(Dispatchers.IO) { + ObjectOutputStream(FileOutputStream(".animations/$fileName")).apply { + writeObject(params) + close() + } + } + } + } diff --git a/src/main/java/animatedledstrip/server/GlobalVars.kt b/src/main/java/animatedledstrip/server/GlobalVars.kt new file mode 100644 index 0000000..2c9ed04 --- /dev/null +++ b/src/main/java/animatedledstrip/server/GlobalVars.kt @@ -0,0 +1,20 @@ +package animatedledstrip.server + +import org.apache.commons.cli.Options + +var server: AnimatedLEDStripServer<*>? = null + +val options = Options().apply { + addOption("d", "Enable debugging") + addOption("t", "Enable trace debugging") + addOption("v", "Enable verbose log statements") + addOption("q", "Disable log outputs") + addOption("E", "Emulate LED strip but do NOT launch emulator") + addOption("f", true, "Specify properties file") + addOption("o", true, "Specify output file name for image debugging") + addOption("r", true, "Specify number of renders between saves") + addOption("i", "Enable image debugging") + addOption("P", "Persist animations across restarts") + addOption("T", "Run test") + addOption("C", "Connect to a running server with a command line") +} \ No newline at end of file diff --git a/src/main/java/animatedledstrip/server/ServerUtils.kt b/src/main/java/animatedledstrip/server/ServerUtils.kt new file mode 100644 index 0000000..ef06da3 --- /dev/null +++ b/src/main/java/animatedledstrip/server/ServerUtils.kt @@ -0,0 +1,9 @@ +package animatedledstrip.server + +import animatedledstrip.utils.delayBlocking + +fun AnimatedLEDStripServer<*>.waitUntilStop() { + while (running) { + delayBlocking(1) + } +} \ No newline at end of file diff --git a/src/main/java/animatedledstrip/server/SocketConnections.kt b/src/main/java/animatedledstrip/server/SocketConnections.kt index 7ce4abb..af2ac6f 100644 --- a/src/main/java/animatedledstrip/server/SocketConnections.kt +++ b/src/main/java/animatedledstrip/server/SocketConnections.kt @@ -23,6 +23,7 @@ package animatedledstrip.server */ +import animatedledstrip.animationutils.Animation import animatedledstrip.animationutils.AnimationData import animatedledstrip.animationutils.id import kotlinx.coroutines.* @@ -46,6 +47,8 @@ object SocketConnections { */ val connections = mutableMapOf() + var localConnection: Connection? = null + /** * Initialize a new connection. Creates a Connection instance with the * specified port, then adds the port and Connection instance to @@ -56,7 +59,8 @@ object SocketConnections { */ fun add(port: Int, server: AnimatedLEDStripServer<*>): Connection { val connection = Connection(port, server) - connections[port] = connection + if (port == 1118) localConnection = connection + else connections[port] = connection return connection } @@ -70,19 +74,16 @@ object SocketConnections { if (hostIP == null) null else InetAddress.getByName(hostIP) ) var clientSocket: Socket? = null - private var disconnected = true + var connected = false + private set private var socOut: ObjectOutputStream? = null - var textBased = false - - val isDisconnected: Boolean - get() = disconnected /** * Open the connection */ fun open() { GlobalScope.launch(connectionThreadPool) { - Logger.debug("Starting port $port") + Logger.debug { "Starting port $port" } openSocket() } } @@ -97,61 +98,94 @@ object SocketConnections { */ private suspend fun openSocket() { withContext(Dispatchers.IO) { - Logger.debug("Socket at port $port started") + Logger.debug { "Socket at port $port started" } while (server.running) { try { clientSocket = serverSocket.accept() - Logger.trace("Accepted new connection on port $port") - Logger.trace("Initializing input stream") val socIn = ObjectInputStream(clientSocket!!.getInputStream()) - Logger.trace("Initializing output stream") socOut = ObjectOutputStream(clientSocket!!.getOutputStream()) - Logger.debug("Sending currently running animations to GUI") + Logger.info { "Connection on port $port Established" } + connected = true // Send all current running continuous animations to newly connected client - server.animationHandler.continuousAnimations.forEach { - it.value.sendAnimation(this@Connection) - } - disconnected = false - Logger.info("Connection on port $port Established") + if (port != 1118) + server.animationHandler.continuousAnimations.forEach { + it.value.sendStartAnimation(this@Connection) + } var input: Any? - while (!disconnected) { - Logger.trace("Waiting for input") + while (connected) { + Logger.trace { "Waiting for input" } try { - input = socIn.readObject() as AnimationData - Logger.trace("Input received") - server.animationHandler.addAnimation(input) + input = when (port) { + 1118 -> socIn.readObject() as String + else -> socIn.readObject() as AnimationData + } + Logger.trace { "Input received" } + when (input) { + is AnimationData -> server.animationHandler.addAnimation(input) + is String -> server.parseTextCommand(input) + } } catch (e: ClassCastException) { - Logger.error("Could not cast input to AnimationData") + Logger.error { "Could not cast input to ${if (port == 1118) "String" else "AnimationData"}" } continue } } } catch (e: SocketException) { // Catch disconnections - Logger.warn("Connection on port $port Lost: $e") - disconnected = true + Logger.warn { "Connection on port $port ${if (port == 1118) "(Local) " else ""}Lost: $e" } + connected = false } catch (e: EOFException) { - Logger.warn("Connection on port $port Lost: $e") - disconnected = true + Logger.warn { "Connection on port $port ${if (port == 1118) "(Local) " else ""}Lost: $e" } + connected = false } } } } /** - * Send animation data to the GUI along with an ID + * Send animation data to the client along with an ID. + * Does not work for port 1118 (local connection). * * @param animation An AnimationData containing data about the animation * @param id The ID for the animation */ fun sendAnimation(animation: AnimationData, id: String) { - if (!isDisconnected) { - Logger.trace("Animation to send: $animation") + check(port != 1118) { "Cannot send animation to local port" } + if (connected) { + Logger.trace { "Animation to send: $animation" } + runBlocking { + withTimeout(5000) { + withContext(Dispatchers.IO) { + socOut?.writeObject( + animation + .id( + if ((animation.animation == Animation.CUSTOMANIMATION || + animation.animation == Animation.CUSTOMREPETITIVEANIMATION) && + animation.id.length == 1 + ) + "${animation.id} $id" + else id + ) + ) + ?: Logger.debug { "Could not send animation $id: Connection socket null" } + if (animation.animation == Animation.ENDANIMATION) Logger.debug { "Sent end of animation $id" } + else Logger.debug { "Sent animation $id" } + } + } + } + } + } + + /** + * Send a string to the local port. + * Only works for a connection with port 1118 (local connection) + */ + fun sendString(str: String) { + check(port == 1118) { "Cannot send string to non-local port" } + if (connected) { runBlocking { withTimeout(5000) { withContext(Dispatchers.IO) { - socOut?.writeObject(animation.id(if (animation.id == "") id else "${animation.id} $id")) - ?: Logger.debug("Could not send animation $id: Connection socket null") - Logger.debug("Sent animation $id") + socOut?.writeObject(str) } } } @@ -177,7 +211,7 @@ object SocketConnections { if (client != null) client.sendAnimation(animation, id) else connections.forEach { it.value.sendAnimation(animation, id) - Logger.trace("Sent animation to client on port ${it.key}") + Logger.trace { "Sent animation to client on port ${it.key}" } } } diff --git a/src/main/java/animatedledstrip/server/SocketWriter.kt b/src/main/java/animatedledstrip/server/SocketWriter.kt new file mode 100644 index 0000000..b4edff1 --- /dev/null +++ b/src/main/java/animatedledstrip/server/SocketWriter.kt @@ -0,0 +1,35 @@ +package animatedledstrip.server + +import org.pmw.tinylog.Configuration +import org.pmw.tinylog.LogEntry +import org.pmw.tinylog.writers.LogEntryValue +import org.pmw.tinylog.writers.PropertiesSupport +import org.pmw.tinylog.writers.Writer + +@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") +@PropertiesSupport(name = "socket", properties = []) +class SocketWriter : Writer { + + val delimiter = ":" + + override fun init(p0: Configuration?) { + } + + override fun getRequiredLogEntryValues(): MutableSet { + return mutableSetOf(LogEntryValue.LEVEL, LogEntryValue.MESSAGE) + } + + override fun write(log: LogEntry?) { + SocketConnections.localConnection?.sendString( + "${log?.level.toString()}$delimiter".padEnd(8, ' ') + + "${log?.message}" + ) + } + + override fun flush() { + } + + override fun close() { + } + +} \ No newline at end of file diff --git a/src/main/resources/tinylog.properties b/src/main/resources/tinylog.properties new file mode 100644 index 0000000..3d0fa89 --- /dev/null +++ b/src/main/resources/tinylog.properties @@ -0,0 +1,7 @@ +tinylog.writer1 = console +tinylog.writer1.format = {{level}:|min-size=8} {message} +tinylog.writer1.level = info + +tinylog.writer2 = socket +tinylog.writer2.format = {{level}:|min-size=8} {message} +tinylog.writer2.level = info \ No newline at end of file diff --git a/src/test/java/AnimatedLEDStripServerTest.kt b/src/test/java/AnimatedLEDStripServerTest.kt index 6171570..29ed327 100644 --- a/src/test/java/AnimatedLEDStripServerTest.kt +++ b/src/test/java/AnimatedLEDStripServerTest.kt @@ -27,16 +27,14 @@ import animatedledstrip.leds.emulated.EmulatedAnimatedLEDStrip import animatedledstrip.server.AnimatedLEDStripServer import animatedledstrip.server.SocketConnections import kotlinx.coroutines.* +import org.junit.Ignore import org.junit.Test -import org.pmw.tinylog.Configurator -import org.pmw.tinylog.Level import java.io.ByteArrayInputStream class AnimatedLEDStripServerTest { init { SocketConnections.hostIP = "0.0.0.0" - Configurator.defaultConfig().level(Level.OFF).activate() } val leds = EmulatedAnimatedLEDStrip(50) @@ -59,6 +57,7 @@ class AnimatedLEDStripServerTest { } @Test + @Ignore fun testLocalTerminalThread() = runBlocking { withTimeout(60000) { val stream = ByteArrayInputStream("q".toByteArray()) diff --git a/src/test/java/AnimationHandlerTest.kt b/src/test/java/AnimationHandlerTest.kt index 80d380d..67667fc 100644 --- a/src/test/java/AnimationHandlerTest.kt +++ b/src/test/java/AnimationHandlerTest.kt @@ -30,16 +30,12 @@ import animatedledstrip.server.AnimationHandler import animatedledstrip.server.SocketConnections import animatedledstrip.utils.delayBlocking import org.junit.Test -import org.pmw.tinylog.Configurator -import org.pmw.tinylog.Level -import kotlin.test.assertFails import kotlin.test.assertTrue class AnimationHandlerTest { init { - Configurator.defaultConfig().level(Level.OFF).activate() SocketConnections.connections.clear() } @@ -92,9 +88,7 @@ class AnimationHandlerTest { @Test fun testRemoveNonExistentAnimation() { val handler = AnimationHandler(leds) - assertFails { - handler.addAnimation(AnimationData().animation(Animation.ENDANIMATION).id("TEST")) - } + handler.addAnimation(AnimationData().animation(Animation.ENDANIMATION).id("TEST")) } } \ No newline at end of file diff --git a/src/test/java/SocketConnectionsTest.kt b/src/test/java/SocketConnectionsTest.kt index 04534d9..1ea9be9 100644 --- a/src/test/java/SocketConnectionsTest.kt +++ b/src/test/java/SocketConnectionsTest.kt @@ -29,8 +29,6 @@ import animatedledstrip.server.SocketConnections import animatedledstrip.utils.delayBlocking import kotlinx.coroutines.* import org.junit.Test -import org.pmw.tinylog.Configurator -import org.pmw.tinylog.Level import java.io.BufferedInputStream import java.io.ObjectInputStream import java.io.ObjectOutputStream @@ -39,14 +37,10 @@ import kotlin.test.assertTrue class SocketConnectionsTest { - init { - Configurator.defaultConfig().level(Level.OFF).activate() - } - @Test fun testAdd() { val server = - AnimatedLEDStripServer(arrayOf("-E"), EmulatedAnimatedLEDStrip::class) + AnimatedLEDStripServer(arrayOf("-Eq"), EmulatedAnimatedLEDStrip::class) SocketConnections.hostIP = "0.0.0.0" SocketConnections.add(1200, server) @@ -57,7 +51,7 @@ class SocketConnectionsTest { fun testOpenSocket() = runBlocking { withTimeout(60000) { val server = - AnimatedLEDStripServer(arrayOf("-E"), EmulatedAnimatedLEDStrip::class).start() + AnimatedLEDStripServer(arrayOf("-Eq"), EmulatedAnimatedLEDStrip::class).start() SocketConnections.hostIP = "0.0.0.0" val c = SocketConnections.add(1201, server) c.open() diff --git a/src/test/resources/tinylog.properties b/src/test/resources/tinylog.properties new file mode 100644 index 0000000..b006e0c --- /dev/null +++ b/src/test/resources/tinylog.properties @@ -0,0 +1,5 @@ +tinylog.writer1 = console +tinylog.writer1.level = off + +tinylog.writer2 = socket +tinylog.writer2.level = off \ No newline at end of file