diff --git a/.gitignore b/.gitignore index 1c4b3d23..c77a1310 100644 --- a/.gitignore +++ b/.gitignore @@ -105,6 +105,8 @@ fabric.properties *.log +*.pem + **/Build.java **/build/* diff --git a/Common/src/main/java/com/github/serivesmejia/eocvsim/util/LogExt.kt b/Common/src/main/java/com/github/serivesmejia/eocvsim/util/LogExt.kt index 40dac066..d1d9c956 100644 --- a/Common/src/main/java/com/github/serivesmejia/eocvsim/util/LogExt.kt +++ b/Common/src/main/java/com/github/serivesmejia/eocvsim/util/LogExt.kt @@ -1,9 +1,16 @@ -package com.github.serivesmejia.eocvsim.util - -import org.slf4j.LoggerFactory -import kotlin.reflect.KClass - -fun Any.loggerFor(clazz: KClass<*>) = lazy { LoggerFactory.getLogger(clazz.java) } -fun Any.loggerForThis() = lazy { LoggerFactory.getLogger(this::class.java) } - -fun loggerOf(name: String) = lazy { LoggerFactory.getLogger(name) } \ No newline at end of file +package com.github.serivesmejia.eocvsim.util + +import org.slf4j.LoggerFactory +import kotlin.reflect.KClass + +fun Any.loggerFor(clazz: KClass<*>) = lazy { + LoggerFactory.getLogger(clazz.java) +} + +fun Any.loggerForThis() = lazy { + LoggerFactory.getLogger(this::class.java) +} + +fun loggerOf(name: String) = lazy { + LoggerFactory.getLogger(name) +} \ No newline at end of file diff --git a/Common/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventHandler.kt b/Common/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventHandler.kt index a004239d..870be586 100644 --- a/Common/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventHandler.kt +++ b/Common/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventHandler.kt @@ -1,241 +1,240 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.util.event - -import com.github.serivesmejia.eocvsim.util.loggerOf -import java.lang.ref.WeakReference - -/** - * Event handler class to manage event listeners - * @param name the name of the event handler - */ -class EventHandler(val name: String) : Runnable { - - companion object { - private val bannedClassLoaders = mutableListOf>() - - /** - * Ban a class loader from being able to add listeners - * and lazily remove all listeners from that class loader - */ - fun banClassLoader(loader: ClassLoader) = bannedClassLoaders.add(WeakReference(loader)) - - /** - * Check if a class loader is banned - */ - fun isBanned(classLoader: ClassLoader): Boolean { - for(bannedClassLoader in bannedClassLoaders) { - if(bannedClassLoader.get() == classLoader) return true - } - - return false - } - } - - val logger by loggerOf("${name}-EventHandler") - - private val lock = Any() - private val onceLock = Any() - - /** - * Get all the listeners in this event handler - * thread-safe operation (synchronized) - */ - val listeners: Array - get() { - synchronized(lock) { - return internalListeners.toTypedArray() - } - } - - /** - * Get all the "doOnce" listeners in this event handler - * thread-safe operation (synchronized) - */ - val onceListeners: Array - get() { - synchronized(onceLock) { - return internalOnceListeners.toTypedArray() - } - } - - var callRightAway = false - - private val internalListeners = ArrayList() - private val internalOnceListeners = ArrayList() - - /** - * Run all the listeners in this event handler - */ - override fun run() { - for(listener in listeners) { - try { - runListener(listener, false) - } catch (ex: Exception) { - if(ex is InterruptedException) { - logger.warn("Rethrowing InterruptedException...") - throw ex - } else { - logger.error("Error while running listener ${listener.javaClass.name}", ex) - } - } - } - - val toRemoveOnceListeners = mutableListOf() - - //executing "doOnce" listeners - for(listener in onceListeners) { - try { - runListener(listener, true) - } catch (ex: Exception) { - if(ex is InterruptedException) { - logger.warn("Rethrowing InterruptedException...") - throw ex - } else { - logger.error("Error while running \"once\" ${listener.javaClass.name}", ex) - removeOnceListener(listener) - } - } - - toRemoveOnceListeners.add(listener) - } - - synchronized(onceLock) { - for(listener in toRemoveOnceListeners) { - internalOnceListeners.remove(listener) - } - } - } - - /** - * Add a listener to this event handler to only be run once - * @param listener the listener to add - */ - fun doOnce(listener: EventListener) { - if(callRightAway) - runListener(listener, true) - else synchronized(onceLock) { - internalOnceListeners.add(listener) - } - } - - /** - * Add a runnable as a listener to this event handler to only be run once - * @param runnable the runnable to add - */ - fun doOnce(runnable: Runnable) = doOnce { runnable.run() } - - /** - * Add a listener to this event handler to be run every time - * @param listener the listener to add - */ - fun doPersistent(listener: EventListener): EventListenerRemover { - synchronized(lock) { - internalListeners.add(listener) - } - - if(callRightAway) runListener(listener, false) - - return EventListenerRemover(this, listener, false) - } - - /** - * Add a runnable as a listener to this event handler to be run every time - * @param runnable the runnable to add - */ - fun doPersistent(runnable: Runnable) = doPersistent { runnable.run() } - - /** - * Remove a listener from this event handler - * @param listener the listener to remove - */ - fun removePersistentListener(listener: EventListener) { - if(internalListeners.contains(listener)) { - synchronized(lock) { internalListeners.remove(listener) } - } - } - - /** - * Remove a "doOnce" listener from this event handler - * @param listener the listener to remove - */ - fun removeOnceListener(listener: EventListener) { - if(internalOnceListeners.contains(listener)) { - synchronized(onceLock) { internalOnceListeners.remove(listener) } - } - } - - /** - * Remove all listeners from this event handler - */ - fun removeAllListeners() { - removeAllPersistentListeners() - removeAllOnceListeners() - } - - /** - * Remove all persistent listeners from this event handler - */ - fun removeAllPersistentListeners() = synchronized(lock) { - internalListeners.clear() - } - - /** - * Remove all "doOnce" listeners from this event handler - */ - fun removeAllOnceListeners() = synchronized(onceLock) { - internalOnceListeners.clear() - } - - /** - * Add a listener to this event handler to only be run once - * with kotlin operator overloading - * @param listener the listener to add - */ - operator fun invoke(listener: EventListener) = doPersistent(listener) - - private fun runListener(listener: EventListener, isOnce: Boolean) { - if(isBanned(listener.javaClass.classLoader)) { - removeBannedListeners() - return - } - - listener.run(EventListenerRemover(this, listener, isOnce)) - } - - private fun removeBannedListeners() { - for(listener in internalListeners.toArray()) { - if(isBanned(listener.javaClass.classLoader)) { - internalListeners.remove(listener) - logger.warn("Removed banned listener from ${listener.javaClass.classLoader}") - } - } - - for(listener in internalOnceListeners.toArray()) { - if(isBanned(listener.javaClass.classLoader)) internalOnceListeners.remove(listener) - logger.warn("Removed banned listener from ${listener.javaClass.classLoader}") - } - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.util.event + +import com.github.serivesmejia.eocvsim.util.loggerOf +import java.lang.ref.WeakReference + +/** + * Event handler class to manage event listeners + * @param name the name of the event handler + */ +class EventHandler(val name: String) : Runnable { + + companion object { + private val bannedClassLoaders = mutableListOf>() + + /** + * Ban a class loader from being able to add listeners + * and lazily remove all listeners from that class loader + */ + fun banClassLoader(loader: ClassLoader) = bannedClassLoaders.add(WeakReference(loader)) + + /** + * Check if a class loader is banned + */ + fun isBanned(classLoader: ClassLoader): Boolean { + for(bannedClassLoader in bannedClassLoaders) { + if(bannedClassLoader.get() == classLoader) return true + } + + return false + } + } + + val logger by loggerOf("${name}-EventHandler") + + private val lock = Any() + private val onceLock = Any() + + /** + * Get all the listeners in this event handler + * thread-safe operation (synchronized) + */ + val listeners: Array + get() { + synchronized(lock) { + return internalListeners.toTypedArray() + } + } + + /** + * Get all the "doOnce" listeners in this event handler + * thread-safe operation (synchronized) + */ + val onceListeners: Array + get() { + synchronized(onceLock) { + return internalOnceListeners.toTypedArray() + } + } + + var callRightAway = false + + private val internalListeners = ArrayList() + private val internalOnceListeners = ArrayList() + + /** + * Run all the listeners in this event handler + */ + override fun run() { + for(listener in listeners) { + try { + runListener(listener, false) + } catch (ex: Exception) { + if(ex is InterruptedException) { + logger.warn("Rethrowing InterruptedException...") + throw ex + } else { + logger.error("Error while running listener ${listener.javaClass.name}", ex) + } + } + } + + val toRemoveOnceListeners = mutableListOf() + + //executing "doOnce" listeners + for(listener in onceListeners) { + try { + runListener(listener, true) + } catch (ex: Exception) { + if(ex is InterruptedException) { + logger.warn("Rethrowing InterruptedException...") + throw ex + } else { + logger.error("Error while running \"once\" ${listener.javaClass.name}", ex) + removeOnceListener(listener) + } + } + + toRemoveOnceListeners.add(listener) + } + + synchronized(onceLock) { + for(listener in toRemoveOnceListeners) { + internalOnceListeners.remove(listener) + } + } + } + + /** + * Add a listener to this event handler to only be run once + * @param listener the listener to add + */ + fun doOnce(listener: EventListener) { + if(callRightAway) + runListener(listener, true) + else synchronized(onceLock) { + internalOnceListeners.add(listener) + } + } + + /** + * Add a runnable as a listener to this event handler to only be run once + * @param runnable the runnable to add + */ + fun doOnce(runnable: Runnable) = doOnce { runnable.run() } + + /** + * Add a listener to this event handler to be run every time + * @param listener the listener to add + */ + fun doPersistent(listener: EventListener): EventListenerRemover { + synchronized(lock) { + internalListeners.add(listener) + } + + if(callRightAway) runListener(listener, false) + + return EventListenerRemover(this, listener, false) + } + + /** + * Add a runnable as a listener to this event handler to be run every time + * @param runnable the runnable to add + */ + fun doPersistent(runnable: Runnable) = doPersistent { runnable.run() } + + /** + * Remove a listener from this event handler + * @param listener the listener to remove + */ + fun removePersistentListener(listener: EventListener) { + if(internalListeners.contains(listener)) { + synchronized(lock) { internalListeners.remove(listener) } + } + } + + /** + * Remove a "doOnce" listener from this event handler + * @param listener the listener to remove + */ + fun removeOnceListener(listener: EventListener) { + if(internalOnceListeners.contains(listener)) { + synchronized(onceLock) { internalOnceListeners.remove(listener) } + } + } + + /** + * Remove all listeners from this event handler + */ + fun removeAllListeners() { + removeAllPersistentListeners() + removeAllOnceListeners() + } + + /** + * Remove all persistent listeners from this event handler + */ + fun removeAllPersistentListeners() = synchronized(lock) { + internalListeners.clear() + } + + /** + * Remove all "doOnce" listeners from this event handler + */ + fun removeAllOnceListeners() = synchronized(onceLock) { + internalOnceListeners.clear() + } + + /** + * Add a listener to this event handler + * @param listener the listener to add + */ + operator fun invoke(listener: EventListener) = doPersistent(listener) + + private fun runListener(listener: EventListener, isOnce: Boolean) { + if(isBanned(listener.javaClass.classLoader)) { + removeBannedListeners() + return + } + + listener.run(EventListenerRemover(this, listener, isOnce)) + } + + private fun removeBannedListeners() { + for(listener in internalListeners.toArray()) { + if(isBanned(listener.javaClass.classLoader)) { + internalListeners.remove(listener) + logger.warn("Removed banned listener from ${listener.javaClass.classLoader}") + } + } + + for(listener in internalOnceListeners.toArray()) { + if(isBanned(listener.javaClass.classLoader)) internalOnceListeners.remove(listener) + logger.warn("Removed banned listener from ${listener.javaClass.classLoader}") + } + } + +} diff --git a/Common/src/main/java/org/firstinspires/ftc/robotcore/external/navigation/Quaternion.java b/Common/src/main/java/org/firstinspires/ftc/robotcore/external/navigation/Quaternion.java new file mode 100644 index 00000000..0dcef9a5 --- /dev/null +++ b/Common/src/main/java/org/firstinspires/ftc/robotcore/external/navigation/Quaternion.java @@ -0,0 +1,300 @@ +/* +Copyright (c) 2016 Robert Atkinson + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted (subject to the limitations in the disclaimer below) provided that +the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Neither the name of Robert Atkinson nor the names of his contributors may be used to +endorse or promote products derived from this software without specific prior +written permission. + +NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS +LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +package org.firstinspires.ftc.robotcore.external.navigation; + +import org.firstinspires.ftc.robotcore.external.matrices.MatrixF; +import org.firstinspires.ftc.robotcore.external.matrices.OpenGLMatrix; +import org.firstinspires.ftc.robotcore.external.matrices.VectorF; + +import java.util.Locale; + +/** + * A {@link Quaternion} can indicate an orientation in three-space without the trouble of + * possible gimbal-lock. + * + * @see https://en.wikipedia.org/wiki/Quaternion + * @see https://en.wikipedia.org/wiki/Gimbal_lock + * @see https://www.youtube.com/watch?v=zc8b2Jo7mno + * @see https://www.youtube.com/watch?v=mHVwd8gYLnI + */ +public class Quaternion +{ + //---------------------------------------------------------------------------------------------- + // Constants + //---------------------------------------------------------------------------------------------- + + public static Quaternion identityQuaternion() + { + return new Quaternion(1.0f, 0.0f, 0.0f, 0.0f, 0); + } + + //---------------------------------------------------------------------------------------------- + // State + //---------------------------------------------------------------------------------------------- + + public float w; + public float x; + public float y; + public float z; + + /** + * the time on the System.nanoTime() clock at which the data was acquired. If no + * timestamp is associated with this particular set of data, this value is zero. + */ + public long acquisitionTime; + + //---------------------------------------------------------------------------------------------- + // Construction + //---------------------------------------------------------------------------------------------- + + public Quaternion() + { + this(0, 0, 0, 0, 0); + } + + public Quaternion(float w, float x, float y, float z, long acquisitionTime) + { + this.w = w; + this.x = x; + this.y = y; + this.z = z; + this.acquisitionTime = acquisitionTime; + } + + public static Quaternion fromMatrix(MatrixF m, long acquisitionTime) + { + // https://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToQuaternion/ + float tr = m.get(0,0) + m.get(1,1) + m.get(2, 2); + + float w, x, y, z; + + if (tr > 0) + { + float s = (float) (Math.sqrt(tr + 1.0) * 2); // S=4*w + w = (float) (0.25 * s); + x = (m.get(2, 1) - m.get(1, 2)) / s; + y = (m.get(0, 2) - m.get(2, 0)) / s; + z = (m.get(1, 0) - m.get(0, 1)) / s; + } + else if ((m.get(0, 0) > m.get(1, 1)) & (m.get(0, 0) > m.get(2, 2))) + { + float s = (float) (Math.sqrt(1.0 + m.get(0, 0) - m.get(1, 1) - m.get(2, 2)) * 2); // S=4*x + w = (m.get(2, 1) - m.get(1, 2)) / s; + x = (float) (0.25 * s); + y = (m.get(0, 1) + m.get(1, 0)) / s; + z = (m.get(0, 2) + m.get(2, 0)) / s; + } + else if (m.get(1, 1) > m.get(2, 2)) + { + float s = (float) (Math.sqrt(1.0 + m.get(1, 1) - m.get(0, 0) - m.get(2, 2)) * 2); // S=4*y + w = (m.get(0, 2) - m.get(2, 0)) / s; + x = (m.get(0, 1) + m.get(1, 0)) / s; + y = (float) (0.25 * s); + z = (m.get(1, 2) + m.get(2, 1)) / s; + } + else + { + float s = (float) (Math.sqrt(1.0 + m.get(2, 2) - m.get(0, 0) - m.get(1, 1)) * 2); // S=4*z + w = (m.get(1, 0) - m.get(0, 1)) / s; + x = (m.get(0, 2) + m.get(2, 0)) / s; + y = (m.get(1, 2) + m.get(2, 1)) / s; + z = (float) (0.25 * s); + } + + return new Quaternion(w, x, y, z, acquisitionTime).normalized(); + } + + //---------------------------------------------------------------------------------------------- + // Operations + //---------------------------------------------------------------------------------------------- + + public float magnitude() + { + return (float)Math.sqrt(w*w + x*x + y*y + z*z); + } + + public Quaternion normalized() + { + float mag = this.magnitude(); + return new Quaternion( + w / mag, + x / mag, + y / mag, + z / mag, + this.acquisitionTime); + } + + public Quaternion conjugate() + { + return new Quaternion(w, -x, -y, -z, this.acquisitionTime); + } + + /** + * @deprecated Use {@link #conjugate()} instead. + */ + @Deprecated + public Quaternion congugate() + { + return conjugate(); + } + + public Quaternion inverse() + { + return normalized().conjugate(); + } + + public Quaternion multiply(Quaternion q, long acquisitionTime) + { + return new Quaternion( + this.w * q.w - this.x * q.x - this.y * q.y - this.z * q.z, + this.w * q.x + this.x * q.w + this.y * q.z - this.z * q.y, + this.w * q.y - this.x * q.z + this.y * q.w + this.z * q.x, + this.w * q.z + this.x * q.y - this.y * q.x + this.z * q.w, + acquisitionTime); + } + + /** + * Apply this rotation to the given vector + * @param vector The vector to rotate (with XYZ order) + * @return The rotated vector + */ + public VectorF applyToVector(VectorF vector) + { + // Adapted from https://www.vcalc.com/wiki/vCalc/V3+-+Vector+Rotation under the + // Creative Common Attribution - ShareAlike License, which all calculators on vCalc are + // licensed under. See Section 3 of the vCalc License and Terms of Use (as of 2022-10-27): + // http://web.archive.org/web/20221027024715/https://www.vcalc.com/static/html/terms.html + + float vx = vector.get(0); + float vy = vector.get(1); + float vz = vector.get(2); + + float qs = this.w; + float q1 = this.x; + float q2 = this.y; + float q3 = this.z; + + //Compute the inverse rotation quaternion "i" + float is = qs; + float i1 = -1*q1; + float i2 = -1*q2; + float i3 = -1*q3; + + float S1V2x = qs*vx; + float S1V2y = qs*vy; + float S1V2z = qs*vz; + + float V1xV21 = q2*vz - q3*vy; + float V1xV22 = -1*(q1*vz - q3*vx); + float V1xV23 = q1*vy - q2*vx; + + float qVs = -1*(q1*vx+q2*vy+q3*vz); + float qV1 = S1V2x + V1xV21; + float qV2 = S1V2y + V1xV22; + float qV3 = S1V2z + V1xV23; + + float TS1V2x = qVs*i1; + float TS1V2y = qVs*i2; + float TS1V2z = qVs*i3; + + float TS2V1x = is*qV1; + float TS2V1y = is*qV2; + float TS2V1z = is*qV3; + + float TV1XV21 = qV2*i3-qV3*i2; + + float qVq1 = TS1V2x + TS2V1x + TV1XV21; + + float TV1XV22 = -1*(qV1*i3-qV3*i1); + float qVq2 = TS1V2y + TS2V1y + TV1XV22; + + float TV1XV23 = qV1*i2-qV2*i1; + float qVq3 = TS1V2z + TS2V1z + TV1XV23; + + return new VectorF(qVq1, qVq2, qVq3); + } + + public MatrixF toMatrix() + { + // https://www.euclideanspace.com/maths/geometry/rotations/conversions/quaternionToMatrix/index.htm + final float xx = x * x; + final float xy = x * y; + final float xz = x * z; + final float xw = x * w; + + final float yy = y * y; + final float yz = y * z; + final float yw = y * w; + + final float zz = z * z; + final float zw = z * w; + + final float m00 = (float) (1.0 - (2.0 * (yy + zz))); + final float m01 = (float) (2.0 * (xy - zw)); + final float m02 = (float) (2.0 * (xz + yw)); + + final float m10 = (float) (2.0 * (xy + zw)); + final float m11 = (float) (1.0 - (2.0 * (xx + zz))); + final float m12 = (float) (2.0 * (yz - xw)); + + final float m20 = (float) (2.0 * (xz - yw)); + final float m21 = (float) (2.0 * (yz + xw)); + final float m22 = (float) (1.0 - (2.0 * (xx + yy))); + + OpenGLMatrix result = new OpenGLMatrix(); + result.put(0, 0, m00); + result.put(0, 1, m01); + result.put(0, 2, m02); + result.put(1, 0, m10); + result.put(1, 1, m11); + result.put(1, 2, m12); + result.put(2, 0, m20); + result.put(2, 1, m21); + result.put(2, 2, m22); + + return result; + } + + public Orientation toOrientation(AxesReference axesReference, AxesOrder axesOrder, AngleUnit angleUnit) + { + Orientation result = Orientation.getOrientation(toMatrix(), axesReference, axesOrder, angleUnit); + result.acquisitionTime = acquisitionTime; + return result; + } + + @Override + public String toString() + { + return String.format(Locale.US, "{w=%.3f, x=%.3f, y=%.3f, z=%.3f}", w, x, y, z); + } +} diff --git a/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/collections/MutableReference.java b/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/collections/MutableReference.java new file mode 100644 index 00000000..eaf9e077 --- /dev/null +++ b/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/collections/MutableReference.java @@ -0,0 +1,68 @@ +/* +Copyright (c) 2017 Robert Atkinson + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted (subject to the limitations in the disclaimer below) provided that +the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Neither the name of Robert Atkinson nor the names of his contributors may be used to +endorse or promote products derived from this software without specific prior +written permission. + +NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS +LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +package org.firstinspires.ftc.robotcore.internal.collections; + +/** + * A very simple utility. While there might be a built-in Android version of this, + * we couldn't find one after a brief search. + */ +@SuppressWarnings("WeakerAccess") +public class MutableReference +{ + protected T value; + + public MutableReference(T value) + { + this.value = value; + } + + public MutableReference() + { + this(null); + } + + public void setValue(T value) + { + this.value = value; + } + + public T getValue() + { + return this.value; + } + + @Override public String toString() + { + return "[{" + getValue() + "}]"; + } +} diff --git a/EOCV-Sim/build.gradle b/EOCV-Sim/build.gradle index aea2c540..b014a1eb 100644 --- a/EOCV-Sim/build.gradle +++ b/EOCV-Sim/build.gradle @@ -1,3 +1,5 @@ +import io.github.fvarrui.javapackager.gradle.PackageTask + import java.nio.file.Paths import java.time.LocalDateTime import java.time.format.DateTimeFormatter @@ -6,8 +8,7 @@ plugins { id 'org.jetbrains.kotlin.jvm' id 'com.github.johnrengelman.shadow' id 'maven-publish' - - id 'edu.sc.seis.launch4j' version '3.0.6' + id 'io.github.fvarrui.javapackager.plugin' } apply from: '../build.common.gradle' @@ -34,15 +35,28 @@ test { apply from: '../test-logging.gradle' -launch4j { - mainClassName = 'com.github.serivesmejia.eocvsim.Main' - icon = "${projectDir}/src/main/resources/images/icon/ico_eocvsim.ico" - - outfile = "${project.name}-${standardVersion}.exe" - - copyConfigurable = [] // Prevents copying dependencies - jarTask = shadowJar +tasks.register('pack', PackageTask) { + dependsOn build + // mandatory + mainClass = 'com.github.serivesmejia.eocvsim.Main' + // optional + bundleJre = true + customizedJre = false + generateInstaller = true + platform = "auto" + + winConfig { + icoFile = file('src/main/resources/images/icon/ico_eocvsim.ico') + + generateMsi = false + disableDirPage = false + disableProgramGroupPage = false + disableFinishedPage = false + } + linuxConfig { + pngFile = file('src/main/resources/images/icon/ico_eocvsim.png') + } } dependencies { @@ -58,10 +72,9 @@ dependencies { implementation "org.slf4j:slf4j-api:$slf4j_version" implementation "org.apache.logging.log4j:log4j-api:$log4j_version" implementation "org.apache.logging.log4j:log4j-core:$log4j_version" - implementation "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" + implementation "org.apache.logging.log4j:log4j-slf4j2-impl:$log4j_version" - implementation "com.github.deltacv:steve:1.0.0" - implementation "com.github.deltacv.AprilTagDesktop:AprilTagDesktop:$apriltag_plugin_version" + implementation "com.github.deltacv:steve:1.1.1" implementation 'info.picocli:picocli:4.6.1' implementation 'com.google.code.gson:gson:2.8.9' @@ -69,6 +82,9 @@ dependencies { implementation 'com.formdev:flatlaf:3.5.1' implementation 'com.formdev:flatlaf-intellij-themes:3.5.1' + implementation 'com.miglayout:miglayout-swing:11.4.2' + + implementation("org.java-websocket:Java-WebSocket:1.5.2") implementation 'net.lingala.zip4j:zip4j:2.11.3' @@ -80,6 +96,8 @@ dependencies { implementation 'com.moandjiezana.toml:toml4j:0.7.2' implementation 'org.ow2.asm:asm:9.7' + + implementation 'org.jboss.shrinkwrap.resolver:shrinkwrap-resolver-depchain:3.3.2' } task(writeBuildClassJava) { diff --git a/EOCV-Sim/src/main/java/com/formdev/flatlaf/demo/HintManager.java b/EOCV-Sim/src/main/java/com/formdev/flatlaf/demo/HintManager.java new file mode 100644 index 00000000..92fd6552 --- /dev/null +++ b/EOCV-Sim/src/main/java/com/formdev/flatlaf/demo/HintManager.java @@ -0,0 +1,352 @@ +/* + * Copyright 2020 FormDev Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.formdev.flatlaf.demo; + +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.geom.Area; +import java.awt.geom.RoundRectangle2D; +import java.util.ArrayList; +import java.util.List; +import javax.swing.*; +import javax.swing.border.Border; +import com.formdev.flatlaf.FlatLaf; +import com.formdev.flatlaf.ui.FlatDropShadowBorder; +import com.formdev.flatlaf.ui.FlatEmptyBorder; +import com.formdev.flatlaf.ui.FlatUIUtils; +import com.formdev.flatlaf.util.UIScale; +import net.miginfocom.swing.*; + +/** + * @author Karl Tauber + */ +public class HintManager +{ + private static final List hintPanels = new ArrayList<>(); + + public static void showHint( Hint hint ) { + HintPanel hintPanel = new HintPanel( hint ); + hintPanel.showHint(); + + hintPanels.add( hintPanel ); + } + + public static void hideAllHints() { + HintPanel[] hintPanels2 = hintPanels.toArray(new HintPanel[0]); + for( HintPanel hintPanel : hintPanels2 ) + hintPanel.hideHint(); + } + + //---- class HintPanel ---------------------------------------------------- + + public static class Hint + { + private final String message; + private final Component owner; + private final int position; + private final Hint nextHint; + + public Hint( String message, Component owner, int position, Hint nextHint ) { + this.message = message; + this.owner = owner; + this.position = position; + this.nextHint = nextHint; + } + } + + //---- class HintPanel ---------------------------------------------------- + + private static class HintPanel + extends JPanel + { + private final Hint hint; + + private JPanel popup; + + private HintPanel( Hint hint ) { + this.hint = hint; + + initComponents(); + + setOpaque( false ); + updateBalloonBorder(); + + hintLabel.setText( "" + hint.message + "" ); + + // grab all mouse events to avoid that components overlapped + // by the hint panel receive them + addMouseListener( new MouseAdapter() {} ); + } + + @Override + public void updateUI() { + super.updateUI(); + + if( UIManager.getLookAndFeel() instanceof FlatLaf ) + setBackground( UIManager.getColor( "HintPanel.backgroundColor" ) ); + else { + // using nonUIResource() because otherwise Nimbus does not fill the background + setBackground( FlatUIUtils.nonUIResource( UIManager.getColor( "info" ) ) ); + } + + if( hint != null ) + updateBalloonBorder(); + } + + private void updateBalloonBorder() { + int direction; + switch( hint.position ) { + case SwingConstants.LEFT: direction = SwingConstants.RIGHT; break; + case SwingConstants.TOP: direction = SwingConstants.BOTTOM; break; + case SwingConstants.RIGHT: direction = SwingConstants.LEFT; break; + case SwingConstants.BOTTOM: direction = SwingConstants.TOP; break; + default: throw new IllegalArgumentException(); + } + + setBorder( new BalloonBorder( direction, FlatUIUtils.getUIColor( "PopupMenu.borderColor", Color.gray ) ) ); + } + + void showHint() { + JRootPane rootPane = SwingUtilities.getRootPane( hint.owner ); + if( rootPane == null ) + return; + + JLayeredPane layeredPane = rootPane.getLayeredPane(); + + // create a popup panel that has a drop shadow + popup = new JPanel( new BorderLayout() ) { + @Override + public void updateUI() { + super.updateUI(); + + // use invokeLater because at this time the UI delegates + // of child components are not yet updated + EventQueue.invokeLater( () -> { + validate(); + setSize( getPreferredSize() ); + } ); + } + }; + popup.setOpaque( false ); + popup.add( this ); + + // calculate x/y location for hint popup + Point pt = SwingUtilities.convertPoint( hint.owner, 0, 0, layeredPane ); + int x = pt.x; + int y = pt.y; + Dimension size = popup.getPreferredSize(); + int gap = UIScale.scale( 6 ); + + switch( hint.position ) { + case SwingConstants.LEFT: + x -= size.width + gap; + break; + + case SwingConstants.TOP: + y -= size.height + gap; + break; + + case SwingConstants.RIGHT: + x += hint.owner.getWidth() + gap; + break; + + case SwingConstants.BOTTOM: + y += hint.owner.getHeight() + gap; + break; + } + + // set hint popup size and show it + popup.setBounds( x, y, size.width, size.height ); + layeredPane.add( popup, JLayeredPane.POPUP_LAYER ); + } + + void hideHint() { + if( popup != null ) { + Container parent = popup.getParent(); + if( parent != null ) { + parent.remove( popup ); + parent.repaint( popup.getX(), popup.getY(), popup.getWidth(), popup.getHeight() ); + } + } + + hintPanels.remove( this ); + } + + private void gotIt() { + // hide hint + hideHint(); + + // show next hint (if any) + if( hint.nextHint != null ) + HintManager.showHint( hint.nextHint ); + } + + private void initComponents() { + // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents + hintLabel = new JLabel(); + gotItButton = new JButton(); + + //======== this ======== + setLayout(new MigLayout( + "insets dialog,hidemode 3", + // columns + "[::200,fill]", + // rows + "[]para" + + "[]")); + + //---- hintLabel ---- + hintLabel.setText("hint"); + add(hintLabel, "cell 0 0"); + + //---- gotItButton ---- + gotItButton.setText("Got it!"); + gotItButton.setFocusable(false); + gotItButton.addActionListener(e -> gotIt()); + add(gotItButton, "cell 0 1,alignx right,growx 0"); + // JFormDesigner - End of component initialization //GEN-END:initComponents + } + + // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables + private JLabel hintLabel; + private JButton gotItButton; + // JFormDesigner - End of variables declaration //GEN-END:variables + } + + //---- class BalloonBorder ------------------------------------------------ + + public static class BalloonBorder + extends FlatEmptyBorder + { + private static final int ARC = 8; + private static final int ARROW_XY = 16; + private static final int ARROW_SIZE = 8; + private static final int SHADOW_SIZE = 6; + private static final int SHADOW_TOP_SIZE = 3; + private static final int SHADOW_SIZE2 = SHADOW_SIZE + 2; + + private final int direction; + private final Color borderColor; + + private final Border shadowBorder; + + public BalloonBorder( int direction, Color borderColor ) { + super( 1 + SHADOW_TOP_SIZE, 1 + SHADOW_SIZE, 1 + SHADOW_SIZE, 1 + SHADOW_SIZE ); + + this.direction = direction; + this.borderColor = borderColor; + + switch( direction ) { + case SwingConstants.LEFT: left += ARROW_SIZE; break; + case SwingConstants.TOP: top += ARROW_SIZE; break; + case SwingConstants.RIGHT: right += ARROW_SIZE; break; + case SwingConstants.BOTTOM: bottom += ARROW_SIZE; break; + } + + shadowBorder = UIManager.getLookAndFeel() instanceof FlatLaf + ? new FlatDropShadowBorder( + UIManager.getColor( "Popup.dropShadowColor" ), + new Insets( SHADOW_SIZE2, SHADOW_SIZE2, SHADOW_SIZE2, SHADOW_SIZE2 ), + FlatUIUtils.getUIFloat( "Popup.dropShadowOpacity", 0.5f ) ) + : null; + } + + @Override + public void paintBorder( Component c, Graphics g, int x, int y, int width, int height ) { + Graphics2D g2 = (Graphics2D) g.create(); + try { + FlatUIUtils.setRenderingHints( g2 ); + g2.translate( x, y ); + + // shadow coordinates + int sx = 0; + int sy = 0; + int sw = width; + int sh = height; + int arrowSize = UIScale.scale( ARROW_SIZE ); + switch( direction ) { + case SwingConstants.LEFT: sx += arrowSize; sw -= arrowSize; break; + case SwingConstants.TOP: sy += arrowSize; sh -= arrowSize; break; + case SwingConstants.RIGHT: sw -= arrowSize; break; + case SwingConstants.BOTTOM: sh -= arrowSize; break; + } + + // paint shadow + if( shadowBorder != null ) + shadowBorder.paintBorder( c, g2, sx, sy, sw, sh ); + + // create balloon shape + int bx = UIScale.scale( SHADOW_SIZE ); + int by = UIScale.scale( SHADOW_TOP_SIZE ); + int bw = width - UIScale.scale( SHADOW_SIZE + SHADOW_SIZE ); + int bh = height - UIScale.scale( SHADOW_TOP_SIZE + SHADOW_SIZE ); + g2.translate( bx, by ); + Shape shape = createBalloonShape( bw, bh ); + + // fill balloon background + g2.setColor( c.getBackground() ); + g2.fill( shape ); + + // paint balloon border + g2.setColor( borderColor ); + g2.setStroke( new BasicStroke( UIScale.scale( 1f ) ) ); + g2.draw( shape ); + } finally { + g2.dispose(); + } + } + + private Shape createBalloonShape( int width, int height ) { + int arc = UIScale.scale( ARC ); + int xy = UIScale.scale( ARROW_XY ); + int awh = UIScale.scale( ARROW_SIZE ); + + Shape rect; + Shape arrow; + switch( direction ) { + case SwingConstants.LEFT: + rect = new RoundRectangle2D.Float( awh, 0, width - 1 - awh, height - 1, arc, arc ); + arrow = FlatUIUtils.createPath( awh,xy, 0,xy+awh, awh,xy+awh+awh ); + break; + + case SwingConstants.TOP: + rect = new RoundRectangle2D.Float( 0, awh, width - 1, height - 1 - awh, arc, arc ); + arrow = FlatUIUtils.createPath( xy,awh, xy+awh,0, xy+awh+awh,awh ); + break; + + case SwingConstants.RIGHT: + rect = new RoundRectangle2D.Float( 0, 0, width - 1 - awh, height - 1, arc, arc ); + int x = width - 1 - awh; + arrow = FlatUIUtils.createPath( x,xy, x+awh,xy+awh, x,xy+awh+awh ); + break; + + case SwingConstants.BOTTOM: + rect = new RoundRectangle2D.Float( 0, 0, width - 1, height - 1 - awh, arc, arc ); + int y = height - 1 - awh; + arrow = FlatUIUtils.createPath( xy,y, xy+awh,y+awh, xy+awh+awh,y ); + break; + + default: + throw new RuntimeException(); + } + + Area area = new Area( rect ); + area.add( new Area( arrow ) ); + return area; + } + } +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt index 85ae0517..2ba19617 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt @@ -36,9 +36,9 @@ import com.github.serivesmejia.eocvsim.pipeline.PipelineSource import com.github.serivesmejia.eocvsim.tuner.TunerManager import com.github.serivesmejia.eocvsim.util.ClasspathScan import com.github.serivesmejia.eocvsim.util.FileFilters +import com.github.serivesmejia.eocvsim.util.JavaProcess import com.github.serivesmejia.eocvsim.util.SysUtil import com.github.serivesmejia.eocvsim.util.event.EventHandler -import com.github.serivesmejia.eocvsim.util.exception.MaxActiveContextsException import com.github.serivesmejia.eocvsim.util.exception.handling.CrashReport import com.github.serivesmejia.eocvsim.util.exception.handling.EOCVSimUncaughtExceptionHandler import com.github.serivesmejia.eocvsim.util.fps.FpsLimiter @@ -49,6 +49,7 @@ import com.qualcomm.robotcore.eventloop.opmode.OpMode import com.qualcomm.robotcore.eventloop.opmode.OpModePipelineHandler import io.github.deltacv.common.pipeline.util.PipelineStatisticsCalculator import io.github.deltacv.common.util.ParsedVersion +import io.github.deltacv.eocvsim.plugin.security.AuthorityFetcher import io.github.deltacv.eocvsim.plugin.loader.PluginManager import io.github.deltacv.vision.external.PipelineRenderHook import nu.pattern.OpenCV @@ -57,9 +58,11 @@ import org.opencv.core.Size import org.openftc.easyopencv.TimestampedPipelineHandler import java.awt.Dimension import java.io.File +import java.lang.Thread.sleep import javax.swing.SwingUtilities import javax.swing.filechooser.FileFilter import javax.swing.filechooser.FileNameExtensionFilter +import kotlin.jvm.Throws import kotlin.system.exitProcess /** @@ -281,6 +284,8 @@ class EOCVSim(val params: Parameters = Parameters()) { configManager.init() + configManager.config.simTheme.install() + pluginManager.init() // woah pluginManager.loadPlugins() @@ -358,7 +363,30 @@ class EOCVSim(val params: Parameters = Parameters()) { pluginManager.enablePlugins() - start() + try { + start() + } catch (e: InterruptedException) { + logger.warn("Main thread interrupted ($hexCode)", e) + } + + if(!destroying) { + destroy(DestroyReason.THREAD_EXIT) + } + + if (isRestarting) { + Thread.interrupted() //clear interrupted flag + EOCVSimFolder.lock?.lock?.close() + + JavaProcess.killSubprocessesOnExit = false + Thread { + JavaProcess.exec(Main::class.java, null, null) + }.start() + + sleep(1000) + } + + logger.info("-- End of EasyOpenCV Simulator v$VERSION ($hexCode) --") + exitProcess(0) } /** @@ -372,6 +400,7 @@ class EOCVSim(val params: Parameters = Parameters()) { * are explicitly updated within this method * @see init */ + @Throws(InterruptedException::class) private fun start() { if(Thread.currentThread() != eocvSimThread) { throw IllegalStateException("start() must be called from the EOCVSim thread") @@ -394,7 +423,7 @@ class EOCVSim(val params: Parameters = Parameters()) { } else null ) - //limit FPG + //limit FPS fpsLimiter.maxFPS = config.pipelineMaxFps.fps.toDouble() try { fpsLimiter.sync() @@ -404,15 +433,6 @@ class EOCVSim(val params: Parameters = Parameters()) { } logger.warn("Main thread interrupted ($hexCode)") - - if(!destroying) { - destroy(DestroyReason.THREAD_EXIT) - } - - if (isRestarting) { - Thread.interrupted() //clear interrupted flag - EOCVSim(params).init() - } } /** @@ -541,7 +561,7 @@ class EOCVSim(val params: Parameters = Parameters()) { * Checks if the simulator is currently recording * @return true if the simulator is currently recording, false otherwise */ - fun isCurrentlyRecording() = currentRecordingSession?.isRecording ?: false + fun isCurrentlyRecording() = currentRecordingSession?.isRecording == true /** * Updates the visualizer title message diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/Main.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/Main.kt index bc494082..4bb6415c 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/Main.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/Main.kt @@ -1,5 +1,3 @@ -@file:JvmName("Main") - package com.github.serivesmejia.eocvsim import com.github.serivesmejia.eocvsim.pipeline.PipelineSource @@ -11,19 +9,23 @@ import kotlin.system.exitProcess val jvmMainThread: Thread = Thread.currentThread() var currentMainThread: Thread = jvmMainThread -/** - * Main entry point for the EOCV-Sim CLI - * @param args the command line arguments - * @see CommandLine - */ -fun main(args: Array) { - System.setProperty("sun.java2d.d3d", "false") +object Main { + + /** + * Main entry point for the EOCV-Sim CLI + * @param args the command line arguments + * @see CommandLine + */ + @JvmStatic + fun main(args: Array) { + System.setProperty("sun.java2d.d3d", "false") - val result = CommandLine( - EOCVSimCommandInterface() - ).setCaseInsensitiveEnumValuesAllowed(true).execute(*args) + val result = CommandLine( + EOCVSimCommandInterface() + ).setCaseInsensitiveEnumValuesAllowed(true).execute(*args) - exitProcess(result) + exitProcess(result) + } } /** diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/config/Config.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/config/Config.java index c7576f15..0bc7f0d6 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/config/Config.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/config/Config.java @@ -40,6 +40,7 @@ public class Config { public volatile Theme simTheme = Theme.Light; + @Deprecated public volatile double zoom = 1; public volatile PipelineFps pipelineMaxFps = PipelineFps.MEDIUM; @@ -50,7 +51,7 @@ public class Config { public volatile Size videoRecordingSize = new Size(640, 480); public volatile PipelineFps videoRecordingFps = PipelineFps.MEDIUM; - public volatile WebcamDriver preferredWebcamDriver = WebcamDriver.OpenIMAJ; + public volatile WebcamDriver preferredWebcamDriver = WebcamDriver.OpenPnp; public volatile String workspacePath = CompiledPipelineManager.Companion.getDEF_WORKSPACE_FOLDER().getAbsolutePath(); public volatile TunableFieldPanelConfig.Config globalTunableFieldsConfig = @@ -63,12 +64,17 @@ public class Config { public volatile HashMap specificTunableFieldConfig = new HashMap<>(); + @Deprecated public volatile List superAccessPluginHashes = new ArrayList<>(); - public Config() { - if(SysUtil.ARCH != SysUtil.SystemArchitecture.X86_64) { - preferredWebcamDriver = WebcamDriver.OpenCV; - } + public volatile HashMap flags = new HashMap<>(); + + public boolean hasFlag(String flagName) { + return flags.get(flagName) != null; + } + + public boolean getFlag(String flagName) { + return flags.get(flagName) != null && flags.get(flagName); } } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/config/ConfigManager.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/config/ConfigManager.java index 5777ada9..d8c1605a 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/config/ConfigManager.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/config/ConfigManager.java @@ -49,6 +49,11 @@ public void init() { logger.info("Creating config file..."); configLoader.saveToFile(config); } + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + logger.info("SHUTDOWN - Saving config to file..."); + saveToFile(); + })); } public void saveToFile() { diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/DialogFactory.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/DialogFactory.java index 80f7f09c..a1cd2f17 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/DialogFactory.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/DialogFactory.java @@ -32,6 +32,7 @@ import com.github.serivesmejia.eocvsim.gui.dialog.source.CreateVideoSource; import com.github.serivesmejia.eocvsim.input.SourceType; import com.github.serivesmejia.eocvsim.util.event.EventHandler; +import io.github.deltacv.eocvsim.plugin.loader.PluginManager; import javax.swing.*; import javax.swing.filechooser.FileFilter; @@ -39,9 +40,6 @@ import java.awt.*; import java.io.File; import java.util.ArrayList; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.concurrent.ThreadPoolExecutor; import java.util.function.IntConsumer; public class DialogFactory { @@ -121,10 +119,6 @@ public static void createConfigDialog(EOCVSim eocvSim) { invokeLater(() -> new Configuration(eocvSim.visualizer.frame, eocvSim)); } - public static void createPluginsDialog(EOCVSim eocvSim) { - invokeLater(() -> new Configuration(eocvSim.visualizer.frame, eocvSim)); - } - public static void createAboutDialog(EOCVSim eocvSim) { invokeLater(() -> new About(eocvSim.visualizer.frame, eocvSim)); } @@ -154,6 +148,14 @@ public static void createPipelineOutput(EOCVSim eocvSim) { }); } + public static AppendDelegate createMavenOutput(PluginManager manager, Runnable onContinue) { + AppendDelegate delegate = new AppendDelegate(); + + invokeLater(() -> new PluginOutput(delegate, manager, manager.getEocvSim(), onContinue)); + + return delegate; + } + public static void createSplashScreen(EventHandler closeHandler) { invokeLater(() -> new SplashScreen(closeHandler)); } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TopMenuBar.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TopMenuBar.kt index ca37f44e..81af4fa4 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TopMenuBar.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TopMenuBar.kt @@ -27,6 +27,7 @@ import com.github.serivesmejia.eocvsim.EOCVSim import com.github.serivesmejia.eocvsim.gui.DialogFactory import com.github.serivesmejia.eocvsim.gui.Visualizer import com.github.serivesmejia.eocvsim.gui.dialog.Output +import com.github.serivesmejia.eocvsim.gui.dialog.PluginOutput import com.github.serivesmejia.eocvsim.gui.util.GuiUtil import com.github.serivesmejia.eocvsim.input.SourceType import com.github.serivesmejia.eocvsim.util.FileFilters @@ -101,8 +102,10 @@ class TopMenuBar(visualizer: Visualizer, eocvSim: EOCVSim) : JMenuBar() { mFileMenu.add(editSettings) - val filePlugins = JMenuItem("Plugins") - filePlugins.addActionListener { DialogFactory.createPluginsDialog(eocvSim) } + val filePlugins = JMenuItem("Manage Plugins") + filePlugins.addActionListener { eocvSim.pluginManager.appender.append(PluginOutput.SPECIAL_OPEN_MGR)} + + mFileMenu.add(filePlugins) mFileMenu.addSeparator() diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/SourceSelectorPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/SourceSelectorPanel.kt index cd4d77df..1bc3ebfc 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/SourceSelectorPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/SourceSelectorPanel.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.swing.Swing import java.awt.FlowLayout import java.awt.GridBagConstraints import java.awt.GridBagLayout +import java.awt.event.MouseAdapter import javax.swing.* class SourceSelectorPanel(private val eocvSim: EOCVSim) : JPanel() { @@ -81,6 +82,7 @@ class SourceSelectorPanel(private val eocvSim: EOCVSim) : JPanel() { sourceSelectorButtonsContainer.add(sourceSelectorCreateBtt) sourceSelectorButtonsContainer.add(sourceSelectorDeleteBtt) + sourceSelectorDeleteBtt.isEnabled = false add(sourceSelectorButtonsContainer, GridBagConstraints().apply { gridy = 1 @@ -92,41 +94,46 @@ class SourceSelectorPanel(private val eocvSim: EOCVSim) : JPanel() { private fun registerListeners() { //listener for changing input sources - sourceSelector.addListSelectionListener { evt -> - if(allowSourceSwitching && !evt.valueIsAdjusting) { - try { - if (sourceSelector.selectedIndex != -1) { - val model = sourceSelector.model - val source = model.getElementAt(sourceSelector.selectedIndex) - - //enable or disable source delete button depending if source is default or not - eocvSim.visualizer.sourceSelectorPanel.sourceSelectorDeleteBtt - .isEnabled = !(eocvSim.inputSourceManager.sources[source]?.isDefault ?: true) - - if (!evt.valueIsAdjusting && source != beforeSelectedSource) { - if (!eocvSim.pipelineManager.paused) { - eocvSim.inputSourceManager.requestSetInputSource(source) - beforeSelectedSource = source - beforeSelectedSourceIndex = sourceSelector.selectedIndex - } else { - //check if the user requested the pause or if it was due to one shoot analysis when selecting images - if (eocvSim.pipelineManager.pauseReason !== PipelineManager.PauseReason.IMAGE_ONE_ANALYSIS) { - sourceSelector.setSelectedIndex(beforeSelectedSourceIndex) - } else { //handling pausing - eocvSim.pipelineManager.requestSetPaused(false) - eocvSim.inputSourceManager.requestSetInputSource(source) - beforeSelectedSource = source - beforeSelectedSourceIndex = sourceSelector.selectedIndex + sourceSelector.addMouseListener(object: MouseAdapter() { + override fun mouseClicked(e: java.awt.event.MouseEvent) { + val index = (e.source as JList<*>).locationToIndex(e.point) + + if (index != -1) { + if(allowSourceSwitching) { + try { + if (sourceSelector.selectedIndex != -1) { + val model = sourceSelector.model + val source = model.getElementAt(sourceSelector.selectedIndex) + + //enable or disable source delete button depending if source is default or not + sourceSelectorDeleteBtt.isEnabled = eocvSim.inputSourceManager.sources[source]?.isDefault == false + + if (source != beforeSelectedSource) { + if (!eocvSim.pipelineManager.paused) { + eocvSim.inputSourceManager.requestSetInputSource(source) + beforeSelectedSource = source + beforeSelectedSourceIndex = sourceSelector.selectedIndex + } else { + //check if the user requested the pause or if it was due to one shoot analysis when selecting images + if (eocvSim.pipelineManager.pauseReason !== PipelineManager.PauseReason.IMAGE_ONE_ANALYSIS) { + sourceSelector.setSelectedIndex(beforeSelectedSourceIndex) + } else { //handling pausing + eocvSim.pipelineManager.requestSetPaused(false) + eocvSim.inputSourceManager.requestSetInputSource(source) + beforeSelectedSource = source + beforeSelectedSourceIndex = sourceSelector.selectedIndex + } + } } + } else { + sourceSelector.setSelectedIndex(1) } + } catch (ignored: ArrayIndexOutOfBoundsException) { } - } else { - sourceSelector.setSelectedIndex(1) } - } catch (ignored: ArrayIndexOutOfBoundsException) { } } - } + }) // delete selected input source sourceSelectorDeleteBtt.addActionListener { diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/PluginOutput.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/PluginOutput.kt new file mode 100644 index 00000000..a602d223 --- /dev/null +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/PluginOutput.kt @@ -0,0 +1,551 @@ +/* + * Copyright (c) 2024 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.dialog + +import com.github.serivesmejia.eocvsim.EOCVSim +import com.github.serivesmejia.eocvsim.gui.dialog.component.BottomButtonsPanel +import com.github.serivesmejia.eocvsim.gui.dialog.component.OutputPanel +import io.github.deltacv.eocvsim.plugin.loader.PluginManager +import io.github.deltacv.eocvsim.plugin.loader.PluginSource +import io.github.deltacv.eocvsim.plugin.repository.PluginRepositoryManager +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.swing.Swing +import java.awt.Dimension +import java.awt.GridBagConstraints +import java.awt.GridBagLayout +import java.awt.Toolkit +import java.awt.Color +import java.awt.datatransfer.StringSelection +import javax.swing.* +import java.awt.Desktop +import javax.swing.event.ChangeEvent +import javax.swing.event.ChangeListener + +class AppendDelegate { + private val appendables = mutableListOf() + + @Synchronized + fun subscribe(appendable: Appendable) { + appendables.add(appendable) + } + + fun subscribe(appendable: (String) -> Unit) { + appendables.add(object : Appendable { + override fun append(csq: CharSequence?): java.lang.Appendable? { + appendable(csq.toString()) + return this + } + + override fun append(csq: CharSequence?, start: Int, end: Int): java.lang.Appendable? { + appendable(csq.toString().substring(start, end)) + return this + } + + override fun append(c: Char): java.lang.Appendable? { + appendable(c.toString()) + return this + } + }) + } + + @Synchronized + fun append(text: String) { + appendables.forEach { it.append(text) } + } + + @Synchronized + fun appendln(text: String) { + appendables.forEach { it.appendLine(text) } + } +} + +class PluginOutput( + appendDelegate: AppendDelegate, + val pluginManager: PluginManager, + val eocvSim: EOCVSim? = null, + val onContinue: Runnable +) : Appendable { + + companion object { + const val SPECIAL = "[13mck]" + + const val SPECIAL_OPEN = "$SPECIAL[OPEN]" + const val SPECIAL_OPEN_MGR = "$SPECIAL[OPEN_MGR]" + const val SPECIAL_CLOSE = "$SPECIAL[CLOSE]" + const val SPECIAL_CONTINUE = "$SPECIAL[CONTINUE]" + const val SPECIAL_FREE = "$SPECIAL[FREE]" + const val SPECIAL_SILENT = "$SPECIAL[SILENT]" + + fun String.trimSpecials(): String { + return this + .replace(SPECIAL_OPEN, "") + .replace(SPECIAL_OPEN_MGR, "") + .replace(SPECIAL_CLOSE, "") + .replace(SPECIAL_CONTINUE, "") + .replace(SPECIAL_SILENT, "") + .replace(SPECIAL_FREE, "") + } + } + + private val output = JDialog() + private val tabbedPane: JTabbedPane + + private var shouldAskForRestart = false + + private val mavenBottomButtonsPanel: MavenOutputBottomButtonsPanel = MavenOutputBottomButtonsPanel(::close) { + mavenOutputPanel.outputArea.text + } + + private val mavenOutputPanel = OutputPanel(mavenBottomButtonsPanel) + + init { + output.isModal = true + output.isAlwaysOnTop = true + output.title = "Plugin Manager" + + tabbedPane = JTabbedPane() + + output.add(tabbedPane.apply { + addTab("Plugins", makePluginManagerPanel()) + addTab("Output", mavenOutputPanel) + }) + + registerListeners() + + output.pack() + output.setSize(500, 365) + + appendDelegate.subscribe(this) + + output.setLocationRelativeTo(null) + output.defaultCloseOperation = WindowConstants.HIDE_ON_CLOSE + } + + @OptIn(DelicateCoroutinesApi::class) + private fun registerListeners() = GlobalScope.launch(Dispatchers.Swing) { + output.addWindowListener(object : java.awt.event.WindowAdapter() { + override fun windowClosing(e: java.awt.event.WindowEvent?) { + if(mavenBottomButtonsPanel.continueButton.isEnabled) { + mavenBottomButtonsPanel.continueButton.doClick() + } + + checkShouldAskForRestart() + } + }) + + mavenBottomButtonsPanel.continueButton.addActionListener { + close() + onContinue.run() + mavenBottomButtonsPanel.continueButton.isEnabled = false + mavenBottomButtonsPanel.closeButton.isEnabled = true + } + + tabbedPane.addChangeListener(object: ChangeListener { + override fun stateChanged(e: ChangeEvent?) { + // remake plugin manager panel + if(tabbedPane.selectedIndex == tabbedPane.indexOfTab("Plugins")) { + tabbedPane.setComponentAt(0, makePluginManagerPanel()) + } + } + }) + } + + private fun checkShouldAskForRestart() { + if(shouldAskForRestart && eocvSim != null) { + val dialogResult = JOptionPane.showConfirmDialog( + output, + "You need to restart the application to apply the changes. Do you want to restart now?", + "Restart required", + JOptionPane.YES_NO_OPTION + ) + + if(dialogResult == JOptionPane.YES_OPTION) { + output.isVisible = false + eocvSim.restart() + } + + shouldAskForRestart = false + } + } + + private fun makePluginManagerPanel(): JPanel { + val pluginsPanel = JPanel() + pluginsPanel.layout = GridBagLayout() + + if(pluginManager.loaders.isEmpty()) { + // center vertically and horizontally + val noPluginsLabel = JLabel("

Nothing to see here...

") + noPluginsLabel.horizontalAlignment = SwingConstants.CENTER + noPluginsLabel.verticalAlignment = SwingConstants.CENTER + + // Use GridBagConstraints to center the label + val constraints = GridBagConstraints().apply { + gridx = 0 // Center horizontally + gridy = 0 // Center vertically + weightx = 1.0 // Take up the entire horizontal space + weighty = 1.0 // Take up the entire vertical space + anchor = GridBagConstraints.CENTER // Center alignment + } + + // Add the label to the panel with the constraints + pluginsPanel.add(noPluginsLabel, constraints) + } else { + val tabbedPane = JTabbedPane(JTabbedPane.LEFT) + + for((_, loader) in pluginManager.loaders) { + val pluginPanel = JPanel() + pluginPanel.layout = GridBagLayout() + + val pluginNameLabel = JLabel( + "

${loader.pluginName} v${loader.pluginVersion} by ${loader.pluginAuthor}

", + SwingConstants.CENTER + ) + + pluginPanel.add(pluginNameLabel, GridBagConstraints().apply { + gridx = 0 + gridy = 0 + weightx = 1.0 + fill = GridBagConstraints.HORIZONTAL + anchor = GridBagConstraints.CENTER + }) + + val signatureStatus = if(loader.signature.verified) + "This plugin has been verified and was signed by ${loader.signature.authority!!.name}." + else "This plugin is not signed." + + val authorEmail = if(loader.pluginAuthorEmail.isBlank()) + "The author did not provide contact information." + else "Contact the author at ${loader.pluginAuthorEmail}" + + val source = if(loader.pluginSource == PluginSource.REPOSITORY) + "Maven repository" + else "local file" + + val sourceEnabled = if(loader.shouldEnable) "It was LOADED from a $source." else "It is DISABLED, it comes from a $source." + + val superAccess = if(loader.hasSuperAccess) + "It has super access." + else "It does not have super access." + + val wantsSuperAccess = if(loader.pluginToml.getBoolean("super-access", false)) + "It requests super access in its manifest." + else "It does not request super access in its manifest." + + val description = if(loader.pluginDescription.isBlank()) + "No description available." + else loader.pluginDescription + + val font = pluginNameLabel.font.deriveFont(13.0f) + + // add a text area for the plugin description + val pluginDescriptionArea = JTextArea(""" + $signatureStatus + $authorEmail + + ** $sourceEnabled $superAccess $wantsSuperAccess ** + + $description + """.trimIndent()) + + pluginDescriptionArea.font = font + pluginDescriptionArea.isEditable = false + pluginDescriptionArea.lineWrap = true + pluginDescriptionArea.wrapStyleWord = true + pluginDescriptionArea.background = pluginPanel.background + + pluginPanel.add(JScrollPane(pluginDescriptionArea), GridBagConstraints().apply { + gridx = 0 + gridy = 1 + weightx = 1.0 + weighty = 1.0 + fill = GridBagConstraints.BOTH + }) + + val restartWarnLabel = JLabel("Restart to apply changes.") + restartWarnLabel.isVisible = false + + val disableButton = JButton("Disable") + val enableButton = JButton("Enable") + + fun refreshButtons() { + disableButton.isEnabled = loader.shouldEnable + enableButton.isEnabled = !loader.shouldEnable + } + + refreshButtons() + + enableButton.addActionListener { + loader.shouldEnable = true + restartWarnLabel.isVisible = true + shouldAskForRestart = true + + refreshButtons() + } + disableButton.addActionListener { + loader.shouldEnable = false + restartWarnLabel.isVisible = true + shouldAskForRestart = true + + refreshButtons() + } + + // center buttons + val buttonsPanel = JPanel() + buttonsPanel.border = BorderFactory.createEmptyBorder(10, 0, 0, 0) + + buttonsPanel.layout = BoxLayout(buttonsPanel, BoxLayout.LINE_AXIS) + + buttonsPanel.add(restartWarnLabel) + buttonsPanel.add(Box.createHorizontalGlue()) + buttonsPanel.add(disableButton) + buttonsPanel.add(Box.createRigidArea(Dimension(5, 0))) + buttonsPanel.add(enableButton) + + pluginPanel.add(buttonsPanel, GridBagConstraints().apply { + gridx = 0 + gridy = 2 + weightx = 1.0 + fill = GridBagConstraints.HORIZONTAL + anchor = GridBagConstraints.CENTER + }) + + pluginPanel.border = BorderFactory.createEmptyBorder(10, 10, 10, 10) + + tabbedPane.addTab(loader.pluginName, pluginPanel) + } + + pluginsPanel.add(tabbedPane, GridBagConstraints().apply { + gridx = 0 + gridy = 0 + weightx = 1.0 + weighty = 1.0 + fill = GridBagConstraints.BOTH + }) + } + + val panel = JPanel() + + panel.layout = GridBagLayout() + panel.add(pluginsPanel, GridBagConstraints().apply { + gridx = 0 + gridy = 0 + weightx = 1.0 + weighty = 1.0 + fill = GridBagConstraints.BOTH + }) + + val bottomButtonsPanel = JPanel() + bottomButtonsPanel.layout = BoxLayout(bottomButtonsPanel, BoxLayout.PAGE_AXIS) + + val openPluginsFolderButton = JButton("Open plugins folder") + + openPluginsFolderButton.addActionListener { + val pluginsFolder = PluginManager.PLUGIN_FOLDER + + if(pluginsFolder.exists() && Desktop.isDesktopSupported()) { + output.isVisible = false + Desktop.getDesktop().open(pluginsFolder) + } else { + JOptionPane.showMessageDialog( + output, + "Unable to open plugins folder, the folder does not exist or the operation is unsupported.", + "Operation failed", + JOptionPane.ERROR_MESSAGE + ) + } + } + + val startFreshButton = JButton("Start fresh") + + startFreshButton.addActionListener { + val dialogResult = JOptionPane.showConfirmDialog( + output, + "Are you sure you want to start fresh? This will remove all plugins from all sources.", + "Start fresh", + JOptionPane.YES_NO_OPTION + ) + + if(dialogResult == JOptionPane.YES_OPTION) { + eocvSim?.config?.flags?.set("startFresh", true) + + PluginRepositoryManager.REPOSITORY_FILE.delete() + PluginRepositoryManager.CACHE_FILE.delete() + + shouldAskForRestart = true + checkShouldAskForRestart() + } + } + + val closeButton = JButton("Close") + + closeButton.addActionListener { + close() + } + + val buttonsPanel = JPanel() + buttonsPanel.layout = BoxLayout(buttonsPanel, BoxLayout.LINE_AXIS) + buttonsPanel.border = BorderFactory.createEmptyBorder(5, 0, 5, 0) + + buttonsPanel.add(Box.createHorizontalGlue()) + + buttonsPanel.add(openPluginsFolderButton) + buttonsPanel.add(Box.createRigidArea(Dimension(5, 0))) + buttonsPanel.add(startFreshButton) + buttonsPanel.add(Box.createRigidArea(Dimension(5, 0))) + buttonsPanel.add(closeButton) + + buttonsPanel.add(Box.createHorizontalGlue()) + + bottomButtonsPanel.add(buttonsPanel) + + // Set a thin light gray line border with padding + bottomButtonsPanel.border = BorderFactory.createCompoundBorder( + BorderFactory.createEmptyBorder(10, 10, 10, 10), // Padding inside the border + JScrollPane().border + ) + + panel.add(bottomButtonsPanel, GridBagConstraints().apply { + gridx = 0 + gridy = 2 + weightx = 1.0 + fill = GridBagConstraints.HORIZONTAL + }) + + return panel + } + + fun close() { + output.isVisible = false + } + + private fun handleSpecials(text: String): Boolean { + when(text) { + SPECIAL_FREE -> { + mavenBottomButtonsPanel.continueButton.isEnabled = false + mavenBottomButtonsPanel.closeButton.isEnabled = true + } + SPECIAL_CLOSE -> close() + SPECIAL_CONTINUE -> { + mavenBottomButtonsPanel.continueButton.isEnabled = true + mavenBottomButtonsPanel.closeButton.isEnabled = false + } + } + + if(!text.startsWith(SPECIAL_SILENT) && text != SPECIAL_CLOSE && text != SPECIAL_FREE) { + SwingUtilities.invokeLater { + SwingUtilities.invokeLater { + if(text == SPECIAL_OPEN_MGR) { + tabbedPane.selectedIndex = tabbedPane.indexOfTab("Plugins") // focus on plugins tab + } else { + tabbedPane.selectedIndex = tabbedPane.indexOfTab("Output") // focus on output tab + } + } + + tabbedPane.setComponentAt(0, makePluginManagerPanel()) + output.isVisible = true + } + } + + return text == SPECIAL_OPEN || text == SPECIAL_CLOSE || text == SPECIAL_CONTINUE || text == SPECIAL_FREE + } + + override fun append(csq: CharSequence?): java.lang.Appendable? { + val text = csq.toString() + + SwingUtilities.invokeLater { + if(handleSpecials(text)) return@invokeLater + + mavenOutputPanel.outputArea.text += text.trimSpecials() + mavenOutputPanel.outputArea.revalidate() + mavenOutputPanel.outputArea.repaint() + } + return this + } + + override fun append(csq: CharSequence?, start: Int, end: Int): java.lang.Appendable? { + val text = csq.toString().substring(start, end) + + SwingUtilities.invokeLater { + if(handleSpecials(text)) return@invokeLater + + mavenOutputPanel.outputArea.text += text.trimSpecials() + mavenOutputPanel.outputArea.revalidate() + mavenOutputPanel.outputArea.repaint() + } + return this + } + + override fun append(c: Char): java.lang.Appendable? { + SwingUtilities.invokeLater { + mavenOutputPanel.outputArea.text += c + mavenOutputPanel.outputArea.revalidate() + mavenOutputPanel.outputArea.repaint() + } + + return this + } + + class MavenOutputBottomButtonsPanel( + override val closeCallback: () -> Unit, + val outputTextSupplier: () -> String + ) : BottomButtonsPanel() { + + val copyButton = JButton("Copy") + val clearButton = JButton("Clear") + val continueButton = JButton("Continue") + val closeButton = JButton("Close") + + override fun create(panel: OutputPanel) { + layout = BoxLayout(this, BoxLayout.LINE_AXIS) + + add(Box.createRigidArea(Dimension(4, 0))) + + copyButton.addActionListener { + Toolkit.getDefaultToolkit().systemClipboard.setContents(StringSelection(outputTextSupplier()), null) + } + + add(copyButton) + add(Box.createRigidArea(Dimension(4, 0))) + + clearButton.addActionListener { panel.outputArea.text = "" } + + add(clearButton) + + add(Box.createHorizontalGlue()) + + add(continueButton) + continueButton.isEnabled = false + + add(Box.createRigidArea(Dimension(4, 0))) + + add(closeButton) + closeButton.addActionListener { closeCallback() } + + add(Box.createRigidArea(Dimension(4, 0))) + } + } +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/component/OutputPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/component/OutputPanel.kt index da348cf8..b80fe5cf 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/component/OutputPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/component/OutputPanel.kt @@ -23,30 +23,55 @@ package com.github.serivesmejia.eocvsim.gui.dialog.component +import com.formdev.flatlaf.FlatLaf import java.awt.Dimension import java.awt.GridBagConstraints import java.awt.GridBagLayout import java.awt.Toolkit import java.awt.datatransfer.StringSelection +import java.awt.Font +import java.io.InputStream import javax.swing.* +import kotlin.math.roundToInt + class OutputPanel( - private val bottomButtonsPanel: BottomButtonsPanel + bottomButtonsPanel: BottomButtonsPanel ) : JPanel(GridBagLayout()) { val outputArea = JTextArea("") - constructor(closeCallback: () -> Unit) : this(DefaultBottomButtonsPanel(closeCallback)) + companion object { + val monoFont: Font by lazy { + Font.createFont( + Font.TRUETYPE_FONT, + this::class.java.getResourceAsStream("/fonts/JetBrainsMono-Medium.ttf") + ) + } + } init { if(bottomButtonsPanel is DefaultBottomButtonsPanel) { bottomButtonsPanel.outputTextSupplier = { outputArea.text } } + // JTextArea will use /fonts/JetBrainsMono-Medium.ttf as font + outputArea.font = monoFont.deriveFont(13f) + outputArea.isEditable = false outputArea.highlighter = null - outputArea.lineWrap = true - outputArea.wrapStyleWord = true + + // set the background color to a darker tone + outputArea.background = if(FlatLaf.isLafDark()) { + outputArea.background.darker() + } else { + java.awt.Color( + (outputArea.background.red * 0.95).roundToInt(), + (outputArea.background.green * 0.95).roundToInt(), + (outputArea.background.blue * 0.95).roundToInt(), + 255 + ) + } val outputScroll = JScrollPane(outputArea) outputScroll.verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_ALWAYS diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateCameraSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateCameraSource.java index b3ecdf00..016330b8 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateCameraSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateCameraSource.java @@ -32,6 +32,7 @@ import io.github.deltacv.steve.opencv.OpenCvWebcam; import io.github.deltacv.steve.opencv.OpenCvWebcamBackend; import io.github.deltacv.steve.openimaj.OpenIMAJWebcamBackend; +import io.github.deltacv.steve.openpnp.OpenPnpBackend; import org.opencv.core.Mat; import org.opencv.core.Size; import org.slf4j.Logger; @@ -95,7 +96,14 @@ public CreateCameraSource(JFrame parent, EOCVSim eocvSim) { public void initCreateImageSource() { WebcamDriver preferredDriver = eocvSim.getConfig().preferredWebcamDriver; - if(preferredDriver == WebcamDriver.OpenIMAJ) { + if(preferredDriver == WebcamDriver.OpenPnp) { + Webcam.Companion.setBackend(OpenPnpBackend.INSTANCE); + try { + webcams = Webcam.Companion.getAvailableWebcams(); + } catch (Throwable e) { + logger.error("OpenPnp failed to discover cameras with error", e); + } + } else if(preferredDriver == WebcamDriver.OpenIMAJ) { try { Webcam.Companion.setBackend(OpenIMAJWebcamBackend.INSTANCE); webcams = Webcam.Companion.getAvailableWebcams(); diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/theme/Theme.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/theme/Theme.java index 98fb99c0..1b061492 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/theme/Theme.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/theme/Theme.java @@ -33,6 +33,9 @@ public enum Theme { Default(() -> { UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName()); }), + System(() -> { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + }), Light(FlatLightLaf::setup), Dark(FlatDarkLaf::setup), Darcula(FlatDarculaLaf::setup), diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/ThreadedMatPoster.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/ThreadedMatPoster.java index d38c12fe..3c0e7327 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/ThreadedMatPoster.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/ThreadedMatPoster.java @@ -1,221 +1,223 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui.util; - -import com.github.serivesmejia.eocvsim.util.fps.FpsCounter; -import io.github.deltacv.common.image.MatPoster; -import org.firstinspires.ftc.robotcore.internal.collections.EvictingBlockingQueue; -import org.opencv.core.Mat; -import org.openftc.easyopencv.MatRecycler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.concurrent.ArrayBlockingQueue; - -public class ThreadedMatPoster implements MatPoster { - private final ArrayList postables = new ArrayList<>(); - - private final EvictingBlockingQueue postQueue; - private final MatRecycler matRecycler; - - private final String name; - - private final Thread posterThread; - - public final FpsCounter fpsCounter = new FpsCounter(); - - private final Object lock = new Object(); - - private volatile boolean paused = false; - - private volatile boolean hasPosterThreadStarted = false; - - Logger logger; - - public static ThreadedMatPoster createWithoutRecycler(String name, int maxQueueItems) { - return new ThreadedMatPoster(name, maxQueueItems, null); - } - - public ThreadedMatPoster(String name, int maxQueueItems) { - this(name, new MatRecycler(maxQueueItems + 2)); - } - - public ThreadedMatPoster(String name, MatRecycler recycler) { - this(name, recycler.getSize(), recycler); - } - - public ThreadedMatPoster(String name, int maxQueueItems, MatRecycler recycler) { - postQueue = new EvictingBlockingQueue<>(new ArrayBlockingQueue<>(maxQueueItems)); - matRecycler = recycler; - posterThread = new Thread(new PosterRunnable(), "MatPoster-" + name + "-Thread"); - - this.name = name; - - logger = LoggerFactory.getLogger("MatPoster-" + name); - - postQueue.setEvictAction(this::evict); //release mat and return it to recycler if it's dropped by the EvictingBlockingQueue - } - - @Override - public void post(Mat m, Object context) { - if (m == null || m.empty()) { - logger.warn("Tried to post empty or null mat, skipped this frame."); - return; - } - - if (matRecycler != null) { - if(matRecycler.getAvailableMatsAmount() < 1) { - //evict one if we don't have any available mats in the recycler - evict(postQueue.poll()); - } - - MatRecycler.RecyclableMat recycledMat = matRecycler.takeMatOrNull(); - m.copyTo(recycledMat); - - postQueue.offer(recycledMat); - } else { - postQueue.offer(m); - } - } - - public void synchronizedPost(Mat m) { - synchronize(() -> post(m)); - } - - public Mat pull() throws InterruptedException { - synchronized(lock) { - return postQueue.take(); - } - } - - public void clearQueue() { - if(postQueue.size() == 0) return; - - synchronized(lock) { - postQueue.clear(); - } - } - - public void synchronize(Runnable runn) { - synchronized(lock) { - runn.run(); - } - } - - public void addPostable(Postable postable) { - //start mat posting thread if it hasn't been started yet - if (!posterThread.isAlive() && !hasPosterThreadStarted) { - posterThread.start(); - } - - postables.add(postable); - } - - public void stop() { - logger.info("Destroying..."); - - posterThread.interrupt(); - - for (Mat m : postQueue) { - if (m != null) { - if(m instanceof MatRecycler.RecyclableMat) { - ((MatRecycler.RecyclableMat)m).returnMat(); - } - } - } - - matRecycler.releaseAll(); - } - - private void evict(Mat m) { - if (m instanceof MatRecycler.RecyclableMat) { - ((MatRecycler.RecyclableMat) m).returnMat(); - } - m.release(); - } - - public void setPaused(boolean paused) { - this.paused = paused; - } - - public boolean getPaused() { - synchronized(lock) { - return paused; - } - } - - public String getName() { - return name; - } - - public interface Postable { - void post(Mat m); - } - - private class PosterRunnable implements Runnable { - - private Mat postableMat = new Mat(); - - @Override - public void run() { - hasPosterThreadStarted = true; - - while (!Thread.interrupted()) { - - while(paused && !Thread.currentThread().isInterrupted()) { - Thread.yield(); - } - - if (postQueue.size() == 0 || postables.size() == 0) continue; //skip if we have no queued frames - - synchronized(lock) { - fpsCounter.update(); - - try { - Mat takenMat = postQueue.take(); - - for (Postable postable : postables) { - takenMat.copyTo(postableMat); - postable.post(postableMat); - } - - takenMat.release(); - - if (takenMat instanceof MatRecycler.RecyclableMat) { - ((MatRecycler.RecyclableMat) takenMat).returnMat(); - } - } catch (InterruptedException e) { - e.printStackTrace(); - break; - } catch (Exception ex) { } - } - - } - - logger.warn("Thread interrupted (" + Integer.toHexString(hashCode()) + ")"); - } - } - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.util; + +import com.github.serivesmejia.eocvsim.util.fps.FpsCounter; +import io.github.deltacv.common.image.MatPoster; +import org.firstinspires.ftc.robotcore.internal.collections.EvictingBlockingQueue; +import org.opencv.core.Mat; +import org.openftc.easyopencv.MatRecycler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.concurrent.ArrayBlockingQueue; + +public class ThreadedMatPoster implements MatPoster { + private final ArrayList postables = new ArrayList<>(); + + private final EvictingBlockingQueue postQueue; + private final MatRecycler matRecycler; + + private final String name; + + private final Thread posterThread; + + public final FpsCounter fpsCounter = new FpsCounter(); + + private final Object lock = new Object(); + + private volatile boolean paused = false; + + private volatile boolean hasPosterThreadStarted = false; + + Logger logger; + + public static ThreadedMatPoster createWithoutRecycler(String name, int maxQueueItems) { + return new ThreadedMatPoster(name, maxQueueItems, null); + } + + public ThreadedMatPoster(String name, int maxQueueItems) { + this(name, new MatRecycler(maxQueueItems + 2)); + } + + public ThreadedMatPoster(String name, MatRecycler recycler) { + this(name, recycler.getSize(), recycler); + } + + public ThreadedMatPoster(String name, int maxQueueItems, MatRecycler recycler) { + postQueue = new EvictingBlockingQueue<>(new ArrayBlockingQueue<>(maxQueueItems)); + matRecycler = recycler; + posterThread = new Thread(new PosterRunnable(), "MatPoster-" + name + "-Thread"); + + this.name = name; + + logger = LoggerFactory.getLogger("MatPoster-" + name); + + postQueue.setEvictAction(this::evict); //release mat and return it to recycler if it's dropped by the EvictingBlockingQueue + } + + @Override + public void post(Mat m, Object context) { + if (m == null || m.empty()) { + logger.warn("Tried to post empty or null mat, skipped this frame."); + return; + } + + if (matRecycler != null) { + if(matRecycler.getAvailableMatsAmount() < 1) { + //evict one if we don't have any available mats in the recycler + evict(postQueue.poll()); + } + + MatRecycler.RecyclableMat recycledMat = matRecycler.takeMatOrNull(); + m.copyTo(recycledMat); + + postQueue.offer(recycledMat); + } else { + postQueue.offer(m); + } + } + + public void synchronizedPost(Mat m) { + synchronize(() -> post(m)); + } + + public Mat pull() throws InterruptedException { + synchronized(lock) { + return postQueue.take(); + } + } + + public void clearQueue() { + if(postQueue.size() == 0) return; + + synchronized(lock) { + postQueue.clear(); + } + } + + public void synchronize(Runnable runn) { + synchronized(lock) { + runn.run(); + } + } + + public void addPostable(Postable postable) { + //start mat posting thread if it hasn't been started yet + if (!posterThread.isAlive() && !hasPosterThreadStarted) { + posterThread.start(); + } + + postables.add(postable); + } + + public void stop() { + logger.info("Destroying..."); + + posterThread.interrupt(); + + for (Mat m : postQueue) { + if (m != null) { + if(m instanceof MatRecycler.RecyclableMat) { + ((MatRecycler.RecyclableMat)m).returnMat(); + } + } + } + + matRecycler.releaseAll(); + } + + private void evict(Mat m) { + if (m instanceof MatRecycler.RecyclableMat) { + ((MatRecycler.RecyclableMat) m).returnMat(); + } + if(m != null) { + m.release(); + } + } + + public void setPaused(boolean paused) { + this.paused = paused; + } + + public boolean getPaused() { + synchronized(lock) { + return paused; + } + } + + public String getName() { + return name; + } + + public interface Postable { + void post(Mat m); + } + + private class PosterRunnable implements Runnable { + + private Mat postableMat = new Mat(); + + @Override + public void run() { + hasPosterThreadStarted = true; + + while (!Thread.interrupted()) { + + while(paused && !Thread.currentThread().isInterrupted()) { + Thread.yield(); + } + + if (postQueue.isEmpty() || postables.isEmpty()) continue; //skip if we have no queued frames + + synchronized(lock) { + fpsCounter.update(); + + try { + Mat takenMat = postQueue.take(); + + for (Postable postable : postables) { + takenMat.copyTo(postableMat); + postable.post(postableMat); + } + + takenMat.release(); + + if (takenMat instanceof MatRecycler.RecyclableMat) { + ((MatRecycler.RecyclableMat) takenMat).returnMat(); + } + } catch (InterruptedException e) { + logger.warn("Thread interrupted ({})", Integer.toHexString(hashCode())); + break; + } catch (Exception ex) { } + } + + } + + logger.warn("Thread interrupted ({})", Integer.toHexString(hashCode())); + } + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/WebcamDriver.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/WebcamDriver.kt index 1dba5fed..7ea6591a 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/WebcamDriver.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/WebcamDriver.kt @@ -1,28 +1,28 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui.util - -enum class WebcamDriver { - OpenIMAJ, OpenCV +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.util + +enum class WebcamDriver { + OpenPnp, OpenIMAJ, OpenCV } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java index 27af6819..77ac0aae 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java @@ -134,6 +134,8 @@ public void addInputSource(String name, InputSource inputSource, boolean dispatc if (sources.containsKey(name)) return; + inputSource.eocvSim = eocvSim; + if(eocvSim.visualizer.sourceSelectorPanel != null) { eocvSim.visualizer.sourceSelectorPanel.setAllowSourceSwitching(false); } @@ -206,6 +208,8 @@ public boolean setInputSource(String sourceName, boolean makeDefault) { public boolean setInputSource(String sourceName) { InputSource src = null; + SysUtil.debugLogCalled("setInputSource"); + if(sourceName == null) { src = new NullSource(); } else { @@ -214,7 +218,6 @@ public boolean setInputSource(String sourceName) { if (src != null) { src.reset(); - src.eocvSim = eocvSim; } //check if source type is a camera, and if so, create a please wait dialog @@ -297,6 +300,8 @@ public void pauseIfImageTwoFrames() { } public void requestSetInputSource(String name) { + SysUtil.debugLogCalled("requestSetInputSource"); + eocvSim.onMainUpdate.doOnce(() -> setInputSource(name)); } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/CameraSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/CameraSource.java index 3998454a..9ddfcbf2 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/CameraSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/CameraSource.java @@ -24,14 +24,18 @@ package com.github.serivesmejia.eocvsim.input.source; import com.github.serivesmejia.eocvsim.gui.Visualizer; +import com.github.serivesmejia.eocvsim.gui.util.WebcamDriver; import com.github.serivesmejia.eocvsim.input.InputSource; import com.github.serivesmejia.eocvsim.util.StrUtil; import com.google.gson.annotations.Expose; import io.github.deltacv.steve.Webcam; +import io.github.deltacv.steve.WebcamPropertyControl; import io.github.deltacv.steve.WebcamRotation; import io.github.deltacv.steve.opencv.OpenCvWebcam; import io.github.deltacv.steve.opencv.OpenCvWebcamBackend; import io.github.deltacv.steve.openimaj.OpenIMAJWebcamBackend; +import io.github.deltacv.steve.openpnp.OpenPnpBackend; +import io.github.deltacv.steve.openpnp.OpenPnpWebcam; import org.opencv.core.Mat; import org.opencv.core.Size; import org.opencv.imgproc.Imgproc; @@ -100,6 +104,11 @@ public CameraSource(int webcamIndex, Size size) { isLegacyByIndex = true; } + public WebcamPropertyControl getWebcamPropertyControl() { + if(camera == null) return null; + return camera.getPropertyControl(); + } + @Override public void setSize(Size size) { this.size = size; @@ -113,7 +122,11 @@ public boolean init() { if(rotation == null) rotation = WebcamRotation.UPRIGHT; if(webcamName != null) { - Webcam.Companion.setBackend(OpenIMAJWebcamBackend.INSTANCE); + if(eocvSim.getConfig().preferredWebcamDriver == WebcamDriver.OpenPnp) { + Webcam.Companion.setBackend(OpenPnpBackend.INSTANCE); + } else { + Webcam.Companion.setBackend(OpenIMAJWebcamBackend.INSTANCE); + } List webcams = Webcam.Companion.getAvailableWebcams(); @@ -143,10 +156,16 @@ public boolean init() { camera.setResolution(size); camera.setRotation(rotation); - camera.open(); + + try { + camera.open(); + } catch (Exception ex) { + logger.error("Error while opening camera " + webcamIndex, ex); + return false; + } if (!camera.isOpen()) { - logger.error("Unable to open camera " + webcamIndex); + logger.error("Unable to open camera " + webcamIndex + ", isOpen() returned false."); return false; } @@ -220,9 +239,12 @@ public Mat update() { if (size == null) size = lastFrame.size(); - // not needed. cameras return RGB now - // Imgproc.cvtColor(newFrame, lastFrame, Imgproc.COLOR_BGR2RGB); - newFrame.copyTo(lastFrame); + + if(camera instanceof OpenPnpWebcam) + // we will look like the smurfs if we don't do this... + // (OpenPnpWebcam returns BGR instead of RGB, ugh, i should fix that) + Imgproc.cvtColor(newFrame, lastFrame, Imgproc.COLOR_BGR2RGB); + else newFrame.copyTo(lastFrame); newFrame.release(); newFrame.returnMat(); diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt index c9a910dd..2547c05a 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt @@ -34,6 +34,7 @@ import com.github.serivesmejia.eocvsim.pipeline.util.PipelineExceptionTracker import com.github.serivesmejia.eocvsim.pipeline.util.PipelineSnapshot import com.github.serivesmejia.eocvsim.util.ReflectUtil import com.github.serivesmejia.eocvsim.util.StrUtil +import com.github.serivesmejia.eocvsim.util.SysUtil import com.github.serivesmejia.eocvsim.util.event.EventHandler import com.github.serivesmejia.eocvsim.util.fps.FpsCounter import com.github.serivesmejia.eocvsim.util.loggerForThis @@ -572,7 +573,7 @@ class PipelineManager( logger.info("Changing to pipeline ${pipelineClass.name}") - debugLogCalled("forceChangePipeline") + SysUtil.debugLogCalled("forceChangePipeline") val instantiator = getInstantiatorFor(pipelineClass) @@ -664,7 +665,7 @@ class PipelineManager( } fun requestForceChangePipeline(index: Int) { - debugLogCalled("requestForceChangePipeline") + SysUtil.debugLogCalled("requestForceChangePipeline") onUpdate.doOnce { forceChangePipeline(index) } } @@ -769,15 +770,6 @@ class PipelineManager( forceChangePipeline(0) // default pipeline } - private fun debugLogCalled(name: String) { - val builder = StringBuilder() - for (s in Thread.currentThread().stackTrace) { - builder.appendLine(s.toString()) - } - - logger.debug("$name called in: {}", builder.toString().trim()) - } - } enum class PipelineTimeout(val ms: Long, val coolName: String) { diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineStandardFileManager.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineStandardFileManager.kt index 9ad3854f..5aab1bd6 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineStandardFileManager.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineStandardFileManager.kt @@ -44,7 +44,13 @@ class PipelineStandardFileManager(delegate: StandardJavaFileManager) : Delegatin val classpathList = arrayListOf() logger.trace("Scanning classpath files...") - + logger.trace("Classpath: {}", System.getProperty("java.class.path")) + + //add all jars in the libs folder...hardcoded for JavaPackager, ugh + classpathList.addAll( + SysUtil.filesUnder(File(System.getProperty("user.dir") + File.separator + "libs"), ".jar") + ) + for(file in SysUtil.getClasspathFiles()) { val files = SysUtil.filesUnder(file, ".jar") files.forEach { diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunerManager.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunerManager.java index 8995031a..09367341 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunerManager.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunerManager.java @@ -75,7 +75,7 @@ public void init() { if(tunableFieldAcceptors == null) { tunableFieldAcceptors = new HashMap<>(); // oh god... - eocvSim.getClasspathScan().getScanResult().getTunableFieldAcceptorClasses().forEach(tunableFieldAcceptors::put); + tunableFieldAcceptors.putAll(eocvSim.getClasspathScan().getScanResult().getTunableFieldAcceptorClasses()); } // for some reason, acceptorManager becomes null after a certain time passes @@ -89,8 +89,8 @@ public void init() { firstInit = false; } - if (eocvSim.pipelineManager.getCurrentPipeline() != null) { - addFieldsFrom(eocvSim.pipelineManager.getCurrentPipeline()); + if (eocvSim.pipelineManager.getReflectTarget() != null) { + addFieldsFrom(eocvSim.pipelineManager.getReflectTarget()); eocvSim.visualizer.updateTunerFields(createTunableFieldPanels()); for(TunableField field : fields.toArray(new TunableField[0])) { @@ -151,7 +151,7 @@ public Class getTunableFieldOf(VirtualField field) { return tunableFieldClass; } - public void addFieldsFrom(OpenCvPipeline pipeline) { + public void addFieldsFrom(Object pipeline) { if (pipeline == null) return; VirtualReflectContext reflectContext = reflect.contextOf(pipeline); @@ -165,11 +165,11 @@ public void addFieldsFrom(OpenCvPipeline pipeline) { // we can't handle this type if(tunableFieldClass == null) continue; - //yay we have a registered TunableField which handles this + //yay we have a registered TunableField which handles this. //now, lets do some more reflection to instantiate this TunableField //and add it to the list... try { - Constructor constructor = tunableFieldClass.getConstructor(OpenCvPipeline.class, VirtualField.class, EOCVSim.class); + Constructor constructor = tunableFieldClass.getConstructor(Object.class, VirtualField.class, EOCVSim.class); this.fields.add(constructor.newInstance(pipeline, field, eocvSim)); } catch(InvocationTargetException e) { if(e.getCause() instanceof CancelTunableFieldAddingException) { diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/BooleanField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/BooleanField.java index e1bd7809..3480c0a8 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/BooleanField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/BooleanField.java @@ -37,7 +37,7 @@ public class BooleanField extends TunableField { boolean lastVal; volatile boolean hasChanged = false; - public BooleanField(OpenCvPipeline instance, VirtualField reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + public BooleanField(Object instance, VirtualField reflectionField, EOCVSim eocvSim) throws IllegalAccessException { super(instance, reflectionField, eocvSim, AllowMode.TEXT); setGuiFieldAmount(0); diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/EnumField.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/EnumField.kt index c25a5a2c..220b2628 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/EnumField.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/EnumField.kt @@ -8,7 +8,7 @@ import io.github.deltacv.eocvsim.virtualreflect.VirtualField import org.openftc.easyopencv.OpenCvPipeline @RegisterTunableField -class EnumField(private val instance: OpenCvPipeline, +class EnumField(instance: Any, reflectionField: VirtualField, eocvSim: EOCVSim) : TunableField>(instance, reflectionField, eocvSim, AllowMode.TEXT) { diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/NumericField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/NumericField.java index 26f7405b..f28f02f3 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/NumericField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/NumericField.java @@ -38,7 +38,7 @@ abstract public class NumericField extends TunableField { protected volatile boolean hasChanged = false; - public NumericField(OpenCvPipeline instance, VirtualField reflectionField, EOCVSim eocvSim, AllowMode allowMode) throws IllegalAccessException { + public NumericField(Object instance, VirtualField reflectionField, EOCVSim eocvSim, AllowMode allowMode) throws IllegalAccessException { super(instance, reflectionField, eocvSim, allowMode); } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/StringField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/StringField.java index ee78c291..80e60552 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/StringField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/StringField.java @@ -41,7 +41,7 @@ public class StringField extends TunableField { volatile boolean hasChanged = false; - public StringField(OpenCvPipeline instance, VirtualField reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + public StringField(Object instance, VirtualField reflectionField, EOCVSim eocvSim) throws IllegalAccessException { super(instance, reflectionField, eocvSim, AllowMode.TEXT); if(initialFieldValue != null) { diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/PointField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/PointField.java index 41c399a2..4de7b0c0 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/PointField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/PointField.java @@ -42,7 +42,7 @@ public class PointField extends TunableField { volatile boolean hasChanged = false; - public PointField(OpenCvPipeline instance, VirtualField reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + public PointField(Object instance, VirtualField reflectionField, EOCVSim eocvSim) throws IllegalAccessException { super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL); if(initialFieldValue != null) { diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/RectField.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/RectField.kt index 7bd0b937..989b88ce 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/RectField.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/RectField.kt @@ -32,7 +32,7 @@ import org.openftc.easyopencv.OpenCvPipeline import java.lang.reflect.Field @RegisterTunableField -class RectField(instance: OpenCvPipeline, reflectionField: VirtualField, eocvSim: EOCVSim) : +class RectField(instance: Any, reflectionField: VirtualField, eocvSim: EOCVSim) : TunableField(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL) { private var rect = arrayOf(0.0, 0.0, 0.0, 0.0) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/ScalarField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/ScalarField.java index 1c380131..3ff0bdd9 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/ScalarField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/ScalarField.java @@ -43,7 +43,7 @@ public class ScalarField extends TunableField { volatile boolean hasChanged = false; - public ScalarField(OpenCvPipeline instance, VirtualField reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + public ScalarField(Object instance, VirtualField reflectionField, EOCVSim eocvSim) throws IllegalAccessException { super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL); if(initialFieldValue == null) { @@ -89,12 +89,16 @@ public void updateGuiFieldValues() { @Override public void setFieldValue(int index, Object newValue) throws IllegalAccessException { try { + double value; + if(newValue instanceof String) { - scalar.val[index] = Double.parseDouble((String) newValue); + value = Double.parseDouble((String) newValue); } else { - scalar.val[index] = (double)newValue; + value = (double)newValue; } - } catch (Exception ex) { + + scalar.val[index] = value; + } catch (NumberFormatException ex) { throw new IllegalArgumentException("Parameter should be a valid number", ex); } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/DoubleField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/DoubleField.java index d7af5eda..06fb9e8e 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/DoubleField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/DoubleField.java @@ -34,7 +34,7 @@ @RegisterTunableField public class DoubleField extends NumericField { - public DoubleField(OpenCvPipeline instance, VirtualField reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + public DoubleField(Object instance, VirtualField reflectionField, EOCVSim eocvSim) throws IllegalAccessException { super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL); value = (double) initialFieldValue; } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/FloatField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/FloatField.java index eb6928b7..a7fe7b12 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/FloatField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/FloatField.java @@ -34,7 +34,7 @@ @RegisterTunableField public class FloatField extends NumericField { - public FloatField(OpenCvPipeline instance, VirtualField reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + public FloatField(Object instance, VirtualField reflectionField, EOCVSim eocvSim) throws IllegalAccessException { super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL); value = (float) initialFieldValue; } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/IntegerField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/IntegerField.java index a4cd5b96..0c1a8bd6 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/IntegerField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/IntegerField.java @@ -34,7 +34,7 @@ @RegisterTunableField public class IntegerField extends NumericField { - public IntegerField(OpenCvPipeline instance, VirtualField reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + public IntegerField(Object instance, VirtualField reflectionField, EOCVSim eocvSim) throws IllegalAccessException { super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS); value = (int) initialFieldValue; } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/LongField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/LongField.java index 839a7b42..c0c18a25 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/LongField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/LongField.java @@ -34,7 +34,7 @@ @RegisterTunableField public class LongField extends NumericField { - public LongField(OpenCvPipeline instance, VirtualField reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + public LongField(Object instance, VirtualField reflectionField, EOCVSim eocvSim) throws IllegalAccessException { super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS); value = (long) initialFieldValue; } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/JavaProcess.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/JavaProcess.java index 451ad89c..4b4451a5 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/JavaProcess.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/JavaProcess.java @@ -1,110 +1,180 @@ -/* - * Copyright (c) 2024 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.util; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.util.LinkedList; -import java.util.List; - -/** - * A utility class for executing a Java process to run a main class within this project. - */ -public final class JavaProcess { - - public interface ProcessIOReceiver { - void receive(InputStream in, InputStream err); - } - - private JavaProcess() {} - - /** - * Executes a Java process with the given class and arguments. - * @param klass the class to execute - * @param ioReceiver the receiver for the process' input and error streams (will use inheritIO if null) - * @param classpath the classpath to use - * (use System.getProperty("java.class.path") for the default classpath) - * @param jvmArgs the JVM arguments to pass to the process - * @param args the arguments to pass to the class - * @return the exit value of the process - * @throws InterruptedException if the process is interrupted - * @throws IOException if an I/O error occurs - */ - public static int execClasspath(Class klass, ProcessIOReceiver ioReceiver, String classpath, List jvmArgs, List args) throws InterruptedException, IOException { - String javaHome = System.getProperty("java.home"); - String javaBin = javaHome + - File.separator + "bin" + - File.separator + "java"; - String className = klass.getName(); - - List command = new LinkedList<>(); - command.add(javaBin); - if (jvmArgs != null) { - command.addAll(jvmArgs); - } - command.add("-cp"); - command.add(classpath); - command.add(className); - if (args != null) { - command.addAll(args); - } - - ProcessBuilder builder = new ProcessBuilder(command); - - if (ioReceiver != null) { - Process process = builder.start(); - ioReceiver.receive(process.getInputStream(), process.getErrorStream()); - killOnExit(process); - - process.waitFor(); - return process.exitValue(); - } else { - builder.inheritIO(); - Process process = builder.start(); - killOnExit(process); - - process.waitFor(); - return process.exitValue(); - } - } - - private static void killOnExit(Process process) { - Runtime.getRuntime().addShutdownHook(new Thread(process::destroy)); - } - - /** - * Executes a Java process with the given class and arguments. - * @param klass the class to execute - * @param jvmArgs the JVM arguments to pass to the process - * @param args the arguments to pass to the class - * @return the exit value of the process - * @throws InterruptedException if the process is interrupted - * @throws IOException if an I/O error occurs - */ - public static int exec(Class klass, List jvmArgs, List args) throws InterruptedException, IOException { - return execClasspath(klass, null, System.getProperty("java.class.path"), jvmArgs, args); - } - +/* + * Copyright (c) 2024 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.LinkedList; +import java.util.List; +import java.util.Scanner; + +/** + * A utility class for executing a Java process to run a main class within this project. + */ +public final class JavaProcess { + + public interface ProcessIOReceiver { + void receive(InputStream in, InputStream err, int pid); + } + + public static class SLF4JIOReceiver implements JavaProcess.ProcessIOReceiver { + + private final Logger logger; + + public SLF4JIOReceiver(Logger logger) { + this.logger = logger; + } + + @Override + public void receive(InputStream out, InputStream err, int pid) { + new Thread(() -> { + Thread.currentThread().setContextClassLoader(SLF4JIOReceiver.class.getClassLoader()); + + Scanner sc = new Scanner(out); + while (sc.hasNextLine()) { + logger.info(sc.nextLine()); + } + }, "SLFJ4IOReceiver-out-" + pid).start(); + + new Thread(() -> { + Thread.currentThread().setContextClassLoader(SLF4JIOReceiver.class.getClassLoader()); + + Scanner sc = new Scanner(err); + while (sc.hasNextLine()) { + logger.error(sc.nextLine()); + } + }, "SLF4JIOReceiver-err-" + pid).start(); + + logger.debug("SLF4JIOReceiver started for PID: {}", pid); // Debug log to check if the logger is working + } + + } + + + private JavaProcess() {} + + private static int count; + + public static boolean killSubprocessesOnExit = true; + + private static final Logger logger = LoggerFactory.getLogger(JavaProcess.class); + + /** + * Executes a Java process with the given class and arguments. + * @param klass the class to execute + * @param ioReceiver the receiver for the process' input and error streams (will use inheritIO if null) + * @param classpath the classpath to use + * (use System.getProperty("java.class.path") for the default classpath) + * @param jvmArgs the JVM arguments to pass to the process + * @param args the arguments to pass to the class + * @return the exit value of the process + * @throws InterruptedException if the process is interrupted + * @throws IOException if an I/O error occurs + */ + public static int execClasspath(Class klass, ProcessIOReceiver ioReceiver, String classpath, List jvmArgs, List args) throws InterruptedException, IOException { + String javaHome = System.getProperty("java.home"); + String javaBin = javaHome + + File.separator + "bin" + + File.separator + "java"; + String className = klass.getName(); + + List command = new LinkedList<>(); + command.add(javaBin); + if (jvmArgs != null) { + command.addAll(jvmArgs); + } + command.add("-cp"); + command.add(classpath); + command.add(className); + if (args != null) { + command.addAll(args); + } + + int processCount = count; + + logger.info("Executing Java process #{} at \"{}\", main class \"{}\", JVM args \"{}\" and args \"{}\"", processCount, javaBin, className, jvmArgs, args); + + count++; + + ProcessBuilder builder = new ProcessBuilder(command); + + if (ioReceiver != null) { + Process process = builder.start(); + logger.info("Started #{} with PID {}, inheriting IO to {}", processCount, process.pid(), ioReceiver.getClass().getSimpleName()); + + ioReceiver.receive(process.getInputStream(), process.getErrorStream(), (int) process.pid()); + killOnExit(process); + + process.waitFor(); + return process.exitValue(); + } else { + builder.inheritIO(); + Process process = builder.start(); + logger.info("Started #{} with PID {}, IO will be inherited to System.out and System.err", processCount, process.pid()); + + killOnExit(process); + + process.waitFor(); + return process.exitValue(); + } + } + + private static void killOnExit(Process process) { + if(!killSubprocessesOnExit) return; + Runtime.getRuntime().addShutdownHook(new Thread(process::destroy)); + } + + /** + * Executes a Java process with the given class and arguments. + * @param klass the class to execute + * @param jvmArgs the JVM arguments to pass to the process + * @param args the arguments to pass to the class + * @return the exit value of the process + * @throws InterruptedException if the process is interrupted + * @throws IOException if an I/O error occurs + */ + public static int exec(Class klass, List jvmArgs, List args) throws InterruptedException, IOException { + return execClasspath(klass, null, System.getProperty("java.class.path"), jvmArgs, args); + } + + /** + * Executes a Java process with the given class and arguments. + * @param klass the class to execute + * @param ioReceiver the receiver for the process' input and error streams (will use inheritIO if null) + * @param jvmArgs the JVM arguments to pass to the process + * @param args the arguments to pass to the class + * @return the exit value of the process + * @throws InterruptedException if the process is interrupted + * @throws IOException if an I/O error occurs + */ + public static int exec(Class klass, ProcessIOReceiver ioReceiver, List jvmArgs, List args) throws InterruptedException, IOException { + return execClasspath(klass, ioReceiver, System.getProperty("java.class.path"), jvmArgs, args); + } + + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/SysUtil.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/SysUtil.java index fd2cadee..fa70650f 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/SysUtil.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/SysUtil.java @@ -289,7 +289,19 @@ public static List getClasspathFiles() { ArrayList files = new ArrayList<>(); for(String path : classpaths) { - files.add(new File(path)); + File absoluteFile = new File(path); + + if(absoluteFile.exists()) { + files.add(absoluteFile); + } else { + File relativeFile = new File(System.getProperty("user.dir") + File.separator + path); + logger.trace("Checking relative file {}", relativeFile.getAbsolutePath()); + + if(relativeFile.exists()) { + files.add(relativeFile); + logger.trace("Relative file OK"); + } + } } return files; @@ -327,6 +339,16 @@ public static CommandResult runShellCommand(String command) { return result; } + + public static void debugLogCalled(String name) { + StringBuilder builder = new StringBuilder(); + for (StackTraceElement s : Thread.currentThread().getStackTrace()) { + builder.append(s.toString()).append("\n"); + } + + logger.debug("{} called in: {}", name, builder.toString().trim()); + } + public enum OperatingSystem { WINDOWS, LINUX, diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/FileExt.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/FileExt.kt index d2264bef..8ef35ac0 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/FileExt.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/FileExt.kt @@ -23,7 +23,9 @@ package com.github.serivesmejia.eocvsim.util.extension +import com.github.serivesmejia.eocvsim.util.SysUtil import java.io.File +import java.security.MessageDigest /** * Operator function to concatenate a string to a file path @@ -31,3 +33,10 @@ import java.io.File operator fun File.plus(str: String): File { return File(this.absolutePath, str) } + + +fun File.fileHash(algorithm: String = "SHA-256"): String { + val messageDigest = MessageDigest.getInstance(algorithm) + messageDigest.update(readBytes()) + return SysUtil.byteArray2Hex(messageDigest.digest()) +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/StrExt.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/StrExt.kt index 2fd8253b..34f4e756 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/StrExt.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/StrExt.kt @@ -1,5 +1,8 @@ package com.github.serivesmejia.eocvsim.util.extension +import com.github.serivesmejia.eocvsim.util.SysUtil +import java.security.MessageDigest + /** * Remove a string from the end of this string * @param rem the string to remove @@ -10,4 +13,12 @@ fun String.removeFromEnd(rem: String): String { return substring(0, length - rem.length).trim() } return trim() +} + +val Any.hashString get() = Integer.toHexString(hashCode())!! + +val String.hashString: String get() { + val messageDigest = MessageDigest.getInstance("SHA-256") + val hash = messageDigest.digest(toByteArray()) + return SysUtil.byteArray2Hex(hash) } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/gui/dialog/SuperAccessRequestMain.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/serialization/PolymorphicAdapter.kt similarity index 53% rename from EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/gui/dialog/SuperAccessRequestMain.java rename to EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/serialization/PolymorphicAdapter.kt index 6d733f60..2c788bb7 100644 --- a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/gui/dialog/SuperAccessRequestMain.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/serialization/PolymorphicAdapter.kt @@ -1,34 +1,54 @@ -/* - * Copyright (c) 2024 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package io.github.deltacv.eocvsim.gui.dialog; - -import javax.swing.*; - -public class SuperAccessRequestMain { - public static void main(String[] args) { - SwingUtilities.invokeLater(() -> { - new SuperAccessRequest(args[0].replace("-", " "), args[1].replace("-", " ")); - }); - } -} +/* + * Copyright (c) 2024 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.util.serialization + +import com.google.gson.* +import java.lang.reflect.Type + +private val gson = Gson() + +open class PolymorphicAdapter( + val name: String, + val classloader: ClassLoader = PolymorphicAdapter::class.java.classLoader +) : JsonSerializer, JsonDeserializer { + + override fun serialize(src: T, typeOfSrc: Type, context: JsonSerializationContext): JsonElement { + val obj = JsonObject() + + obj.addProperty("${name}Class", src!!::class.java.name) + obj.add(name, gson.toJsonTree(src)) + + return obj + } + + @Suppress("UNCHECKED_CAST") + override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): T { + val className = json.asJsonObject.get("${name}Class").asString + + val clazz = classloader.loadClass(className) + + return gson.fromJson(json.asJsonObject.get(name), clazz) as T + } + +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt b/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt index dda9ea55..0e16ae09 100644 --- a/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt +++ b/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt @@ -1,118 +1,121 @@ -package com.qualcomm.robotcore.eventloop.opmode - -import com.github.serivesmejia.eocvsim.EOCVSim -import com.github.serivesmejia.eocvsim.input.InputSource -import com.github.serivesmejia.eocvsim.input.InputSourceManager -import com.github.serivesmejia.eocvsim.pipeline.handler.SpecificPipelineHandler -import com.github.serivesmejia.eocvsim.util.event.EventHandler -import com.qualcomm.robotcore.hardware.HardwareMap -import io.github.deltacv.eocvsim.input.VisionInputSourceHander -import io.github.deltacv.vision.external.source.ThreadSourceHander -import org.firstinspires.ftc.robotcore.external.Telemetry -import org.openftc.easyopencv.OpenCvPipeline -import org.openftc.easyopencv.OpenCvViewport - -/** - * Enum class to classify the type of an OpMode - */ -enum class OpModeType { AUTONOMOUS, TELEOP } - -/** - * Handler for OpModes in the pipeline manager - * It helps to manage the lifecycle of OpModes - * and to set the input source and viewport - * within it in order to transform a pipeline - * lifecycle into an OpMode lifecycle - * @param inputSourceManager the input source manager to set the input source - * @param viewport the viewport to set in the OpMode - * @see SpecificPipelineHandler - */ -class OpModePipelineHandler(val inputSourceManager: InputSourceManager, private val viewport: OpenCvViewport) : SpecificPipelineHandler( - { it is OpMode } -) { - - private val onStop = EventHandler("OpModePipelineHandler-onStop") - - /** - * Set up for the OpMode lifecycle - * It sets the input source and registers the VisionInputSourceHander - * to let the OpMode request and find input sources from the simulator - */ - override fun preInit() { - if(pipeline == null) return - - inputSourceManager.setInputSource(null) - ThreadSourceHander.register(VisionInputSourceHander(pipeline?.notifier ?: return, viewport)) - - pipeline?.telemetry = telemetry - pipeline?.hardwareMap = HardwareMap() } - - override fun init() { } - - override fun processFrame(currentInputSource: InputSource?) { - } - - /** - * Stops the OpMode and the pipeline if an exception is thrown - * It also calls the onStop event to notify any listeners - * that the OpMode has stopped - */ - override fun onException() { - if(pipeline != null) { - pipeline!!.forceStop() - onStop.run() - } - } - - /** - * Stops the OpMode and the pipeline when the pipeline is changed - * It also calls the onStop event to notify any listeners - * that the OpMode has stopped - */ - override fun onChange(beforePipeline: OpenCvPipeline?, newPipeline: OpenCvPipeline, telemetry: Telemetry) { - if(beforePipeline is OpMode) { - beforePipeline.forceStop() - onStop.run() - } - - super.onChange(beforePipeline, newPipeline, telemetry) - } - -} - -/** - * Extension function to get the OpModeType of a class from its annotations - * @return the OpModeType of the class - */ -val Class<*>.opModeType get() = when { - this.autonomousAnnotation != null -> OpModeType.AUTONOMOUS - this.teleopAnnotation != null -> OpModeType.TELEOP - else -> null -} - -/** - * Extension function to get the OpModeType of an OpMode from its annotations - * @return the OpModeType of the OpMode - */ -val OpMode.opModeType get() = this.javaClass.opModeType - -/** - * Extension function to get the Autonomous annotation of a class - * @return the Autonomous annotation of the class - */ -val Class<*>.autonomousAnnotation get() = this.getAnnotation(Autonomous::class.java) - -/** - * Extension function to get the TeleOp annotation of a class - * @return the TeleOp annotation of the class - */ -val Class<*>.teleopAnnotation get() = this.getAnnotation(TeleOp::class.java) - -/** - * Extension function to get the Autonomous annotation of an OpMode - */ -val OpMode.autonomousAnnotation get() = this.javaClass.autonomousAnnotation -/** - * Extension function to get the TeleOp annotation of an OpMode - */ +package com.qualcomm.robotcore.eventloop.opmode + +import com.github.serivesmejia.eocvsim.EOCVSim +import com.github.serivesmejia.eocvsim.input.InputSource +import com.github.serivesmejia.eocvsim.input.InputSourceManager +import com.github.serivesmejia.eocvsim.pipeline.handler.SpecificPipelineHandler +import com.github.serivesmejia.eocvsim.util.event.EventHandler +import com.qualcomm.robotcore.hardware.HardwareMap +import io.github.deltacv.eocvsim.input.VisionInputSourceHander +import io.github.deltacv.vision.external.source.ThreadSourceHander +import org.firstinspires.ftc.robotcore.external.Telemetry +import org.openftc.easyopencv.OpenCvPipeline +import org.openftc.easyopencv.OpenCvViewport + +/** + * Enum class to classify the type of an OpMode + */ +enum class OpModeType { AUTONOMOUS, TELEOP } + +/** + * Handler for OpModes in the pipeline manager + * It helps to manage the lifecycle of OpModes + * and to set the input source and viewport + * within it in order to transform a pipeline + * lifecycle into an OpMode lifecycle + * @param inputSourceManager the input source manager to set the input source + * @param viewport the viewport to set in the OpMode + * @see SpecificPipelineHandler + */ +class OpModePipelineHandler( + val inputSourceManager: InputSourceManager, + private val viewport: OpenCvViewport +) : SpecificPipelineHandler( + { it is OpMode } +) { + + private val onStop = EventHandler("OpModePipelineHandler-onStop") + + /** + * Set up for the OpMode lifecycle + * It sets the input source and registers the VisionInputSourceHander + * to let the OpMode request and find input sources from the simulator + */ + override fun preInit() { + if(pipeline == null) return + + inputSourceManager.setInputSource(null) + ThreadSourceHander.register(VisionInputSourceHander(pipeline?.notifier ?: return, viewport, inputSourceManager)) + + pipeline?.telemetry = telemetry + pipeline?.hardwareMap = HardwareMap() } + + override fun init() { } + + override fun processFrame(currentInputSource: InputSource?) { + } + + /** + * Stops the OpMode and the pipeline if an exception is thrown + * It also calls the onStop event to notify any listeners + * that the OpMode has stopped + */ + override fun onException() { + if(pipeline != null) { + pipeline!!.forceStop() + onStop.run() + } + } + + /** + * Stops the OpMode and the pipeline when the pipeline is changed + * It also calls the onStop event to notify any listeners + * that the OpMode has stopped + */ + override fun onChange(beforePipeline: OpenCvPipeline?, newPipeline: OpenCvPipeline, telemetry: Telemetry) { + if(beforePipeline is OpMode) { + beforePipeline.forceStop() + onStop.run() + } + + super.onChange(beforePipeline, newPipeline, telemetry) + } + +} + +/** + * Extension function to get the OpModeType of a class from its annotations + * @return the OpModeType of the class + */ +val Class<*>.opModeType get() = when { + this.autonomousAnnotation != null -> OpModeType.AUTONOMOUS + this.teleopAnnotation != null -> OpModeType.TELEOP + else -> null +} + +/** + * Extension function to get the OpModeType of an OpMode from its annotations + * @return the OpModeType of the OpMode + */ +val OpMode.opModeType get() = this.javaClass.opModeType + +/** + * Extension function to get the Autonomous annotation of a class + * @return the Autonomous annotation of the class + */ +val Class<*>.autonomousAnnotation get() = this.getAnnotation(Autonomous::class.java) + +/** + * Extension function to get the TeleOp annotation of a class + * @return the TeleOp annotation of the class + */ +val Class<*>.teleopAnnotation get() = this.getAnnotation(TeleOp::class.java) + +/** + * Extension function to get the Autonomous annotation of an OpMode + */ +val OpMode.autonomousAnnotation get() = this.javaClass.autonomousAnnotation +/** + * Extension function to get the TeleOp annotation of an OpMode + */ val OpMode.teleopAnnotation get() = this.javaClass.teleopAnnotation \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/gui/dialog/SuperAccessRequest.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/gui/dialog/SuperAccessRequest.kt index 6b8d8074..1b5b083e 100644 --- a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/gui/dialog/SuperAccessRequest.kt +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/gui/dialog/SuperAccessRequest.kt @@ -1,165 +1,185 @@ -/* - * Copyright (c) 2024 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package io.github.deltacv.eocvsim.gui.dialog - -import com.formdev.flatlaf.FlatLightLaf -import com.formdev.flatlaf.intellijthemes.FlatArcDarkIJTheme -import com.formdev.flatlaf.intellijthemes.FlatArcOrangeIJTheme -import com.formdev.flatlaf.intellijthemes.FlatCobalt2IJTheme -import com.github.serivesmejia.eocvsim.gui.EOCVSimIconLibrary -import com.github.serivesmejia.eocvsim.gui.Icons -import com.github.serivesmejia.eocvsim.gui.Icons.getImage -import java.awt.BorderLayout -import java.awt.Dimension -import java.awt.Font -import java.awt.event.WindowEvent -import java.awt.event.WindowListener -import javax.swing.* -import kotlin.system.exitProcess - -class SuperAccessRequest(sourceName: String, reason: String) { - - init { - FlatArcDarkIJTheme.setup() - - val panel = JPanel(BorderLayout()) - val frame = JDialog() - - frame.title = "EOCV-Sim SuperAccess Request" - - // Create UI components - val titleLabel = JLabel("SuperAccess request from $sourceName", JLabel.CENTER) - - // Set the font of the JLabel to bold - titleLabel.font = titleLabel.font.deriveFont(Font.BOLD, 21f) - - // Text Area for Reason Input - val reasonTextArea = JTextPane().apply { - isEditable = false - contentType = "text/html" - - font = font.deriveFont(18f) - - var formattedReason = "" - - for(line in reason.split("
")) { - formattedReason += wrapText(line, 80) + "
" - } - - text = formattedReason - } - val scrollPane = JScrollPane(reasonTextArea) - - // Create a panel for buttons - val buttonPanel = JPanel().apply { - layout = BoxLayout(this, BoxLayout.X_AXIS) - val acceptButton = JButton("Accept").apply { - preferredSize = Dimension(100, 30) - addActionListener { - exitProcess(171) - } - } - - val rejectButton = JButton("Reject").apply { - preferredSize = Dimension(100, 30) - addActionListener { - exitProcess(-172) - } - } - - add(Box.createHorizontalGlue()) // Push buttons to the center - add(acceptButton) - add(Box.createRigidArea(Dimension(10, 0))) // Space between buttons - add(rejectButton) - add(Box.createHorizontalGlue()) // Push buttons to the center - } - - // Add components to the panel - panel.add(titleLabel, BorderLayout.NORTH) - panel.add(scrollPane, BorderLayout.CENTER) - panel.add(buttonPanel, BorderLayout.SOUTH) - - // Setup the frame - frame.contentPane = panel - frame.isAlwaysOnTop = true - frame.defaultCloseOperation = JDialog.HIDE_ON_CLOSE - frame.isResizable = false - - frame.addWindowListener(object: WindowListener { - override fun windowOpened(e: WindowEvent?) {} - override fun windowClosing(e: WindowEvent?) {} - override fun windowIconified(e: WindowEvent?) {} - override fun windowDeiconified(e: WindowEvent?) {} - override fun windowActivated(e: WindowEvent?) {} - override fun windowDeactivated(e: WindowEvent?) {} - - override fun windowClosed(e: WindowEvent?) { - exitProcess(-129) - } - }) - - frame.setIconImage(EOCVSimIconLibrary.icoEOCVSim.image) - - frame.pack() - - frame.setLocationRelativeTo(null) // Center the window on the screen - frame.isVisible = true - } - -} - -private fun wrapText(text: String, maxLineLength: Int): String { - val words = text.split(" ") - val wrappedLines = mutableListOf() - var currentLine = StringBuilder() - val breakTag = "
" // HTML break tag - - for (word in words) { - // Calculate the length of the line with a break tag - val lineWithWord = if (currentLine.isEmpty()) { - word - } else { - "$currentLine $word" - } - val lineLengthWithBreak = lineWithWord.length + (if (currentLine.isEmpty()) 0 else breakTag.length) - - // Check if adding the next word exceeds the maxLineLength - if (lineLengthWithBreak > maxLineLength) { - // If it does, add the current line to the list and start a new line - wrappedLines.add(currentLine.toString()) - currentLine = StringBuilder(word) - } else { - // Append the word to the current line - currentLine.append(" ").append(word) - } - } - - // Add the last line if it's not empty - if (currentLine.isNotEmpty()) { - wrappedLines.add(currentLine.toString()) - } - - // Join all lines with
for HTML - return wrappedLines.joinToString(breakTag) +/* + * Copyright (c) 2024 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package io.github.deltacv.eocvsim.gui.dialog + +import com.formdev.flatlaf.FlatLightLaf +import com.formdev.flatlaf.intellijthemes.FlatArcDarkIJTheme +import com.formdev.flatlaf.intellijthemes.FlatArcOrangeIJTheme +import com.formdev.flatlaf.intellijthemes.FlatCobalt2IJTheme +import com.github.serivesmejia.eocvsim.gui.EOCVSimIconLibrary +import com.github.serivesmejia.eocvsim.gui.Icons +import com.github.serivesmejia.eocvsim.gui.Icons.getImage +import java.awt.BorderLayout +import java.awt.Dimension +import java.awt.Font +import java.awt.event.WindowEvent +import java.awt.event.WindowListener +import javax.swing.* + +class SuperAccessRequest(sourceName: String, reason: String, val untrusted: Boolean, val callback: (Boolean) -> Unit) { + + init { + FlatArcDarkIJTheme.setup() + + val panel = JPanel(BorderLayout()) + val frame = JDialog() + + frame.title = "EOCV-Sim SuperAccess Request" + + // Create UI components + val titleLabel = JLabel("SuperAccess request from $sourceName", JLabel.CENTER) + + // Set the font of the JLabel to bold + titleLabel.font = titleLabel.font.deriveFont(Font.BOLD, 21f) + + // Text Area for Reason Input + val reasonTextArea = JTextPane().apply { + isEditable = false + contentType = "text/html" + + font = font.deriveFont(18f) + + var formattedReason = "" + + for(line in reason.split("
")) { + formattedReason += wrapText(line, 80) + "
" + } + + text = formattedReason + } + val scrollPane = JScrollPane(reasonTextArea) + + // Create a panel for buttons + val buttonPanel = JPanel().apply { + layout = BoxLayout(this, BoxLayout.X_AXIS) + val acceptButton = JButton("Accept").apply { + preferredSize = Dimension(100, 30) + addActionListener { + if(untrusted) { + JOptionPane.showConfirmDialog( + frame, + "We were unable to verify the integrity and origin of the plugin. Are you sure you want to accept this request? Do so at your own risk.", + "Confirm", + JOptionPane.YES_NO_OPTION, + JOptionPane.QUESTION_MESSAGE + ).let { + if(it == JOptionPane.YES_OPTION) { + callback(true) + } else { + callback(false) + } + } + } else { + callback(true) + } + + frame.isVisible = false + frame.dispose() + } + } + + val rejectButton = JButton("Reject").apply { + preferredSize = Dimension(100, 30) + addActionListener { + callback(false) + + frame.isVisible = false + frame.dispose() + } + } + + add(Box.createHorizontalGlue()) // Push buttons to the center + add(acceptButton) + add(Box.createRigidArea(Dimension(10, 0))) // Space between buttons + add(rejectButton) + add(Box.createHorizontalGlue()) // Push buttons to the center + } + + // Add components to the panel + panel.add(titleLabel, BorderLayout.NORTH) + panel.add(scrollPane, BorderLayout.CENTER) + panel.add(buttonPanel, BorderLayout.SOUTH) + + // Setup the frame + frame.contentPane = panel + frame.isAlwaysOnTop = true + frame.defaultCloseOperation = JDialog.HIDE_ON_CLOSE + frame.isResizable = false + + frame.addWindowListener(object: WindowListener { + override fun windowOpened(e: WindowEvent?) {} + override fun windowClosing(e: WindowEvent?) {} + override fun windowIconified(e: WindowEvent?) {} + override fun windowDeiconified(e: WindowEvent?) {} + override fun windowActivated(e: WindowEvent?) {} + override fun windowDeactivated(e: WindowEvent?) {} + + override fun windowClosed(e: WindowEvent?) { + } + }) + + frame.setIconImage(EOCVSimIconLibrary.icoEOCVSim.image) + + frame.pack() + + frame.setLocationRelativeTo(null) // Center the window on the screen + frame.isVisible = true + } + +} + +private fun wrapText(text: String, maxLineLength: Int): String { + val words = text.split(" ") + val wrappedLines = mutableListOf() + var currentLine = StringBuilder() + val breakTag = "
" // HTML break tag + + for (word in words) { + // Calculate the length of the line with a break tag + val lineWithWord = if (currentLine.isEmpty()) { + word + } else { + "$currentLine $word" + } + val lineLengthWithBreak = lineWithWord.length + (if (currentLine.isEmpty()) 0 else breakTag.length) + + // Check if adding the next word exceeds the maxLineLength + if (lineLengthWithBreak > maxLineLength) { + // If it does, add the current line to the list and start a new line + wrappedLines.add(currentLine.toString()) + currentLine = StringBuilder(word) + } else { + // Append the word to the current line + currentLine.append(" ").append(word) + } + } + + // Add the last line if it's not empty + if (currentLine.isNotEmpty()) { + wrappedLines.add(currentLine.toString()) + } + + // Join all lines with
for HTML + return wrappedLines.joinToString(breakTag) } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSource.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSource.kt index 2ef966d2..44e50b8c 100644 --- a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSource.kt +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSource.kt @@ -1,50 +1,57 @@ -package io.github.deltacv.eocvsim.input - -import com.github.serivesmejia.eocvsim.input.InputSource -import com.github.serivesmejia.eocvsim.util.loggerForThis -import io.github.deltacv.vision.external.source.VisionSourceBase -import io.github.deltacv.vision.external.util.ThrowableHandler -import io.github.deltacv.vision.external.util.Timestamped -import org.opencv.core.Mat -import org.opencv.core.Size - -class VisionInputSource( - private val inputSource: InputSource, - throwableHandler: ThrowableHandler? = null -) : VisionSourceBase(throwableHandler) { - - val logger by loggerForThis() - - override fun init(): Int { - return 0 - } - - override fun close(): Boolean { - inputSource.close() - inputSource.reset() - return true - } - - override fun startSource(size: Size?): Boolean { - inputSource.setSize(size) - inputSource.init() - return true - } - - override fun stopSource(): Boolean { - inputSource.close() - return true; - } - - private val emptyMat = Mat() - - override fun pullFrame(): Timestamped { - return try { - val frame = inputSource.update(); - Timestamped(frame, inputSource.captureTimeNanos) - } catch(e: Exception) { - Timestamped(emptyMat, 0) - } - } - +package io.github.deltacv.eocvsim.input + +import com.github.serivesmejia.eocvsim.input.InputSource +import com.github.serivesmejia.eocvsim.input.source.CameraSource +import com.github.serivesmejia.eocvsim.util.loggerForThis +import io.github.deltacv.eocvsim.input.control.CameraSourceControlMap +import io.github.deltacv.vision.external.source.CameraControlMap +import io.github.deltacv.vision.external.source.VisionSourceBase +import io.github.deltacv.vision.external.util.ThrowableHandler +import io.github.deltacv.vision.external.util.Timestamped +import org.opencv.core.Mat +import org.opencv.core.Size + +class VisionInputSource( + private val inputSource: InputSource, + throwableHandler: ThrowableHandler? = null +) : VisionSourceBase(throwableHandler) { + + val logger by loggerForThis() + + override fun init(): Int { + return 0 + } + + override fun getControlMap() = if(inputSource is CameraSource) { + CameraSourceControlMap(inputSource) + } else throw IllegalStateException("Controls are not available for source ${inputSource.name}") + + override fun close(): Boolean { + inputSource.close() + inputSource.reset() + return true + } + + override fun startSource(size: Size?): Boolean { + inputSource.setSize(size) + inputSource.init() + return true + } + + override fun stopSource(): Boolean { + inputSource.close() + return true; + } + + private val emptyMat = Mat() + + override fun pullFrame(): Timestamped { + return try { + val frame = inputSource.update(); + Timestamped(frame, inputSource.captureTimeNanos) + } catch(e: Exception) { + Timestamped(emptyMat, 0) + } + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSourceHander.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSourceHander.kt index d57af4e0..f2682f9c 100644 --- a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSourceHander.kt +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSourceHander.kt @@ -1,68 +1,75 @@ -package io.github.deltacv.eocvsim.input - -import com.github.serivesmejia.eocvsim.input.source.CameraSource -import com.github.serivesmejia.eocvsim.input.source.ImageSource -import com.github.serivesmejia.eocvsim.input.source.VideoSource -import io.github.deltacv.vision.external.source.ViewportAndSourceHander -import io.github.deltacv.vision.external.source.VisionSource -import io.github.deltacv.vision.internal.opmode.OpModeNotifier -import io.github.deltacv.vision.internal.opmode.OpModeState -import io.github.deltacv.vision.internal.opmode.RedirectToOpModeThrowableHandler -import org.opencv.core.Mat -import org.opencv.core.Size -import org.opencv.videoio.VideoCapture -import org.openftc.easyopencv.OpenCvViewport -import java.io.File -import java.io.IOException -import javax.imageio.ImageIO - -class VisionInputSourceHander(val notifier: OpModeNotifier, val viewport: OpenCvViewport) : ViewportAndSourceHander { - - private fun isImage(path: String) = try { - ImageIO.read(File(path)) != null - } catch(ex: IOException) { false } - - private fun isVideo(path: String): Boolean { - val capture = VideoCapture(path) - val mat = Mat() - - capture.read(mat) - - val isVideo = !mat.empty() - - capture.release() - - return isVideo - } - - override fun hand(name: String): VisionSource { - val source = VisionInputSource(if(File(name).exists()) { - if(isImage(name)) { - ImageSource(name) - } else if(isVideo(name)) { - VideoSource(name, null) - } else throw IllegalArgumentException("File is neither an image nor a video") - } else { - val index = name.toIntOrNull() - ?: if(name == "default" || name == "Webcam 1") 0 - else throw IllegalArgumentException("Unknown source $name") - - CameraSource(index, Size(640.0, 480.0)) - }, RedirectToOpModeThrowableHandler(notifier)) - - notifier.onStateChange { - when(notifier.state) { - OpModeState.STOPPED -> { - source.stop() - it.removeThis() - } - else -> {} - } - } - - return source - } - - override fun viewport() = viewport - +package io.github.deltacv.eocvsim.input + +import com.github.serivesmejia.eocvsim.input.InputSourceManager +import com.github.serivesmejia.eocvsim.input.source.CameraSource +import com.github.serivesmejia.eocvsim.input.source.ImageSource +import com.github.serivesmejia.eocvsim.input.source.VideoSource +import io.github.deltacv.vision.external.source.ViewportAndSourceHander +import io.github.deltacv.vision.external.source.VisionSource +import io.github.deltacv.vision.internal.opmode.OpModeNotifier +import io.github.deltacv.vision.internal.opmode.OpModeState +import io.github.deltacv.vision.internal.opmode.RedirectToOpModeThrowableHandler +import org.opencv.core.Mat +import org.opencv.core.Size +import org.opencv.videoio.VideoCapture +import org.openftc.easyopencv.OpenCvViewport +import java.io.File +import java.io.IOException +import javax.imageio.ImageIO + +class VisionInputSourceHander( + val notifier: OpModeNotifier, + val viewport: OpenCvViewport, + val inputSourceManager: InputSourceManager +) : ViewportAndSourceHander { + + private fun isImage(path: String) = try { + ImageIO.read(File(path)) != null + } catch(ex: IOException) { false } + + private fun isVideo(path: String): Boolean { + val capture = VideoCapture(path) + val mat = Mat() + + capture.read(mat) + + val isVideo = !mat.empty() + + capture.release() + + return isVideo + } + + override fun hand(name: String): VisionSource { + val source = VisionInputSource(if(File(name).exists()) { + if(isImage(name)) { + ImageSource(name) + } else if(isVideo(name)) { + VideoSource(name, null) + } else throw IllegalArgumentException("File is neither an image nor a video") + } else { + val index = name.toIntOrNull() + ?: if(name == "default" || name == "Webcam 1") 0 + else null + + if(index == null) { + inputSourceManager.sources.get(name) ?: throw IllegalArgumentException("Input source $name not found") + } else CameraSource(index, Size(640.0, 480.0)) + }, RedirectToOpModeThrowableHandler(notifier)) + + notifier.onStateChange { + when(notifier.state) { + OpModeState.STOPPED -> { + source.stop() + it.removeThis() + } + else -> {} + } + } + + return source + } + + override fun viewport() = viewport + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/control/CameraSourceControlMap.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/control/CameraSourceControlMap.kt new file mode 100644 index 00000000..27148794 --- /dev/null +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/control/CameraSourceControlMap.kt @@ -0,0 +1,19 @@ +package io.github.deltacv.eocvsim.input.control + +import com.github.serivesmejia.eocvsim.input.source.CameraSource +import io.github.deltacv.vision.external.source.CameraControlMap +import org.firstinspires.ftc.robotcore.external.hardware.camera.controls.CameraControl +import org.firstinspires.ftc.robotcore.external.hardware.camera.controls.ExposureControl + +class CameraSourceControlMap( + val source: CameraSource +) : CameraControlMap { + + @Suppress("UNCHECKED_CAST") + override fun get(classType: Class) = + when(classType) { + ExposureControl::class.java -> CameraSourceExposureControl(source) as T + else -> null + } + +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/control/CameraSourceExposureControl.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/control/CameraSourceExposureControl.kt new file mode 100644 index 00000000..91433cf0 --- /dev/null +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/control/CameraSourceExposureControl.kt @@ -0,0 +1,65 @@ +package io.github.deltacv.eocvsim.input.control + +import com.github.serivesmejia.eocvsim.input.source.CameraSource +import io.github.deltacv.steve.WebcamProperty +import org.firstinspires.ftc.robotcore.external.hardware.camera.controls.ExposureControl +import org.firstinspires.ftc.robotcore.internal.collections.MutableReference +import java.util.concurrent.TimeUnit + +class CameraSourceExposureControl( + cameraSource: CameraSource +) : ExposureControl { + + val control = cameraSource.webcamPropertyControl + + override fun getMode() = if(control.getPropertyAuto(WebcamProperty.EXPOSURE)) { + ExposureControl.Mode.Auto + } else ExposureControl.Mode.Manual + + override fun setMode(mode: ExposureControl.Mode): Boolean { + control.setPropertyAuto(WebcamProperty.EXPOSURE, mode == ExposureControl.Mode.Auto) + return control.getPropertyAuto(WebcamProperty.EXPOSURE) + } + + override fun isModeSupported(mode: ExposureControl.Mode) = + mode == ExposureControl.Mode.Auto || mode == ExposureControl.Mode.Manual + + override fun getMinExposure(resultUnit: TimeUnit): Long { + val bounds = control.getPropertyBounds(WebcamProperty.EXPOSURE) + return TimeUnit.SECONDS.convert(bounds.min.toLong(), resultUnit) + } + + override fun getMaxExposure(resultUnit: TimeUnit): Long { + val bounds = control.getPropertyBounds(WebcamProperty.EXPOSURE) + return TimeUnit.SECONDS.convert(bounds.max.toLong(), resultUnit) + } + + override fun getExposure(resultUnit: TimeUnit): Long { + return TimeUnit.SECONDS.convert(control.getProperty(WebcamProperty.EXPOSURE).toLong(), resultUnit) + } + + override fun getCachedExposure( + resultUnit: TimeUnit, + refreshed: MutableReference, + permittedStaleness: Long, + permittedStalenessUnit: TimeUnit + ) = getExposure(resultUnit) + + override fun setExposure(duration: Long, durationUnit: TimeUnit): Boolean { + val seconds = TimeUnit.SECONDS.convert(duration, durationUnit).toInt() + + control.setProperty(WebcamProperty.EXPOSURE, seconds) + return control.getProperty(WebcamProperty.EXPOSURE) == seconds + } + + override fun isExposureSupported() = control.isPropertySupported(WebcamProperty.EXPOSURE) + + override fun getAePriority(): Boolean { + throw UnsupportedOperationException("AE priority is not supported by EOCV-Sim") + } + + override fun setAePriority(priority: Boolean): Boolean { + throw UnsupportedOperationException("AE priority is not supported by EOCV-Sim") + } + +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/EOCVSimPlugin.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/EOCVSimPlugin.kt index 13a9e264..63784a00 100644 --- a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/EOCVSimPlugin.kt +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/EOCVSimPlugin.kt @@ -1,68 +1,83 @@ -/* - * Copyright (c) 2024 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package io.github.deltacv.eocvsim.plugin - -import io.github.deltacv.eocvsim.plugin.loader.PluginContext - -/** - * Represents a plugin for EOCV-Sim - */ -abstract class EOCVSimPlugin { - - /** - * The context of the plugin, containing entry point objects - */ - val context get() = PluginContext.current(this) - - /** - * The EOCV-Sim instance, containing the main functionality of the program - */ - val eocvSim get() = context.eocvSim - - /** - * The virtual filesystem assigned to this plugin. - * With this filesystem, the plugin can access files in a sandboxed environment. - * Without SuperAccess, the plugin can only access files in its own virtual fs. - * @see SandboxFileSystem - */ - val fileSystem get() = context.fileSystem - - var enabled = false - internal set - - /** - * Called when the plugin is loaded by the PluginLoader - */ - abstract fun onLoad() - - /** - * Called when the plugin is enabled by the PluginLoader - */ - abstract fun onEnable() - - /* - * Called when the plugin is disabled by the PluginLoader - */ - abstract fun onDisable() +/* + * Copyright (c) 2024 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package io.github.deltacv.eocvsim.plugin + +import io.github.deltacv.eocvsim.plugin.loader.PluginContext + +/** + * Represents a plugin for EOCV-Sim + */ +abstract class EOCVSimPlugin { + + /** + * The context of the plugin, containing entry point objects + */ + val context get() = PluginContext.current(this) + + /** + * The EOCV-Sim instance, containing the main functionality of the program + */ + val eocvSim get() = context.eocvSim + + /** + * The virtual filesystem assigned to this plugin. + * With this filesystem, the plugin can access files in a sandboxed environment. + * Without SuperAccess, the plugin can only access files in its own virtual fs. + * @see SandboxFileSystem + */ + val fileSystem get() = context.fileSystem + + /** + * Whether this plugin comes from a maven repository or a file + * This affects the classpath resolution and the way the plugin is loaded. + * @see io.github.deltacv.eocvsim.plugin.loader.PluginSource + */ + val pluginSource get() = context.pluginSource + + /** + * The classpath of the plugin, additional to the JVM classpath + * This classpath is used to load classes from the plugin jar file + * and other dependencies that come from other jar files. + * @see io.github.deltacv.eocvsim.plugin.loader.PluginClassLoader + */ + val classpath get() = context.classpath + + var enabled = false + internal set + + /** + * Called when the plugin is loaded by the PluginLoader + */ + abstract fun onLoad() + + /** + * Called when the plugin is enabled by the PluginLoader + */ + abstract fun onEnable() + + /* + * Called when the plugin is disabled by the PluginLoader + */ + abstract fun onDisable() } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginClassLoader.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginClassLoader.kt index af498d85..bc557d69 100644 --- a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginClassLoader.kt +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginClassLoader.kt @@ -1,159 +1,272 @@ -/* - * Copyright (c) 2024 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package io.github.deltacv.eocvsim.plugin.loader - -import com.github.serivesmejia.eocvsim.util.SysUtil -import com.github.serivesmejia.eocvsim.util.extension.removeFromEnd -import io.github.deltacv.eocvsim.sandbox.restrictions.MethodCallByteCodeChecker -import io.github.deltacv.eocvsim.sandbox.restrictions.dynamicLoadingMethodBlacklist -import io.github.deltacv.eocvsim.sandbox.restrictions.dynamicLoadingPackageBlacklist -import io.github.deltacv.eocvsim.sandbox.restrictions.dynamicLoadingPackageWhitelist -import java.io.ByteArrayOutputStream -import java.io.File -import java.io.IOException -import java.io.InputStream -import java.net.URL -import java.nio.file.FileSystems -import java.util.zip.ZipEntry -import java.util.zip.ZipFile - -/** - * ClassLoader for loading classes from a plugin jar file - * @param pluginJar the jar file of the plugin - * @param pluginContext the plugin context - */ -class PluginClassLoader(private val pluginJar: File, val pluginContextProvider: () -> PluginContext) : ClassLoader() { - - private val zipFile = try { - ZipFile(pluginJar) - } catch(e: Exception) { - throw IOException("Failed to open plugin JAR file", e) - } - - private val loadedClasses = mutableMapOf>() - - init { - FileSystems.getDefault() - } - - private fun loadClass(entry: ZipEntry): Class<*> { - val name = entry.name.removeFromEnd(".class").replace('/', '.') - - zipFile.getInputStream(entry).use { inStream -> - ByteArrayOutputStream().use { outStream -> - SysUtil.copyStream(inStream, outStream) - val bytes = outStream.toByteArray() - - if(!pluginContextProvider().hasSuperAccess) - MethodCallByteCodeChecker(bytes, dynamicLoadingMethodBlacklist) - - val clazz = defineClass(name, bytes, 0, bytes.size) - loadedClasses[name] = clazz - - return clazz - } - } - } - - override fun findClass(name: String) = loadedClasses[name] ?: loadClass(name, false) - - /** - * Load a class from the plugin jar file - * @param name the name of the class to load - * @return the loaded class - * @throws IllegalAccessError if the class is blacklisted - */ - fun loadClassStrict(name: String): Class<*> { - if(!pluginContextProvider().hasSuperAccess) { - for (blacklistedPackage in dynamicLoadingPackageBlacklist) { - if (name.contains(blacklistedPackage)) { - throw IllegalAccessError("Plugins are blacklisted to use $name") - } - } - } - - return loadClass(zipFile.getEntry(name.replace('.', '/') + ".class") ?: throw ClassNotFoundException(name)) - } - - override fun loadClass(name: String, resolve: Boolean): Class<*> { - var clazz = loadedClasses[name] - - if(clazz == null) { - var inWhitelist = false - - for(whiteListedPackage in dynamicLoadingPackageWhitelist) { - if(name.contains(whiteListedPackage)) { - inWhitelist = true - break - } - } - - if(!inWhitelist && !pluginContextProvider().hasSuperAccess) { - throw IllegalAccessError("Plugins are not whitelisted to use $name") - } - - clazz = try { - Class.forName(name) - } catch (e: ClassNotFoundException) { - loadClassStrict(name) - } - - if(resolve) resolveClass(clazz) - } - - return clazz!! - } - - override fun getResourceAsStream(name: String): InputStream? { - val entry = zipFile.getEntry(name) - - if(entry != null) { - try { - return zipFile.getInputStream(entry) - } catch (e: IOException) { } - } - - return super.getResourceAsStream(name) - } - - override fun getResource(name: String): URL? { - // Try to find the resource inside the plugin JAR - val entry = zipFile.getEntry(name) - - if (entry != null) { - try { - // Construct a URL for the resource inside the plugin JAR - return URL("jar:file:${pluginJar.absolutePath}!/$name") - } catch (e: Exception) { - e.printStackTrace() - } - } - - // Fallback to the parent classloader if not found in the plugin JAR - return super.getResource(name) - } - - override fun toString() = "PluginClassLoader@\"${pluginJar.name}\"" - +/* + * Copyright (c) 2024 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package io.github.deltacv.eocvsim.plugin.loader + +import com.github.serivesmejia.eocvsim.util.SysUtil +import com.github.serivesmejia.eocvsim.util.extension.removeFromEnd +import io.github.deltacv.eocvsim.sandbox.restrictions.MethodCallByteCodeChecker +import io.github.deltacv.eocvsim.sandbox.restrictions.dynamicLoadingExactMatchBlacklist +import io.github.deltacv.eocvsim.sandbox.restrictions.dynamicLoadingMethodBlacklist +import io.github.deltacv.eocvsim.sandbox.restrictions.dynamicLoadingPackageBlacklist +import io.github.deltacv.eocvsim.sandbox.restrictions.dynamicLoadingPackageWhitelist +import java.io.ByteArrayOutputStream +import java.lang.ref.WeakReference +import java.io.File +import java.io.IOException +import java.io.InputStream +import java.net.URL +import java.util.zip.ZipEntry +import java.util.zip.ZipFile + +/** + * ClassLoader for loading classes from a plugin jar file + * @param pluginJar the jar file of the plugin + * @param classpath additional classpath that the plugin may require + * @param pluginContext the plugin context + */ +class PluginClassLoader( + private val pluginJar: File, + val classpath: List, + val pluginContextProvider: () -> PluginContext +) : ClassLoader() { + + private var additionalZipFiles = mutableListOf>() + + private val zipFile = try { + ZipFile(pluginJar) + } catch (e: Exception) { + throw IOException("Failed to open plugin JAR file", e) + } + + private val loadedClasses = mutableMapOf>() + + private fun loadClass(entry: ZipEntry, zipFile: ZipFile = this.zipFile): Class<*> { + val name = entry.name.removeFromEnd(".class").replace('/', '.') + + zipFile.getInputStream(entry).use { inStream -> + ByteArrayOutputStream().use { outStream -> + SysUtil.copyStream(inStream, outStream) + val bytes = outStream.toByteArray() + + if (!pluginContextProvider().hasSuperAccess) + MethodCallByteCodeChecker(bytes, dynamicLoadingMethodBlacklist) + + val clazz = defineClass(name, bytes, 0, bytes.size) + loadedClasses[name] = clazz + + return clazz + } + } + } + + override fun findClass(name: String) = loadedClasses[name] ?: loadClass(name, false) + + /** + * Load a class from the plugin jar file + * @param name the name of the class to load + * @return the loaded class + */ + fun loadClassStrict(name: String): Class<*> { + return loadClass(zipFile.getEntry(name.replace('.', '/') + ".class") ?: throw ClassNotFoundException(name)) + } + + override fun loadClass(name: String, resolve: Boolean): Class<*> { + var clazz = loadedClasses[name] + + try { + if (clazz == null) { + var canForName = true + + if (!pluginContextProvider().hasSuperAccess) { + var inWhitelist = false + + for (whiteListedPackage in dynamicLoadingPackageWhitelist) { + if (name.contains(whiteListedPackage)) { + inWhitelist = true + break + } + } + + if (!inWhitelist && !pluginContextProvider().hasSuperAccess) { + canForName = false + throw IllegalAccessError("Plugins are not whitelisted to use $name") + } + + blacklistLoop@ + for (blacklistedPackage in dynamicLoadingPackageBlacklist) { + // If the class is whitelisted, skip the blacklist check + if (inWhitelist) continue@blacklistLoop + + if (name.contains(blacklistedPackage)) { + canForName = false + throw IllegalAccessError("Plugins are blacklisted to use $name") + } + } + + for (blacklistedClass in dynamicLoadingExactMatchBlacklist) { + if (name == blacklistedClass) { + canForName = false + throw IllegalAccessError("Plugins are blacklisted to use $name") + } + } + } + + if (canForName) { + clazz = Class.forName(name) + } + } + } catch(e: Throwable) { + try { + clazz = loadClassStrict(name) + } catch(_: Throwable) { + val classpathClass = classFromClasspath(name) + + if(classpathClass != null) { + clazz = classpathClass + } else { + throw e + } + } + } + + if (resolve) resolveClass(clazz) + return clazz!! + } + + override fun getResourceAsStream(name: String): InputStream? { + val entry = zipFile.getEntry(name) + + if (entry != null) { + try { + return zipFile.getInputStream(entry) + } catch (_: IOException) { + } + } else { + // Try to find the resource inside the classpath + resourceAsStreamFromClasspath(name)?.let { return it } + } + + return super.getResourceAsStream(name) + } + + override fun getResource(name: String): URL? { + // Try to find the resource inside the plugin JAR + val entry = zipFile.getEntry(name) + + if (entry != null) { + // Construct a URL for the resource inside the plugin JAR + return URL("jar:file:${pluginJar.absolutePath}!/$name") + } else { + resourceFromClasspath(name)?.let { return it } + } + + // Fallback to the parent classloader if not found in the plugin JAR + return super.getResource(name) + } + + /** + * Get a resource from the classpath specified in the constructor + */ + fun resourceAsStreamFromClasspath(name: String): InputStream? { + for (file in classpath) { + if (file == pluginJar) continue + + val zipFile = ZipFile(file) + + val entry = zipFile.getEntry(name) + + if (entry != null) { + try { + additionalZipFiles.add(WeakReference(zipFile)) + return zipFile.getInputStream(entry) + } catch (e: Exception) { + zipFile.close() + } + } else { + zipFile.close() + } + } + + return null + } + + /** + * Get a resource from the classpath specified in the constructor + */ + fun resourceFromClasspath(name: String): URL? { + for (file in classpath) { + if (file == pluginJar) continue + + val zipFile = ZipFile(file) + + try { + val entry = zipFile.getEntry(name) + + if (entry != null) { + try { + return URL("jar:file:${file.absolutePath}!/$name") + } catch (e: Exception) { + } + } + } finally { + zipFile.close() + } + } + + return null + } + + /** + * Load a class from the classpath specified in the constructor + */ + fun classFromClasspath(className: String): Class<*>? { + for (file in classpath) { + if (file == pluginJar) continue + + val zipFile = ZipFile(file) + + try { + val entry = zipFile.getEntry(className.replace('.', '/') + ".class") + + if (entry != null) { + return loadClass(entry, zipFile = zipFile) + } + } finally { + zipFile.close() + } + } + + return null + } + + fun close() { + zipFile.close() + for(ref in additionalZipFiles) { + ref.get()?.close() + } + } + + override fun toString() = "PluginClassLoader@\"${pluginJar.name}\"" + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginContext.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginContext.kt index 6fe21731..82c402ef 100644 --- a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginContext.kt +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginContext.kt @@ -1,41 +1,45 @@ -/* - * Copyright (c) 2024 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package io.github.deltacv.eocvsim.plugin.loader - -import com.github.serivesmejia.eocvsim.EOCVSim -import io.github.deltacv.eocvsim.plugin.EOCVSimPlugin -import io.github.deltacv.eocvsim.sandbox.nio.SandboxFileSystem - -class PluginContext( - val eocvSim: EOCVSim, val fileSystem: SandboxFileSystem, val loader: PluginLoader -) { - companion object { - @JvmStatic fun current(plugin: EOCVSimPlugin) = (plugin.javaClass.classLoader as PluginClassLoader).pluginContextProvider() - } - - val plugin get() = loader.plugin - - val hasSuperAccess get() = loader.hasSuperAccess - fun requestSuperAccess(reason: String) = loader.requestSuperAccess(reason) +/* + * Copyright (c) 2024 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package io.github.deltacv.eocvsim.plugin.loader + +import com.github.serivesmejia.eocvsim.EOCVSim +import io.github.deltacv.eocvsim.plugin.EOCVSimPlugin +import io.github.deltacv.eocvsim.sandbox.nio.SandboxFileSystem + +class PluginContext( + val eocvSim: EOCVSim, + val fileSystem: SandboxFileSystem, + val loader: PluginLoader +) { + companion object { + @JvmStatic fun current(plugin: EOCVSimPlugin) = (plugin.javaClass.classLoader as PluginClassLoader).pluginContextProvider() + } + + val plugin get() = loader.plugin + val pluginSource get() = loader.pluginSource + val classpath get() = loader.classpath + + val hasSuperAccess get() = loader.hasSuperAccess + fun requestSuperAccess(reason: String) = loader.requestSuperAccess(reason) } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginLoader.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginLoader.kt index c47ebafe..2ae9631a 100644 --- a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginLoader.kt +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginLoader.kt @@ -1,215 +1,301 @@ -/* - * Copyright (c) 2024 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package io.github.deltacv.eocvsim.plugin.loader - -import com.github.serivesmejia.eocvsim.EOCVSim -import com.github.serivesmejia.eocvsim.config.ConfigLoader -import com.github.serivesmejia.eocvsim.util.SysUtil -import com.github.serivesmejia.eocvsim.util.event.EventHandler -import com.github.serivesmejia.eocvsim.util.extension.plus -import com.github.serivesmejia.eocvsim.util.loggerForThis -import com.moandjiezana.toml.Toml -import io.github.deltacv.common.util.ParsedVersion -import io.github.deltacv.eocvsim.plugin.EOCVSimPlugin -import io.github.deltacv.eocvsim.sandbox.nio.SandboxFileSystem -import net.lingala.zip4j.ZipFile -import java.io.File -import java.security.MessageDigest - -/** - * Loads a plugin from a jar file - * @param pluginFile the jar file of the plugin - * @param eocvSim the EOCV-Sim instance - */ -class PluginLoader(val pluginFile: File, val eocvSim: EOCVSim) { - - val logger by loggerForThis() - - var loaded = false - private set - - var enabled = false - private set - - val pluginClassLoader: PluginClassLoader - - lateinit var pluginToml: Toml - private set - - lateinit var pluginName: String - private set - lateinit var pluginVersion: String - private set - - lateinit var pluginAuthor: String - private set - lateinit var pluginAuthorEmail: String - private set - - lateinit var pluginClass: Class<*> - private set - lateinit var plugin: EOCVSimPlugin - private set - - /** - * The file system for the plugin - */ - lateinit var fileSystem: SandboxFileSystem - private set - - val fileSystemZip by lazy { PluginManager.FILESYSTEMS_FOLDER + File.separator + "${hash()}-fs" } - val fileSystemZipPath by lazy { fileSystemZip.toPath() } - - /** - * Whether the plugin has super access (full system access) - */ - val hasSuperAccess get() = eocvSim.config.superAccessPluginHashes.contains(pluginHash) - - init { - pluginClassLoader = PluginClassLoader(pluginFile) { - PluginContext(eocvSim, fileSystem, this) - } - } - - /** - * Load the plugin from the jar file - * @throws InvalidPluginException if the plugin.toml file is not found - * @throws UnsupportedPluginException if the plugin requests an api version higher than the current one - */ - fun load() { - if(loaded) return - - pluginToml = Toml().read(pluginClassLoader.getResourceAsStream("plugin.toml") - ?: throw InvalidPluginException("No plugin.toml in the jar file") - ) - - pluginName = pluginToml.getString("name") ?: throw InvalidPluginException("No name in plugin.toml") - pluginVersion = pluginToml.getString("version") ?: throw InvalidPluginException("No version in plugin.toml") - - pluginAuthor = pluginToml.getString("author") ?: throw InvalidPluginException("No author in plugin.toml") - pluginAuthorEmail = pluginToml.getString("author-email", "") - - logger.info("Loading plugin $pluginName v$pluginVersion by $pluginAuthor") - - setupFs() - - if(pluginToml.contains("api-version")) { - val parsedVersion = ParsedVersion(pluginToml.getString("api-version")) - - if(parsedVersion > EOCVSim.PARSED_VERSION) - throw UnsupportedPluginException("Plugin request api version of v${parsedVersion}, EOCV-Sim is currently running at v${EOCVSim.PARSED_VERSION}") - } - - if(pluginToml.getBoolean("super-access", false)) { - requestSuperAccess(pluginToml.getString("super-access-reason", "")) - } - - pluginClass = pluginClassLoader.loadClassStrict(pluginToml.getString("main")) - plugin = pluginClass.getConstructor().newInstance() as EOCVSimPlugin - - plugin.onLoad() - - loaded = true - } - - private fun setupFs() { - if(!fileSystemZip.exists()) { - val zip = ZipFile(fileSystemZip) // kinda wack but uh, yeah... - zip.addFile(ConfigLoader.CONFIG_SAVEFILE) - zip.removeFile(ConfigLoader.CONFIG_SAVEFILE.name) - zip.close() - } - - fileSystem = SandboxFileSystem(this) - } - - /** - * Enable the plugin - */ - fun enable() { - if(enabled || !loaded) return - - logger.info("Enabling plugin $pluginName v$pluginVersion") - - plugin.enabled = true - plugin.onEnable() - - enabled = true - } - - /** - * Disable the plugin - */ - fun disable() { - if(!enabled || !loaded) return - - logger.info("Disabling plugin $pluginName v$pluginVersion") - - plugin.enabled = false - plugin.onDisable() - - kill() - } - - /** - * Kill the plugin - * This will close the file system and ban the class loader - * @see EventHandler.banClassLoader - */ - fun kill() { - fileSystem.close() - enabled = false - EventHandler.banClassLoader(pluginClassLoader) - } - - /** - * Request super access for the plugin - * @param reason the reason for requesting super access - */ - fun requestSuperAccess(reason: String): Boolean { - if(hasSuperAccess) return true - return eocvSim.pluginManager.requestSuperAccessFor(this, reason) - } - - /** - * Get the hash of the plugin file based off the plugin name and author - * @return the hash - */ - fun hash(): String { - val messageDigest = MessageDigest.getInstance("SHA-256") - messageDigest.update("${pluginName} by ${pluginAuthor}".toByteArray()) - return SysUtil.byteArray2Hex(messageDigest.digest()) - } - - /** - * Get the hash of the plugin file based off the file contents - * @return the hash - */ - val pluginHash by lazy { - val messageDigest = MessageDigest.getInstance("SHA-256") - messageDigest.update(pluginFile.readBytes()) - SysUtil.byteArray2Hex(messageDigest.digest()) - } - +/* + * Copyright (c) 2024 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package io.github.deltacv.eocvsim.plugin.loader + +import com.github.serivesmejia.eocvsim.EOCVSim +import com.github.serivesmejia.eocvsim.config.ConfigLoader +import com.github.serivesmejia.eocvsim.gui.dialog.AppendDelegate +import com.github.serivesmejia.eocvsim.gui.dialog.PluginOutput +import com.github.serivesmejia.eocvsim.util.SysUtil +import com.github.serivesmejia.eocvsim.util.event.EventHandler +import com.github.serivesmejia.eocvsim.util.extension.fileHash +import com.github.serivesmejia.eocvsim.util.extension.hashString +import com.github.serivesmejia.eocvsim.util.extension.plus +import com.github.serivesmejia.eocvsim.util.loggerForThis +import com.moandjiezana.toml.Toml +import io.github.deltacv.common.util.ParsedVersion +import io.github.deltacv.eocvsim.plugin.EOCVSimPlugin +import io.github.deltacv.eocvsim.plugin.security.PluginSignature +import io.github.deltacv.eocvsim.plugin.security.PluginSignatureVerifier +import io.github.deltacv.eocvsim.sandbox.nio.SandboxFileSystem +import net.lingala.zip4j.ZipFile +import java.io.File +import java.security.MessageDigest + +enum class PluginSource { + REPOSITORY, + FILE +} + +class PluginParser(pluginToml: Toml) { + val pluginName = pluginToml.getString("name")?.trim() ?: throw InvalidPluginException("No name in plugin.toml") + val pluginVersion = pluginToml.getString("version")?.trim() ?: throw InvalidPluginException("No version in plugin.toml") + + val pluginAuthor = pluginToml.getString("author")?.trim() ?: throw InvalidPluginException("No author in plugin.toml") + val pluginAuthorEmail = pluginToml.getString("author-email", "")?.trim() + + val pluginMain = pluginToml.getString("main")?.trim() ?: throw InvalidPluginException("No main in plugin.toml") + + val pluginDescription = pluginToml.getString("description", "")?.trim() + + /** + * Get the hash of the plugin based off the plugin name and author + * @return the hash + */ + fun hash() = "${pluginName}${PluginOutput.SPECIAL}${pluginAuthor}".hashString +} + +/** + * Loads a plugin from a jar file + * @param pluginFile the jar file of the plugin + * @param eocvSim the EOCV-Sim instance + */ +class PluginLoader( + val pluginFile: File, + val classpath: List, + val pluginSource: PluginSource, + val eocvSim: EOCVSim, + val appender: AppendDelegate +) { + + val logger by loggerForThis() + + var loaded = false + private set + + var enabled = false + private set + + val pluginClassLoader: PluginClassLoader + + var shouldEnable: Boolean + get() { + return eocvSim.config.flags.getOrDefault(hash(), true) + } + set(value) { + eocvSim.config.flags[hash()] = value + eocvSim.configManager.saveToFile() + } + + lateinit var pluginToml: Toml + private set + + lateinit var pluginName: String + private set + lateinit var pluginVersion: String + private set + + lateinit var pluginDescription: String + private set + + lateinit var pluginAuthor: String + private set + lateinit var pluginAuthorEmail: String + private set + + lateinit var pluginClass: Class<*> + private set + lateinit var plugin: EOCVSimPlugin + private set + + /** + * The file system for the plugin + */ + lateinit var fileSystem: SandboxFileSystem + private set + + /** + * The signature of the plugin, issued by a verified authority + */ + val signature by lazy { PluginSignatureVerifier.verify(pluginFile) } + + val fileSystemZip by lazy { PluginManager.FILESYSTEMS_FOLDER + File.separator + "${hash()}-fs" } + val fileSystemZipPath by lazy { fileSystemZip.toPath() } + + /** + * Whether the plugin has super access (full system access) + */ + val hasSuperAccess get() = eocvSim.pluginManager.superAccessDaemonClient.checkAccess(pluginFile) + + init { + pluginClassLoader = PluginClassLoader( + pluginFile, + classpath + ) { + PluginContext(eocvSim, fileSystem, this) + } + } + + /** + * Fetch the plugin info from the plugin.toml file + * Fills the pluginName, pluginVersion, pluginAuthor and pluginAuthorEmail fields + */ + fun fetchInfoFromToml() { + if(::pluginToml.isInitialized) return + + pluginToml = Toml().read(pluginClassLoader.getResourceAsStream("plugin.toml") + ?: throw InvalidPluginException("No plugin.toml in the jar file") + ) + + val parser = PluginParser(pluginToml) + + pluginName = parser.pluginName + pluginVersion = parser.pluginVersion + pluginAuthor = parser.pluginAuthor + pluginAuthorEmail = parser.pluginAuthorEmail ?: "" + pluginDescription = parser.pluginDescription ?: "" + } + + /** + * Load the plugin from the jar file + * @throws InvalidPluginException if the plugin.toml file is not found + * @throws UnsupportedPluginException if the plugin requests an api version higher than the current one + */ + fun load() { + if(loaded) return + + fetchInfoFromToml() + + if(!shouldEnable) { + appender.appendln("${PluginOutput.SPECIAL_SILENT}Plugin $pluginName v$pluginVersion is disabled") + return + } + + appender.appendln("${PluginOutput.SPECIAL_SILENT}Loading plugin $pluginName v$pluginVersion by $pluginAuthor from ${pluginSource.name}") + + signature + + setupFs() + + if(pluginToml.contains("api-version") || pluginToml.contains("min-api-version")) { + // default to api-version if min-api-version is not present + val apiVersionKey = if(pluginToml.contains("api-version")) "api-version" else "min-api-version" + val parsedVersion = ParsedVersion(pluginToml.getString(apiVersionKey)) + + if(parsedVersion > EOCVSim.PARSED_VERSION) + throw UnsupportedPluginException("Plugin requires a minimum api version of v${parsedVersion}, EOCV-Sim is currently running at v${EOCVSim.PARSED_VERSION}") + + logger.info("Plugin $pluginName requests min api version of v${parsedVersion}") + } + + if(pluginToml.contains("max-api-version")) { + val parsedVersion = ParsedVersion(pluginToml.getString("max-api-version")) + + if(parsedVersion < EOCVSim.PARSED_VERSION) + throw UnsupportedPluginException("Plugin requires a max api version of v${parsedVersion}, EOCV-Sim is currently running at v${EOCVSim.PARSED_VERSION}") + + logger.info("Plugin $pluginName requests max api version of v${parsedVersion}") + } + + if(pluginToml.contains("exact-api-version")) { + val parsedVersion = ParsedVersion(pluginToml.getString("exact-api-version")) + + if(parsedVersion != EOCVSim.PARSED_VERSION) + throw UnsupportedPluginException("Plugin requires an exact api version of v${parsedVersion}, EOCV-Sim is currently running at v${EOCVSim.PARSED_VERSION}") + + logger.info("Plugin $pluginName requests exact api version of v${parsedVersion}") + } + + if(pluginToml.getBoolean("super-access", false)) { + requestSuperAccess(pluginToml.getString("super-access-reason", "")) + } + + pluginClass = pluginClassLoader.loadClassStrict(pluginToml.getString("main")) + plugin = pluginClass.getConstructor().newInstance() as EOCVSimPlugin + + plugin.onLoad() + + loaded = true + } + + private fun setupFs() { + if(!fileSystemZip.exists()) { + val zip = ZipFile(fileSystemZip) // kinda wack but uh, yeah... + zip.addFile(ConfigLoader.CONFIG_SAVEFILE) + zip.removeFile(ConfigLoader.CONFIG_SAVEFILE.name) + zip.close() + } + + fileSystem = SandboxFileSystem(this) + } + + /** + * Enable the plugin + */ + fun enable() { + if(enabled || !loaded) return + + if(!shouldEnable) return + + appender.appendln("${PluginOutput.SPECIAL_SILENT}Enabling plugin $pluginName v$pluginVersion") + + plugin.enabled = true + plugin.onEnable() + + enabled = true + } + + /** + * Disable the plugin + */ + fun disable() { + if(!enabled || !loaded) return + + appender.appendln("${PluginOutput.SPECIAL_SILENT}Disabling plugin $pluginName v$pluginVersion") + + plugin.enabled = false + plugin.onDisable() + + kill() + } + + /** + * Kill the plugin + * This will close the file system and ban the class loader + * @see EventHandler.banClassLoader + */ + fun kill() { + if(!loaded) return + fileSystem.close() + enabled = false + EventHandler.banClassLoader(pluginClassLoader) + + pluginClassLoader.close() + } + + /** + * Request super access for the plugin + * @param reason the reason for requesting super access + */ + fun requestSuperAccess(reason: String): Boolean { + return eocvSim.pluginManager.requestSuperAccessFor(this, reason) + } + + /** + * Get the hash of the plugin based off the plugin name and author + * @return the hash + */ + fun hash() = "${pluginName}${PluginOutput.SPECIAL}${pluginAuthor}".hashString + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginManager.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginManager.kt index ff137677..3e9fb974 100644 --- a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginManager.kt +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginManager.kt @@ -1,170 +1,272 @@ -/* - * Copyright (c) 2024 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package io.github.deltacv.eocvsim.plugin.loader - -import com.github.serivesmejia.eocvsim.EOCVSim -import com.github.serivesmejia.eocvsim.util.JavaProcess -import com.github.serivesmejia.eocvsim.util.extension.plus -import com.github.serivesmejia.eocvsim.util.io.EOCVSimFolder -import com.github.serivesmejia.eocvsim.util.loggerForThis -import io.github.deltacv.eocvsim.gui.dialog.SuperAccessRequestMain -import java.io.File -import java.util.* - -/** - * Manages the loading, enabling and disabling of plugins - * @param eocvSim the EOCV-Sim instance - */ -class PluginManager(val eocvSim: EOCVSim) { - - companion object { - val PLUGIN_FOLDER = (EOCVSimFolder + File.separator + "plugins").apply { mkdir() } - val FILESYSTEMS_FOLDER = (PLUGIN_FOLDER + File.separator + "filesystems").apply { mkdir() } - - const val GENERIC_SUPERACCESS_WARN = "Plugins run in a restricted environment by default. SuperAccess will grant full system access. Ensure you trust the authors before accepting." - const val GENERIC_LAWYER_YEET = "

By accepting, you acknowledge that deltacv is not responsible for damages caused by third-party plugins." - } - - val logger by loggerForThis() - - private val _pluginFiles = mutableListOf() - - /** - * List of plugin files in the plugins folder - */ - val pluginFiles get() = _pluginFiles.toList() - - private val loaders = mutableMapOf() - - private var isEnabled = false - - /** - * Initializes the plugin manager - * Loads all plugin files in the plugins folder - * Creates a PluginLoader for each plugin file - * and stores them in the loaders map - * @see PluginLoader - */ - fun init() { - val filesInPluginFolder = PLUGIN_FOLDER.listFiles() ?: arrayOf() - - for (file in filesInPluginFolder) { - if (file.extension == "jar") _pluginFiles.add(file) - } - - if(pluginFiles.isEmpty()) { - logger.info("No plugins to load") - return - } - - for (pluginFile in pluginFiles) { - loaders[pluginFile] = PluginLoader(pluginFile, eocvSim) - } - - isEnabled = true - } - - /** - * Loads all plugins - * @see PluginLoader.load - */ - fun loadPlugins() { - for ((file, loader) in loaders) { - try { - loader.load() - } catch (e: Throwable) { - logger.error("Failure loading ${file.name}", e) - loaders.remove(file) - loader.kill() - } - } - } - - /** - * Enables all plugins - * @see PluginLoader.enable - */ - fun enablePlugins() { - for (loader in loaders.values) { - try { - loader.enable() - } catch (e: Throwable) { - logger.error("Failure enabling ${loader.pluginName} v${loader.pluginVersion}", e) - loader.kill() - } - } - } - - /** - * Disables all plugins - * @see PluginLoader.disable - */ - @Synchronized - fun disablePlugins() { - if(!isEnabled) return - - for (loader in loaders.values) { - try { - loader.disable() - } catch (e: Throwable) { - logger.error("Failure disabling ${loader.pluginName} v${loader.pluginVersion}", e) - loader.kill() - } - } - - isEnabled = false - } - - /** - * Requests super access for a plugin loader - * - * @param loader the plugin loader to request super access for - * @param reason the reason for requesting super access - * @return true if super access was granted, false otherwise - */ - fun requestSuperAccessFor(loader: PluginLoader, reason: String): Boolean { - if(loader.hasSuperAccess) return true - - logger.info("Requesting super access for ${loader.pluginName} v${loader.pluginVersion}") - - var warning = "$GENERIC_SUPERACCESS_WARN" - if(reason.trim().isNotBlank()) { - warning += "

$reason" - } - - warning += GENERIC_LAWYER_YEET - - warning += "" - - val name = "${loader.pluginName} by ${loader.pluginAuthor}".replace(" ", "-") - - if(JavaProcess.exec(SuperAccessRequestMain::class.java, null, Arrays.asList(name, warning)) == 171) { - eocvSim.config.superAccessPluginHashes.add(loader.pluginHash) - eocvSim.configManager.saveToFile() - return true - } - - return false - } +/* + * Copyright (c) 2024 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package io.github.deltacv.eocvsim.plugin.loader + +import com.github.serivesmejia.eocvsim.EOCVSim +import com.github.serivesmejia.eocvsim.gui.DialogFactory +import com.github.serivesmejia.eocvsim.gui.dialog.PluginOutput +import com.github.serivesmejia.eocvsim.gui.dialog.PluginOutput.Companion.trimSpecials +import com.github.serivesmejia.eocvsim.util.extension.plus +import com.github.serivesmejia.eocvsim.util.io.EOCVSimFolder +import com.github.serivesmejia.eocvsim.util.loggerForThis +import com.github.serivesmejia.eocvsim.util.loggerOf +import io.github.deltacv.eocvsim.plugin.repository.PluginRepositoryManager +import io.github.deltacv.eocvsim.plugin.security.superaccess.SuperAccessDaemon +import io.github.deltacv.eocvsim.plugin.security.superaccess.SuperAccessDaemonClient +import io.github.deltacv.eocvsim.plugin.security.toMutable +import java.io.File +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock +import kotlin.properties.Delegates + +/** + * Manages the loading, enabling and disabling of plugins + * @param eocvSim the EOCV-Sim instance + */ +class PluginManager(val eocvSim: EOCVSim) { + + companion object { + val PLUGIN_FOLDER = (EOCVSimFolder + File.separator + "plugins").apply { mkdir() } + val FILESYSTEMS_FOLDER = (PLUGIN_FOLDER + File.separator + "filesystems").apply { mkdir() } + + const val GENERIC_SUPERACCESS_WARN = "Plugins run in a restricted environment by default. SuperAccess will grant full system access. Ensure you trust the authors before accepting." + const val GENERIC_LAWYER_YEET = "

By accepting, you acknowledge that deltacv is not responsible for damages caused by third-party plugins." + } + + val logger by loggerForThis() + + val superAccessDaemonClient = SuperAccessDaemonClient() + + private val _loadedPluginHashes = mutableListOf() + val loadedPluginHashes get() = _loadedPluginHashes.toList() + + private val haltLock = ReentrantLock() + private val haltCondition = haltLock.newCondition() + + val appender by lazy { + val appender = DialogFactory.createMavenOutput(this) { + haltLock.withLock { + haltCondition.signalAll() + } + } + + val logger by loggerOf("PluginOutput") + + appender.subscribe { + if(!it.isBlank()) { + val message = it.trimSpecials() + + if(message.isNotBlank()) { + logger.info(message) + } + } + } + + appender + } + + val repositoryManager by lazy { + PluginRepositoryManager(appender, haltLock, haltCondition) + } + + private val _pluginFiles = mutableListOf() + + /** + * List of plugin files in the plugins folder + */ + val pluginFiles get() = _pluginFiles.toList() + + private val _loaders = mutableMapOf() + val loaders get() = _loaders.toMap() + + private var enableTimestamp by Delegates.notNull() + private var isEnabled = false + + /** + * Initializes the plugin manager + * Loads all plugin files in the plugins folder + * Creates a PluginLoader for each plugin file + * and stores them in the loaders map + * @see PluginLoader + */ + fun init() { + eocvSim.visualizer.onInitFinished { + appender.append(PluginOutput.SPECIAL_FREE) + } + + superAccessDaemonClient.init() + + repositoryManager.init() + + val pluginFilesInFolder = PLUGIN_FOLDER.listFiles()?.let { + it.filter { file -> file.extension == "jar" } + } ?: emptyList() + + _pluginFiles.addAll(repositoryManager.resolveAll()) + + if(eocvSim.config.flags.getOrDefault("startFresh", false)) { + logger.warn("startFresh = true, deleting all plugins in the plugins folder") + + for (file in pluginFilesInFolder) { + file.delete() + } + + eocvSim.config.flags["startFresh"] = false + eocvSim.configManager.saveToFile() + } else { + _pluginFiles.addAll(pluginFilesInFolder) + } + + for (file in pluginFiles) { + if (file.extension == "jar") _pluginFiles.add(file) + } + + if(pluginFiles.isEmpty()) { + appender.appendln(PluginOutput.SPECIAL_SILENT + "No plugins to load") + return + } + + for (pluginFile in pluginFiles) { + _loaders[pluginFile] = PluginLoader( + pluginFile, + repositoryManager.resolvedFiles, + if(pluginFile in repositoryManager.resolvedFiles) + PluginSource.REPOSITORY else PluginSource.FILE, + eocvSim, + appender + ) + } + + enableTimestamp = System.currentTimeMillis() + isEnabled = true + } + + /** + * Loads all plugins + * @see PluginLoader.load + */ + fun loadPlugins() { + for ((file, loader) in _loaders) { + try { + loader.fetchInfoFromToml() + + val hash = loader.hash() + + if(hash in _loadedPluginHashes) { + val source = if(loader.pluginSource == PluginSource.REPOSITORY) "repository.toml file" else "plugins folder" + + appender.appendln("Plugin ${loader.pluginName} by ${loader.pluginAuthor} is already loaded. Please delete the duplicate from the $source !") + return + } + + loader.load() + _loadedPluginHashes.add(hash) + } catch (e: Throwable) { + appender.appendln("Failure loading ${loader.pluginName} v${loader.pluginVersion}:") + appender.appendln(e.message ?: "Unknown error") + logger.error("Failure loading ${loader.pluginName} v${loader.pluginVersion}", e) + + _loaders.remove(file) + loader.kill() + } + } + } + + /** + * Enables all plugins + * @see PluginLoader.enable + */ + fun enablePlugins() { + for (loader in _loaders.values) { + try { + loader.enable() + } catch (e: Throwable) { + appender.appendln("Failure enabling ${loader.pluginName} v${loader.pluginVersion}: ${e.message}") + logger.error("Failure enabling ${loader.pluginName} v${loader.pluginVersion}", e) + loader.kill() + } + } + } + + /** + * Disables all plugins + * @see PluginLoader.disable + */ + @Synchronized + fun disablePlugins() { + if(!isEnabled) return + + for (loader in _loaders.values) { + try { + loader.disable() + } catch (e: Throwable) { + appender.appendln("Failure disabling ${loader.pluginName} v${loader.pluginVersion}: ${e.message}") + logger.error("Failure disabling ${loader.pluginName} v${loader.pluginVersion}", e) + loader.kill() + } + } + + isEnabled = false + } + + /** + * Requests super access for a plugin loader + * + * @param loader the plugin loader to request super access for + * @param reason the reason for requesting super access + * @return true if super access was granted, false otherwise + */ + fun requestSuperAccessFor(loader: PluginLoader, reason: String): Boolean { + if(loader.hasSuperAccess) { + appender.appendln(PluginOutput.SPECIAL_SILENT + "Plugin ${loader.pluginName} v${loader.pluginVersion} already has super access") + return true + } + + val signature = loader.signature + + appender.appendln(PluginOutput.SPECIAL_SILENT + "Requesting super access for ${loader.pluginName} v${loader.pluginVersion}") + + var access = false + + superAccessDaemonClient.sendRequest(SuperAccessDaemon.SuperAccessMessage.Request( + loader.pluginFile.absolutePath, + signature.toMutable(), + reason + )) { + if(it) { + access = true + } + + haltLock.withLock { + haltCondition.signalAll() + } + } + + haltLock.withLock { + haltCondition.await() + } + + return access + } } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/repository/PluginRepositoryManager.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/repository/PluginRepositoryManager.kt new file mode 100644 index 00000000..1672abd5 --- /dev/null +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/repository/PluginRepositoryManager.kt @@ -0,0 +1,318 @@ +/* + * Copyright (c) 2024 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package io.github.deltacv.eocvsim.plugin.repository + +import com.github.serivesmejia.eocvsim.gui.dialog.AppendDelegate +import com.github.serivesmejia.eocvsim.gui.dialog.PluginOutput +import com.github.serivesmejia.eocvsim.util.SysUtil +import com.github.serivesmejia.eocvsim.util.extension.hashString +import com.github.serivesmejia.eocvsim.util.extension.plus +import com.github.serivesmejia.eocvsim.util.loggerForThis +import com.moandjiezana.toml.Toml +import io.github.deltacv.eocvsim.plugin.loader.PluginManager +import org.jboss.shrinkwrap.resolver.api.maven.ConfigurableMavenResolverSystem +import org.jboss.shrinkwrap.resolver.api.maven.Maven +import java.io.File +import java.util.concurrent.locks.ReentrantLock +import java.util.concurrent.locks.Condition +import java.util.concurrent.TimeUnit +import kotlin.collections.iterator +import kotlin.concurrent.withLock + +class PluginRepositoryManager( + val appender: AppendDelegate, + val haltLock: ReentrantLock, + val haltCondition: Condition +) { + + companion object { + val REPOSITORY_FILE = PluginManager.PLUGIN_FOLDER + File.separator + "repository.toml" + val CACHE_FILE = PluginManager.PLUGIN_FOLDER + File.separator + "cache.toml" + + val REPOSITORY_TOML_RES = PluginRepositoryManager::class.java.getResourceAsStream("/repository.toml") + val CACHE_TOML_RES = PluginRepositoryManager::class.java.getResourceAsStream("/cache.toml") + + const val UNSURE_USER_HELP = "If you're unsure on what to do, delete the repository.toml file and restart the program." + } + + private lateinit var pluginsToml: Toml + private lateinit var pluginsRepositories: Toml + private lateinit var plugins: Toml + + private lateinit var cacheToml: Toml + private lateinit var cachePluginsToml: Toml + private lateinit var cacheTransitiveToml: Toml + + private lateinit var resolver: ConfigurableMavenResolverSystem + + private val _resolvedFiles = mutableListOf() + val resolvedFiles get() = _resolvedFiles.toList() + + val logger by loggerForThis() + + fun init() { + logger.info("Initializing...") + + appender // init appender + + SysUtil.copyFileIs(CACHE_TOML_RES, CACHE_FILE, false) + cacheToml = Toml().read(CACHE_FILE) + + cachePluginsToml = cacheToml.getTable("plugins") + ?: Toml() + + cacheTransitiveToml = cacheToml.getTable("transitive") + ?: Toml() + + SysUtil.copyFileIs(REPOSITORY_TOML_RES, REPOSITORY_FILE, false) + pluginsToml = Toml().read(REPOSITORY_FILE) + + pluginsRepositories = pluginsToml.getTable("repositories") + ?: throw InvalidFileException("No repositories found in repository.toml. $UNSURE_USER_HELP") + plugins = pluginsToml.getTable("plugins") + ?: Toml() + + resolver = Maven.configureResolver() + .withClassPathResolution(false) + + for (repo in pluginsRepositories.toMap()) { + if(repo.value !is String) + throw InvalidFileException("Invalid repository URL in repository.toml. $UNSURE_USER_HELP") + + var repoUrl = repo.value as String + if(!repoUrl.endsWith("/")) { + repoUrl += "/" + } + + resolver.withRemoteRepo(repo.key, repoUrl, "default") + + logger.info("Added repository ${repo.key} with URL $repoUrl") + } + } + + fun resolveAll(): List { + val files = mutableListOf() + val newCache = mutableMapOf() + val newTransitiveCache = mutableMapOf>() + var shouldHalt = false + + for (plugin in plugins.toMap()) { + if (plugin.value !is String) { + throw InvalidFileException("Invalid plugin dependency in repository.toml. $UNSURE_USER_HELP") + } + + val pluginDep = plugin.value as String + var pluginJar: File? + + try { + // Attempt to resolve from cache + pluginJar = if (resolveFromCache(pluginDep, newCache, newTransitiveCache)) { + File(newCache[pluginDep.hashString]!!) + } else { + // Resolve from the resolver if not found in cache + resolveFromResolver(pluginDep, newCache, newTransitiveCache) + } + + files += pluginJar + } catch (ex: Exception) { + handleResolutionError(pluginDep, ex) + shouldHalt = true + } + } + + writeCacheFile(newCache, newTransitiveCache) + + handleResolutionOutcome(shouldHalt) + + return files + } + + // Function to handle resolution from cache + private fun resolveFromCache( + pluginDep: String, + newCache: MutableMap, + newTransitiveCache: MutableMap> + ): Boolean { + for (cached in cachePluginsToml.toMap()) { + if (cached.key == pluginDep.hashString) { + val cachedFile = File(cached.value as String) + + if (cachedFile.exists() && areAllTransitivesCached(pluginDep)) { + addToResolvedFiles(cachedFile, pluginDep, newCache, newTransitiveCache) + appender.appendln( + PluginOutput.SPECIAL_SILENT + + "Found cached plugin \"$pluginDep\" (${pluginDep.hashString}). All transitive dependencies OK." + ) + return true + } else { + appender.appendln( + PluginOutput.SPECIAL_SILENT + + "Dependency missing for plugin $pluginDep. Resolving..." + ) + } + } + } + return false + } + + // Function to check if all transitive dependencies are cached + private fun areAllTransitivesCached(pluginDep: String): Boolean { + val deps = cacheTransitiveToml.getList(pluginDep.hashString) + + val depsHash = depsToHash(deps) + val tomlHash = cacheTransitiveToml.getString("${pluginDep.hashString}_hash", "") + + val matchesDepsHash = depsHash == tomlHash + + if(!matchesDepsHash) { + appender.appendln( + PluginOutput.SPECIAL_SILENT + + "Mismatch, $depsHash != $tomlHash" + ) + + appender.appendln( + PluginOutput.SPECIAL_SILENT + + "Transitive dependencies hash mismatch for plugin $pluginDep. Resolving..." + ) + } + + return matchesDepsHash && + deps != null && + deps.isNotEmpty() && + deps.all { + val exists = File(it).exists() + + if(!exists) { + appender.appendln( + PluginOutput.SPECIAL_SILENT + + "Couldn't find file specified in cache for plugin $pluginDep, expected at \"$it\"." + ) + } + + exists + } + } + + // Function to resolve plugin using the resolver + private fun resolveFromResolver( + pluginDep: String, + newCache: MutableMap, + newTransitiveCache: MutableMap> + ): File { + appender.appendln("Resolving plugin \"$pluginDep\"...") + + var pluginJar: File? = null + resolver.resolve(pluginDep) + .withTransitivity() + .asFile() + .forEach { file -> + if (pluginJar == null) { + pluginJar = file + newCache[pluginDep.hashString] = file.absolutePath + } else { + newTransitiveCache.getOrPut(pluginDep.hashString) { mutableListOf() } + .add(file.absolutePath) + } + + _resolvedFiles += file + } + + return pluginJar!! + } + + // Function to add resolved files to the cache and resolved list + private fun addToResolvedFiles( + cachedFile: File, + pluginDep: String, + newCache: MutableMap, + newTransitiveCache: MutableMap> + ) { + _resolvedFiles += cachedFile + newCache[pluginDep.hashString] = cachedFile.absolutePath + + cacheTransitiveToml.getList(pluginDep.hashString)?.forEach { transitive -> + _resolvedFiles += File(transitive) + newTransitiveCache.getOrPut(pluginDep.hashString) { mutableListOf() } + .add(transitive) + } + } + + // Function to handle resolution errors + private fun handleResolutionError(pluginDep: String, ex: Exception) { + logger.warn("Failed to resolve plugin dependency \"$pluginDep\"", ex) + appender.appendln("Failed to resolve plugin \"$pluginDep\": ${ex.message}") + } + + // Function to handle the outcome of the resolution process + private fun handleResolutionOutcome(shouldHalt: Boolean) { + if (shouldHalt) { + appender.append(PluginOutput.SPECIAL_CONTINUE) + haltLock.withLock { + haltCondition.await(15, TimeUnit.SECONDS) // wait for user to read the error + } + } else { + appender.append(PluginOutput.SPECIAL_CLOSE) + } + } + + private fun writeCacheFile( + cache: Map, + transitiveCache: Map> + ) { + val cacheBuilder = StringBuilder() + + cacheBuilder.appendLine("# Do not edit this file, it is generated by the application.") + + cacheBuilder.appendLine("[plugins]") // add plugins table + + for(cached in cache) { + cacheBuilder.appendLine("${cached.key} = \"${cached.value.replace("\\", "/")}\"") + } + + cacheBuilder.appendLine("[transitive]") // add transitive dependencies table + + for((plugin, deps) in transitiveCache) { + cacheBuilder.appendLine("$plugin = [") // add plugin hash as key + + for((i, dep) in deps.withIndex()) { + cacheBuilder.append("\t\"${dep.replace("\\", "/")}\"") // add dependency path + if(i < deps.size - 1) cacheBuilder.append(",") // add comma if not last + + cacheBuilder.appendLine() + } + + cacheBuilder.appendLine("]") + + cacheBuilder.appendLine("${plugin}_hash=\"${depsToHash(deps)}\"") + } + + SysUtil.saveFileStr(CACHE_FILE, cacheBuilder.toString().trim()) + } + + private fun depsToHash(deps: List) = + deps.joinToString(File.pathSeparator).replace("\\", "/").trimEnd(File.pathSeparatorChar).hashString +} + + +class InvalidFileException(msg: String) : RuntimeException(msg) \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/security/Authority.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/security/Authority.kt new file mode 100644 index 00000000..20d71294 --- /dev/null +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/security/Authority.kt @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2024 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package io.github.deltacv.eocvsim.plugin.security + +import com.github.serivesmejia.eocvsim.util.io.EOCVSimFolder +import com.github.serivesmejia.eocvsim.util.loggerForThis +import com.moandjiezana.toml.Toml +import java.io.File +import java.net.URL +import java.security.KeyFactory +import java.security.PublicKey +import java.security.spec.X509EncodedKeySpec +import java.util.Base64 +import java.util.concurrent.TimeUnit + +data class Authority( + val name: String, + val publicKey: PublicKey +) + +data class MutableAuthority( + var name: String, + var publicKey: ByteArray +) + +fun Authority.toMutable() = MutableAuthority(name, publicKey.encoded) + +data class CachedAuthority( + val authority: Authority, + val fetchedTime: Long // Timestamp when the key was fetched +) + +object AuthorityFetcher { + + const val AUTHORITY_SERVER_URL = "https://raw.githubusercontent.com/deltacv/Authorities/refs/heads/master" + + fun sectionRegex(name: String) = Regex("\\[\\Q$name\\E](?:\\n[^\\[]+)*") + + private val AUTHORITIES_FILE = File(EOCVSimFolder, "authorities.toml") + + private val TTL_DURATION_MS = TimeUnit.HOURS.toMillis(8) + + private val logger by loggerForThis() + private val cache = mutableMapOf() + + fun fetchAuthority(name: String): Authority? { + // Check if the authority is cached and valid + cache[name]?.let { cachedAuthority -> + if (System.currentTimeMillis() - cachedAuthority.fetchedTime < TTL_DURATION_MS) { + logger.info("Returning cached authority for $name") + return cachedAuthority.authority + } else { + logger.info("Cached authority for $name has expired") + } + } + + // Load authorities from file if it exists + if (AUTHORITIES_FILE.exists()) { + try { + val authoritiesToml = Toml().read(AUTHORITIES_FILE) + val authorityData = authoritiesToml.getTable(name) + val authorityPublicKey = authorityData?.getString("public") + val fetchedTime = authorityData?.getLong("timestamp") + + // Check if the fetched time is valid and within the TTL + if (authorityPublicKey != null && fetchedTime != null && + System.currentTimeMillis() - fetchedTime < TTL_DURATION_MS) { + + val authority = Authority(name, parsePublicKey(authorityPublicKey)) + cache[name] = CachedAuthority(authority, fetchedTime) + + return authority + } + } catch (e: Exception) { + logger.error("Failed to read authorities file", e) + AUTHORITIES_FILE.delete() + } + } + + // Fetch the authority from the server + val authorityUrl = "${AUTHORITY_SERVER_URL.trim('/')}/$name" + return try { + val authorityPublicKey = URL(authorityUrl).readText() + val pem = parsePem(authorityPublicKey) + + val authority = Authority(name, parsePublicKey(pem)) + + // Cache the fetched authority + cache[name] = CachedAuthority(authority, System.currentTimeMillis()) + + // write to the authorities file + saveAuthorityToFile(name, pem) + + authority + } catch (e: Exception) { + logger.error("Failed to fetch authority from server", e) + null + } + } + + private fun saveAuthorityToFile(name: String, publicKey: String) { + try { + val sb = StringBuilder() + + // Load existing authorities if the file exists + if (AUTHORITIES_FILE.exists()) { + val existingToml = AUTHORITIES_FILE.readText() + + sb.append(if(existingToml.contains("[$name]")) { + existingToml.replace(sectionRegex(name), "") + } else { + existingToml + }) + } + + // Append new authority information + sb.appendLine("[$name]") + sb.appendLine("public = \"$publicKey\"") + sb.appendLine("timestamp = ${System.currentTimeMillis()}") + + // Write the updated content to the file + AUTHORITIES_FILE.appendText(sb.toString()) + } catch (e: Exception) { + logger.error("Failed to save authority to file", e) + } + } + + fun parsePem(pem: String) = + pem.trim().lines().drop(1).dropLast(1).joinToString("") + + private fun parsePublicKey(keyPEM: String): PublicKey { + // Decode the Base64 encoded string + val encoded = Base64.getDecoder().decode(keyPEM) + + // Generate the public key + val keySpec = X509EncodedKeySpec(encoded) + val keyFactory = KeyFactory.getInstance("RSA") + return keyFactory.generatePublic(keySpec) + } +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/security/KeyGeneratorTool.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/security/KeyGeneratorTool.kt new file mode 100644 index 00000000..97a6f916 --- /dev/null +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/security/KeyGeneratorTool.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2024 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package io.github.deltacv.eocvsim.plugin.security + +import java.io.File +import java.io.FileWriter +import java.security.KeyPair +import java.security.KeyPairGenerator +import java.security.PrivateKey +import java.util.Base64 + +fun main() { + // Generate RSA key pair + val keyPair: KeyPair = generateKeyPair() + + // Save keys to files + saveKeyToFile("private_key.pem", keyPair.private) + saveKeyToFile("public_key.pem", keyPair.public) + + println("Keys generated and saved to files.") +} + +fun generateKeyPair(): KeyPair { + val keyPairGenerator = KeyPairGenerator.getInstance("RSA") + keyPairGenerator.initialize(2048) // Use 2048 bits for key size + return keyPairGenerator.generateKeyPair() +} + +fun saveKeyToFile(filename: String, key: java.security.Key) { + val encodedKey = Base64.getEncoder().encodeToString(key.encoded) + val keyType = if (key is PrivateKey) "PRIVATE" else "PUBLIC" + + val pemFormat = "-----BEGIN ${keyType} KEY-----\n$encodedKey\n-----END ${keyType} KEY-----" + + FileWriter(File(filename)).use { writer -> + writer.write(pemFormat) + } +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/security/PluginSignatureVerifier.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/security/PluginSignatureVerifier.kt new file mode 100644 index 00000000..63d33eb9 --- /dev/null +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/security/PluginSignatureVerifier.kt @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2024 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package io.github.deltacv.eocvsim.plugin.security + +import com.github.serivesmejia.eocvsim.util.extension.hashString +import com.github.serivesmejia.eocvsim.util.loggerForThis +import com.moandjiezana.toml.Toml +import io.github.deltacv.eocvsim.plugin.loader.InvalidPluginException +import java.io.File +import java.security.PublicKey +import java.security.Signature +import java.util.Base64 +import java.util.zip.ZipFile + +data class PluginSignature( + val verified: Boolean, + val authority: Authority?, + val timestamp: Long +) + +data class MutablePluginSignature( + var verified: Boolean, + var authority: MutableAuthority?, + var timestamp: Long +) + +fun PluginSignature.toMutable() = MutablePluginSignature(verified, authority?.toMutable(), timestamp) + +object PluginSignatureVerifier { + private val emptyResult = PluginSignature(false, null, 0L) + + val logger by loggerForThis() + + fun verify(pluginFile: File): PluginSignature { + logger.info("Verifying plugin signature of ${pluginFile.name}") + + ZipFile(pluginFile).use { zip -> + val signatureEntry = zip.getEntry("signature.toml") + val pluginEntry = zip.getEntry("plugin.toml") + + if (signatureEntry == null) { + logger.warn("Plugin JAR does not contain a signature.toml file") + return emptyResult + } + + if(pluginEntry == null) { + logger.warn("Plugin JAR does not contain a plugin.toml file") + return emptyResult + } + + val signatureToml = zip.getInputStream(signatureEntry).bufferedReader() + val signature = Toml().read(signatureToml) + + val authorityName = signature.getString("authority") + if (authorityName == null) { + logger.warn("signature.toml does not contain an authority field") + return emptyResult + } + + val authorityPublic = signature.getString("public") + if (authorityPublic == null) { + logger.warn("signature.toml does not contain a public (key) field") + return emptyResult + } + + val authority = AuthorityFetcher.fetchAuthority(authorityName) + if (authority == null) { + logger.warn("Failed to fetch authority $authorityName") + return emptyResult + } + + if(Base64.getEncoder().encodeToString(authority.publicKey.encoded) != authorityPublic) { + logger.warn("Authority public key does not match the one in the signature") + return emptyResult + } + + val signatureData = signature.getTable("signatures") ?: run { + logger.warn("signature.toml does not contain a signatures table") + return emptyResult + } + + // Get the public key from the authority + val publicKey = authority.publicKey + + val signatures = mutableMapOf() + + // Verify each signature + for ((path, sign) in signatureData.toMap()) { + if(sign !is String) { + logger.warn("Signature for class $path is not a string") + return emptyResult + } + + signatures[path] = sign + } + + // Now, verify each class in the JAR file + val classEntries = zip.entries().toList().filter { entry -> entry.name.endsWith(".class") } + + if(classEntries.count() != signatures.count()) { + logger.warn("Amount of classes in the JAR does not match the amount of signatures") + return emptyResult + } + + val signatureStatus = mutableMapOf() + + for(sign in signatures) { + signatureStatus[sign.key] = false + } + + for (classEntry in classEntries) { + val className = classEntry.name.removeSuffix(".class").replace('/', '.') // Convert to fully qualified class name + + // Fetch the class bytecode + val classData = zip.getInputStream(classEntry).readBytes() + + // Hash the class name to get the signature key + val classHash = className.hashString + + // Verify the signature of the class + if (!verifySignature(classData, signatures[classHash]!!, publicKey)) { + throw InvalidPluginException("Signature verification failed for class $className. Please try to re-download the plugin or discard it immediately.") + } else { + signatureStatus[classHash] = true + } + } + + if(signatureStatus.containsValue(false)) { + throw InvalidPluginException("Some classes in the plugin are not signed. Please try to re-download the plugin or discard it immediately.") + } + + logger.info("Plugin ${pluginFile.absolutePath} has been verified, signed by $authorityName") + + + return PluginSignature(true, authority, System.currentTimeMillis()) + } + } + + private fun verifySignature(classData: ByteArray, signatureString: String, publicKey: PublicKey): Boolean { + // Extract the actual signature bytes from the string (you'll need to implement this) + val signatureBytes = decodeSignature(signatureString) // Implement this method + return try { + val signature = Signature.getInstance("SHA256withRSA") + signature.initVerify(publicKey) + signature.update(classData) + signature.verify(signatureBytes) + } catch (e: Exception) { + logger.error("Error during signature verification", e) + false + } + } + + private fun decodeSignature(signatureString: String): ByteArray { + // Remove any whitespace or newline characters that may exist in the string + val cleanSignature = signatureString.replace("\\s".toRegex(), "") + + // Decode the Base64-encoded string into a byte array + return Base64.getDecoder().decode(cleanSignature) + } +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/security/PluginSigningTool.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/security/PluginSigningTool.kt new file mode 100644 index 00000000..b6a4ee74 --- /dev/null +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/security/PluginSigningTool.kt @@ -0,0 +1,219 @@ +/* + * Copyright (c) 2024 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package io.github.deltacv.eocvsim.plugin.security + +import com.github.serivesmejia.eocvsim.gui.dialog.PluginOutput +import com.github.serivesmejia.eocvsim.util.extension.hashString +import com.github.serivesmejia.eocvsim.util.loggerForThis +import com.moandjiezana.toml.Toml +import io.github.deltacv.eocvsim.plugin.loader.PluginParser +import picocli.CommandLine +import java.io.File +import java.security.KeyFactory +import java.security.PrivateKey +import java.security.PublicKey +import java.security.Signature +import java.security.spec.PKCS8EncodedKeySpec +import javax.crypto.Cipher +import java.util.Base64 +import java.util.Scanner +import java.util.zip.ZipFile +import java.util.zip.ZipEntry +import java.io.FileOutputStream +import java.io.IOException +import java.util.zip.ZipOutputStream +import kotlin.math.log +import kotlin.system.exitProcess + +class PluginSigningTool : Runnable { + + val logger by loggerForThis() + + @CommandLine.Option(names = ["-p", "--plugin"], description = ["The plugin JAR file to sign"], required = true) + var pluginFile: String? = null + + @CommandLine.Option(names = ["-a", "--authority"], description = ["The authority to sign the plugin with"], required = true) + var authority: String? = null + + @CommandLine.Option(names = ["-k", "--key"], description = ["The private key file to sign the plugin with"], required = true) + var privateKeyFile: String? = null + + override fun run() { + logger.info("Signing plugin $pluginFile with authority $authority") + + val authority = AuthorityFetcher.fetchAuthority(authority ?: throw IllegalArgumentException("Authority is required")) + if (authority == null) { + exitProcess(1) + } + + // Load the private key + val privateKey = loadPrivateKey(privateKeyFile ?: throw IllegalArgumentException("Private key file is required")) + + val publicKey = authority.publicKey // Assuming Authority has a publicKey property + if (!isPrivateKeyMatchingAuthority(publicKey, privateKey)) { + logger.error("Private key does not match to the authority's public key.") + exitProcess(1) + } else { + logger.info("Private key matches to the authority's public key.") + } + + signPlugin(File(pluginFile), privateKey, authority) + + logger.info("Plugin signed successfully and saved") + } + + private fun loadPrivateKey(privateKeyPath: String): PrivateKey { + val keyBytes = File(privateKeyPath).readBytes() + val keyString = String(keyBytes) + val decodedKey = Base64.getDecoder().decode(AuthorityFetcher.parsePem(keyString)) + + val keySpec = PKCS8EncodedKeySpec(decodedKey) + val keyFactory = KeyFactory.getInstance("RSA") + + return keyFactory.generatePrivate(keySpec) + } + + private fun signPlugin(jarFile: File, privateKey: PrivateKey, authority: Authority) { + // Generate signatures for each class in the JAR + val signatures = mutableMapOf() + + ZipFile(jarFile).use { zip -> + val classEntries = zip.entries().asSequence().filter { it.name.endsWith(".class") } + + for (classEntry in classEntries) { + val className = classEntry.name.removeSuffix(".class").replace('/', '.') + + val classData = zip.getInputStream(classEntry).readBytes() + + // Sign the class data + val signature = signClass(classData, privateKey) + signatures[className] = signature + + logger.info("Signed class $className") + } + } + + logger.info("Signed all classes, creating signature.toml in jar") + + // Create signature.toml + createSignatureToml(signatures, authority, jarFile) + } + + private fun signClass(classData: ByteArray, privateKey: PrivateKey): String { + val signature = Signature.getInstance("SHA256withRSA") + signature.initSign(privateKey) + signature.update(classData) + val signatureBytes = signature.sign() + return Base64.getEncoder().encodeToString(signatureBytes) + } + + private fun createSignatureToml( + signatures: Map, + authority: Authority, jarFile: File + ) { + val toml = StringBuilder() + toml.appendLine("authority = \"${authority.name}\"") + toml.appendLine("public = \"${Base64.getEncoder().encodeToString(authority.publicKey.encoded)}\"") + toml.appendLine("timestamp = ${System.currentTimeMillis()}") + toml.appendLine("[signatures]") + + for ((className, signature) in signatures) { + toml.appendLine("${className.hashString} = \"$signature\"") + } + + saveTomlToJar(toml.toString(), jarFile) + } + + + private fun saveTomlToJar(tomlContent: String, jarFile: File) { + // Create a temporary file for the new JAR + val tempJarFile = File(jarFile.parentFile, "${jarFile.name}.tmp") + + ZipOutputStream(FileOutputStream(tempJarFile)).use { zipOut -> + // First, copy existing entries from the original JAR + jarFile.inputStream().use { jarIn -> + val jarInput = java.util.zip.ZipInputStream(jarIn) + var entry: ZipEntry? + while (jarInput.nextEntry.also { entry = it } != null) { + if(entry!!.name == "signature.toml") { + continue + } + + // Write each existing entry to the new JAR + zipOut.putNextEntry(ZipEntry(entry!!.name)) + jarInput.copyTo(zipOut) + zipOut.closeEntry() + } + } + + // Now add the new signature.toml file + val tomlEntry = ZipEntry("signature.toml") + zipOut.putNextEntry(tomlEntry) + zipOut.write(tomlContent.toByteArray(Charsets.UTF_8)) + zipOut.closeEntry() + } + + // Replace the original JAR with the new one + if (!jarFile.delete()) { + throw IOException("Failed to delete original JAR file: ${jarFile.absolutePath}") + } + if (!tempJarFile.renameTo(jarFile)) { + throw IOException("Failed to rename temporary JAR file to original name: ${tempJarFile.absolutePath}") + } + } + + private fun isPrivateKeyMatchingAuthority(publicKey: PublicKey, privateKey: PrivateKey): Boolean { + // encrypt and decrypt a sample message to check if the keys match + val sampleMessage = PluginOutput.SPECIAL + + val cipher = Cipher.getInstance("RSA") + cipher.init(Cipher.ENCRYPT_MODE, publicKey) + val encrypted = cipher.doFinal(sampleMessage.toByteArray()) + + cipher.init(Cipher.DECRYPT_MODE, privateKey) + val decrypted = cipher.doFinal(encrypted) + + return sampleMessage == String(decrypted) + } +} + +fun main(args: Array) { + if(args.isEmpty()) { + val scanner = Scanner(System.`in`) + + val tool = PluginSigningTool() + println("Enter the plugin JAR file path:") + tool.pluginFile = scanner.next() + + println("Enter the authority to sign the plugin with:") + tool.authority = scanner.next() + + println("Enter the private key file path:") + tool.privateKeyFile = scanner.next() + + tool.run() + } else { + CommandLine(PluginSigningTool()).execute(*args) + } +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/security/superaccess/SuperAccessDaemon.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/security/superaccess/SuperAccessDaemon.kt new file mode 100644 index 00000000..c4194121 --- /dev/null +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/security/superaccess/SuperAccessDaemon.kt @@ -0,0 +1,239 @@ +/* + * Copyright (c) 2024 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package io.github.deltacv.eocvsim.plugin.security.superaccess + +import com.github.serivesmejia.eocvsim.util.extension.plus +import com.github.serivesmejia.eocvsim.util.extension.fileHash +import com.github.serivesmejia.eocvsim.util.io.EOCVSimFolder +import com.github.serivesmejia.eocvsim.util.loggerForThis +import com.github.serivesmejia.eocvsim.util.serialization.PolymorphicAdapter +import com.google.gson.GsonBuilder +import com.moandjiezana.toml.Toml +import io.github.deltacv.eocvsim.gui.dialog.SuperAccessRequest +import io.github.deltacv.eocvsim.plugin.loader.PluginManager.Companion.GENERIC_LAWYER_YEET +import io.github.deltacv.eocvsim.plugin.loader.PluginManager.Companion.GENERIC_SUPERACCESS_WARN +import io.github.deltacv.eocvsim.plugin.loader.PluginParser +import io.github.deltacv.eocvsim.plugin.security.Authority +import io.github.deltacv.eocvsim.plugin.security.AuthorityFetcher +import io.github.deltacv.eocvsim.plugin.security.MutablePluginSignature +import org.java_websocket.client.WebSocketClient +import org.java_websocket.handshake.ServerHandshake +import java.io.File +import java.lang.Exception +import java.net.URI +import java.util.concurrent.Executors +import java.util.zip.ZipFile +import javax.swing.SwingUtilities +import kotlin.system.exitProcess + +object SuperAccessDaemon { + val logger by loggerForThis() + + val gson = GsonBuilder() + .registerTypeHierarchyAdapter(SuperAccessMessage::class.java, PolymorphicAdapter("message")) + .registerTypeHierarchyAdapter(SuperAccessResponse::class.java, PolymorphicAdapter("response")) + .create() + + @get:Synchronized + @set:Synchronized + private var uniqueId = 0 + + sealed class SuperAccessMessage { + data class Request(var pluginPath: String, var signature: MutablePluginSignature, var reason: String) : SuperAccessMessage() + data class Check(var pluginPath: String) : SuperAccessMessage() + + var id = uniqueId++ + } + + sealed class SuperAccessResponse(val id: Int) { + class Success(id: Int) : SuperAccessResponse(id) + class Failure(id: Int) : SuperAccessResponse(id) + } + + val SUPERACCESS_FILE = EOCVSimFolder + File.separator + "superaccess.txt" + + private val access = mutableMapOf() + + @JvmStatic + fun main(args: Array) { + if(args.isEmpty()) { + logger.error("Port is required.") + return + } + if(args.size > 1) { + logger.warn("Ignoring extra arguments.") + } + + System.setProperty("sun.java2d.d3d", "false") + + WsClient(args[0].toIntOrNull() ?: throw IllegalArgumentException("Port is not a valid int")).connect() + } + + class WsClient(port: Int) : WebSocketClient(URI("ws://localhost:$port")) { + + private val executor = Executors.newFixedThreadPool(4) + + override fun onOpen(p0: ServerHandshake?) { + logger.info("SuperAccessDaemon connection opened") + } + + override fun onMessage(msg: String) { + val message = gson.fromJson(msg, SuperAccessMessage::class.java) + + executor.submit { + when (message) { + is SuperAccessMessage.Request -> { + handleRequest(message) + } + + is SuperAccessMessage.Check -> { + handleCheck(message) + } + } + } + } + + private fun handleRequest(message: SuperAccessMessage.Request) { + val pluginFile = File(message.pluginPath) + + val parser = parsePlugin(pluginFile) ?: run { + logger.error("Failed to parse plugin at ${message.pluginPath}") + (gson.toJson(SuperAccessResponse.Failure(message.id))) + return@handleRequest + } + + if(SUPERACCESS_FILE.exists() && SUPERACCESS_FILE.readLines().contains(pluginFile.fileHash())) { + accessGranted(message.id, message.pluginPath) + return + } + + logger.info("Requesting SuperAccess for ${message.pluginPath}") + + var validAuthority: Authority? = null + var untrusted = false + + if(message.signature.authority != null) { + val declaredAuthority = message.signature.authority!! + val authorityName = declaredAuthority.name + + val fetchedAuthorityKey = AuthorityFetcher.fetchAuthority(authorityName)?.publicKey + + if(fetchedAuthorityKey == null) { + logger.error("Failed to fetch authority $authorityName") + } else { + if(fetchedAuthorityKey.encoded.contentEquals(declaredAuthority.publicKey)) { + logger.info("Authority key matches the fetched key for $authorityName") + validAuthority = Authority(authorityName, fetchedAuthorityKey) + } else { + logger.warn("Authority key does not match the fetched key for $authorityName") + untrusted = true + } + } + } else { + val fetch = AuthorityFetcher.fetchAuthority(parser.pluginAuthor) + untrusted = fetch != null // the plugin is claiming to be made by the authority, but it's not signed by them + } + + val reason = message.reason + + val name = "${parser.pluginName} v${parser.pluginVersion} by ${parser.pluginAuthor}" + + var warning = "$GENERIC_SUPERACCESS_WARN" + if(reason.trim().isNotBlank()) { + warning += "

$reason" + } + + warning += if(validAuthority != null) { + "

This plugin has been digitally signed by ${validAuthority.name}, ensuring its integrity and authenticity.
${validAuthority.name} is a trusted authority in the EOCV-Sim ecosystem." + } else if(untrusted) { + "

This plugin claims to be made by trusted authority ${parser.pluginAuthor}, but it has not been digitally signed by them.

Beware of potential security risks.

" + } else { + GENERIC_LAWYER_YEET + } + + warning += "" + + SwingUtilities.invokeLater { + SuperAccessRequest(name, warning, validAuthority == null || untrusted) { granted -> + if(granted) { + SUPERACCESS_FILE.appendText(pluginFile.fileHash() + "\n") + accessGranted(message.id, message.pluginPath) + } else { + accessDenied(message.id, message.pluginPath) + } + } + } + } + + private fun handleCheck(message: SuperAccessMessage.Check) { + if(access.containsKey(message.pluginPath)) { + if(access[message.pluginPath] == true) { + accessGranted(message.id, message.pluginPath) + return + } else { + accessDenied(message.id, message.pluginPath) + return + } + } + + val pluginFile = File(message.pluginPath) + + if(SUPERACCESS_FILE.exists() && SUPERACCESS_FILE.readLines().contains(pluginFile.fileHash())) { + accessGranted(message.id, message.pluginPath) + } else { + accessDenied(message.id, message.pluginPath) + } + } + + private fun accessGranted(id: Int, pluginPath: String) { + access[pluginPath] = true + send(gson.toJson(SuperAccessResponse.Success(id))) + } + + private fun accessDenied(id: Int, pluginPath: String) { + access[pluginPath] = false + send(gson.toJson(SuperAccessResponse.Failure(id))) + } + + override fun onClose(p0: Int, p1: String?, p2: Boolean) { + logger.info("SuperAccessDaemon connection closed: $p0, $p1, $p2") + exitProcess(-1) + } + + override fun onError(p0: Exception?) { + logger.error("Error in SuperAccessDaemon", p0) + } + } + + private fun parsePlugin(file: File): PluginParser? { + ZipFile(file).use { + val pluginToml = it.getEntry("plugin.toml") + if(pluginToml != null) { + return PluginParser(Toml().read(it.getInputStream(pluginToml))) + } + } + + return null + } +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/security/superaccess/SuperAccessDaemonClient.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/security/superaccess/SuperAccessDaemonClient.kt new file mode 100644 index 00000000..d92156df --- /dev/null +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/security/superaccess/SuperAccessDaemonClient.kt @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2024 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package io.github.deltacv.eocvsim.plugin.security.superaccess + +import com.github.serivesmejia.eocvsim.util.JavaProcess +import com.github.serivesmejia.eocvsim.util.loggerForThis +import org.java_websocket.WebSocket +import org.java_websocket.handshake.ClientHandshake +import org.java_websocket.server.WebSocketServer +import java.io.File +import java.lang.Exception +import java.net.InetSocketAddress +import java.util.concurrent.Executors +import java.util.concurrent.locks.Condition +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + +typealias ResponseReceiver = (SuperAccessDaemon.SuperAccessResponse) -> Unit +typealias ResponseCondition = (SuperAccessDaemon.SuperAccessResponse) -> Boolean + +class SuperAccessDaemonClient { + + val logger by loggerForThis() + + private val startLock = ReentrantLock() + val startCondition = startLock.newCondition() + + // create a new WebSocket server + private val server = WsServer(startLock, startCondition) + + fun init() { + server.start() + + startLock.withLock { + startCondition.await() + } + + logger.info("SuperAccessDaemonClient initialized") + } + + fun sendRequest(request: SuperAccessDaemon.SuperAccessMessage.Request, onResponse: (Boolean) -> Unit) { + if(server.connections.isEmpty()) { + onResponse(false) + return + } + + server.broadcast(SuperAccessDaemon.gson.toJson(request)) + + server.addResponseReceiver(request.id) { response -> + if(response is SuperAccessDaemon.SuperAccessResponse.Success) { + onResponse(true) + } else if(response is SuperAccessDaemon.SuperAccessResponse.Failure) { + onResponse(false) + } + } + } + + fun checkAccess(file: File): Boolean { + val lock = ReentrantLock() + val condition = lock.newCondition() + + val check = SuperAccessDaemon.SuperAccessMessage.Check(file.absolutePath) + server.broadcast(SuperAccessDaemon.gson.toJson(check)) + + var hasAccess = false + + server.addResponseReceiver(check.id) { response -> + if(response is SuperAccessDaemon.SuperAccessResponse.Success) { + hasAccess = true + + lock.withLock { + condition.signalAll() + } + } else if(response is SuperAccessDaemon.SuperAccessResponse.Failure) { + hasAccess = false + + lock.withLock { + condition.signalAll() + } + } + } + + lock.withLock { + condition.await(3, java.util.concurrent.TimeUnit.SECONDS) + } + + return hasAccess + } + + private class WsServer( + val startLock: ReentrantLock, + val startCondition: Condition, + ) : WebSocketServer(InetSocketAddress(0)) { + // create an executor with 1 thread + private val executor = Executors.newSingleThreadExecutor() + + val logger by loggerForThis() + + private val responseReceiver = mutableMapOf() + + private val pendingRequests = mutableMapOf() + private var processRestarts = 0 + + // Notify all pending requests if the process dies + private fun notifyPendingRequestsOfFailure() { + pendingRequests.forEach { key, value -> + value(SuperAccessDaemon.SuperAccessResponse.Failure(key)) + } + pendingRequests.clear() + } + + override fun onOpen(conn: WebSocket, p1: ClientHandshake?) { + val hostString = conn.localSocketAddress.hostString + if(hostString != "127.0.0.1" && hostString != "localhost" && hostString != "0.0.0.0") { + logger.warn("Connection from ${conn.remoteSocketAddress} refused, only localhost connections are allowed") + conn.close(1013, "Ipc does not allow connections incoming from non-localhost addresses") + } + + logger.info("SuperAccessDaemon is here.") + + processRestarts = 0 + + startLock.withLock { + startCondition.signalAll() + } + } + + override fun onClose( + p0: WebSocket?, + p1: Int, + p2: String?, + p3: Boolean + ) { + logger.info("SuperAccessDaemon is gone.") + notifyPendingRequestsOfFailure() // Notify all waiting clients + } + + override fun onMessage(ws: WebSocket, msg: String) { + val response = SuperAccessDaemon.gson.fromJson(msg, SuperAccessDaemon.SuperAccessResponse::class.java) + + for((condition, receiver) in responseReceiver.toMap()) { + if(condition(response)) { + receiver(response) + } + } + + pendingRequests.remove(response.id) + } + + override fun onError(p0: WebSocket?, p1: Exception?) { + logger.error("SuperAccessDaemon error", p1) + } + + override fun onStart() { + fun startProcess() { + executor.submit { + logger.info("Starting SuperAccessDaemon on port $port") + + val exitCode = JavaProcess.exec( + SuperAccessDaemon::class.java, + JavaProcess.SLF4JIOReceiver(logger), + null, listOf(port.toString()) + ) + + if(processRestarts == 6) { + logger.error("SuperAccessDaemon restarted too many times, aborting.") + return@submit + } + + logger.error("SuperAccessDaemon exited with code $exitCode, restarting...") + startProcess() // restart the process + + processRestarts++ + } + } + + startProcess() + } + + fun addResponseReceiver(id: Int, receiver: ResponseReceiver) { + pendingRequests[id] = receiver + responseReceiver[{ it.id == id }] = receiver + } + } + +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/sandbox/restrictions/DynamicLoadingClassRestrictions.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/sandbox/restrictions/DynamicLoadingClassRestrictions.kt index 4547cb54..f1fd0948 100644 --- a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/sandbox/restrictions/DynamicLoadingClassRestrictions.kt +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/sandbox/restrictions/DynamicLoadingClassRestrictions.kt @@ -1,106 +1,129 @@ -/* - * Copyright (c) 2024 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package io.github.deltacv.eocvsim.sandbox.restrictions - -val dynamicLoadingPackageWhitelist = setOf( - "java.lang", - "java.util", - "java.awt", - "javax.swing", - "java.nio", - "java.io.File", - - "kotlin", - - "com.github.serivesmejia.eocvsim", - "io.github.deltacv.eocvsim", - "org.firstinspires.ftc", - "com.qualcomm", - "org.opencv", - "org.openftc", - "android", - - "com.moandjiezana.toml", - "net.lingala.zip4j", - "com.google.gson", - "com.google.jimfs", - "org.slf4j", - "com.apache.logging", - "com.formdev.flatlaf" -) - -val dynamicLoadingPackageBlacklist = setOf( - // System and Runtime Classes - "java.lang.Runtime", - "java.lang.ProcessBuilder", - "java.lang.reflect", - - // File and I/O Operations - "java.io.RandomAccessFile", - "java.nio.file.Paths", - "java.nio.file.Files", - "java.nio.file.FileSystems", - - // Thread and Process Management - "java.lang.Process", - - // EOCV-Sim dangerous utils - "com.github.serivesmejia.eocvsim.util.SysUtil", - "com.github.serivesmejia.eocvsim.util.io", - "com.github.serivesmejia.eocvsim.util.ClasspathScan", - "com.github.serivesmejia.eocvsim.util.JavaProcess", - "com.github.serivesmejia.eocvsim.util.ReflectUtil", - "com.github.serivesmejia.eocvsim.util.FileExtKt", - "com.github.serivesmejia.eocvsim.util.compiler", - "com.github.serivesmejia.eocvsim.config", - - "io.github.deltacv.eocvsim.plugin.sandbox.nio.JimfsWatcher" -) - -val dynamicLoadingMethodBlacklist = setOf( - "java.lang.System#load", - "java.lang.System#loadLibrary", - "java.lang.System#setSecurityManager", - "java.lang.System#getSecurityManager", - "java.lang.System#setProperty", - "java.lang.System#clearProperty", - "java.lang.System#setenv", - - "java.lang.Class#newInstance", - "java.lang.Class#forName", - - "java.io.File#delete", - "java.io.File#createNewFile", - "java.io.File#mkdirs", - "java.io.File#renameTo", - "java.io.File#setExecutable", - "java.io.File#setReadable", - "java.io.File#setWritable", - "java.io.File#setLastModified", - "java.io.File#deleteOnExit", - "java.io.File#setReadOnly", - "java.io.File#setWritable", - "java.io.File#setReadable", - "java.io.File#setExecutable", +/* + * Copyright (c) 2024 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package io.github.deltacv.eocvsim.sandbox.restrictions + +val dynamicLoadingPackageWhitelist = setOf( + "java.lang", + "java.lang.RuntimeException", + "java.util", + "java.awt", + "javax.swing", + "java.nio", + "java.io.IOException", + "java.io.File", + "java.io.PrintStream", + + "kotlin", + + "com.github.serivesmejia.eocvsim", + "io.github.deltacv.eocvsim", + "org.firstinspires.ftc", + "com.qualcomm", + "org.opencv", + "org.openftc", + "android", + + "com.moandjiezana.toml", + "net.lingala.zip4j", + "com.google.gson", + "com.google.jimfs", + "org.slf4j", + "com.apache.logging", + "com.formdev.flatlaf" +) + +val dynamicLoadingExactMatchBlacklist = setOf( + "java.lang.Runtime" +) + +val dynamicLoadingPackageBlacklist = setOf( + // System and Runtime Classes + "java.lang.ProcessBuilder", + "java.lang.reflect", + + // File and I/O Operations + "java.io.RandomAccessFile", + "java.nio.file.Paths", + "java.nio.file.Files", + "java.nio.file.FileSystems", + + // Thread and Process Management + "java.lang.Process", + + // EOCV-Sim dangerous utils + "com.github.serivesmejia.eocvsim.util.SysUtil", + "com.github.serivesmejia.eocvsim.util.io", + "com.github.serivesmejia.eocvsim.util.ClasspathScan", + "com.github.serivesmejia.eocvsim.util.JavaProcess", + "com.github.serivesmejia.eocvsim.util.ReflectUtil", + "com.github.serivesmejia.eocvsim.util.FileExtKt", + "com.github.serivesmejia.eocvsim.util.compiler", + "com.github.serivesmejia.eocvsim.config", +) + +val dynamicLoadingMethodBlacklist = setOf( + "java.lang.System#load", + "java.lang.System#loadLibrary", + "java.lang.System#setSecurityManager", + "java.lang.System#getSecurityManager", + "java.lang.System#setProperty", + "java.lang.System#clearProperty", + "java.lang.System#setenv", + + "java.lang.Class#newInstance", + "java.lang.Class#forName", + + // Additional java.io.File methods to blacklist + "java.io.File#delete", + "java.io.File#createNewFile", + "java.io.File#mkdirs", + "java.io.File#renameTo", + "java.io.File#setExecutable", + "java.io.File#setReadable", + "java.io.File#setWritable", + "java.io.File#setLastModified", + "java.io.File#deleteOnExit", + "java.io.File#setReadOnly", + "java.io.File#setWritable", + "java.io.File#setReadable", + "java.io.File#setExecutable", + "java.io.File#list", + "java.io.File#listFiles", + "java.io.File#listRoots", + "java.io.File#canRead", + "java.io.File#canWrite", + "java.io.File#canExecute", + "java.io.File#length", + "java.io.File#getTotalSpace", + "java.io.File#getFreeSpace", + "java.io.File#getUsableSpace", + "java.io.File#getAbsolutePath", + "java.io.File#getCanonicalPath", + "java.io.File#getParentFile", + "java.io.File#exists", + "java.io.File#isDirectory", + "java.io.File#isFile", + "java.io.File#isHidden", + "java.io.File#lastModified" ) \ No newline at end of file diff --git a/EOCV-Sim/src/main/resources/cache.toml b/EOCV-Sim/src/main/resources/cache.toml new file mode 100644 index 00000000..92466bcc --- /dev/null +++ b/EOCV-Sim/src/main/resources/cache.toml @@ -0,0 +1 @@ +# Do not edit this file, it is generated by the application. \ No newline at end of file diff --git a/EOCV-Sim/src/main/resources/fonts/JetBrainsMono-Medium.ttf b/EOCV-Sim/src/main/resources/fonts/JetBrainsMono-Medium.ttf new file mode 100644 index 00000000..97671156 Binary files /dev/null and b/EOCV-Sim/src/main/resources/fonts/JetBrainsMono-Medium.ttf differ diff --git a/EOCV-Sim/src/main/resources/repository.toml b/EOCV-Sim/src/main/resources/repository.toml new file mode 100644 index 00000000..79e1b2ae --- /dev/null +++ b/EOCV-Sim/src/main/resources/repository.toml @@ -0,0 +1,10 @@ +[repositories] +# Declare the URL of the Maven repositories to use, with a friendly name. +# The URL must preferably end with a slash to be handled correctly. +central = "https://repo.maven.apache.org/maven2/" +jitpack = "https://jitpack.io/" + +[plugins] +# Declare the plugin ID and the Maven coordinates of the plugin, similar to how you do it in Gradle. +# (group:artifact:version) which will be used to download the plugin from one of Maven repositories. +# Any dependency that does not have a plugin.toml in its jar will not be considered after download. \ No newline at end of file diff --git a/EOCV-Sim/src/main/resources/templates/default_workspace.zip b/EOCV-Sim/src/main/resources/templates/default_workspace.zip index 9d41330a..0bbfc537 100644 Binary files a/EOCV-Sim/src/main/resources/templates/default_workspace.zip and b/EOCV-Sim/src/main/resources/templates/default_workspace.zip differ diff --git a/EOCV-Sim/src/main/resources/templates/gradle_workspace.zip b/EOCV-Sim/src/main/resources/templates/gradle_workspace.zip index 29a5cc7d..e69fb559 100644 Binary files a/EOCV-Sim/src/main/resources/templates/gradle_workspace.zip and b/EOCV-Sim/src/main/resources/templates/gradle_workspace.zip differ diff --git a/README.md b/README.md index c234f49a..61511494 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,22 @@ Join the [deltacv discord server](https://discord.gg/A3RMYzf6DA) ! ### Formerly, EOCV-Sim was hosted on a [personal account repo](https://github.com/serivesmejia/EOCV-Sim/). Released prior to 3.0.0 can be found there for historic purposes. +## [v3.8.0 - Major Plugin System Rework](https://github.com/deltacv/EOCV-Sim/releases/tag/v3.8.0) +- This is the 25th release for EOCV-Sim + - Changelog + - Updates AprilTagDesktop to 2.1.0-C, enabling support for Linux AARCH64. + - Implements a system that allows for plugins to be downloaded from a maven repository and loaded into the simulator. + - New repository.toml file that contains the list of repositories to download plugins from and the maven coordinates to download. + - Adds a new dialog to manage the loaded plugins and check the output of the plugin system. + - Rewrites SuperAccess verification to be handled by a separate JVM process, ensuring that the main process is not compromised by malicious code. + - Implements a plugin signature verification system to allow for authors to sign their plugins and ensure that they are not tampered with. + - The signature is stored on a custom format in the plugin jar, the signing authorities are pulled from a public key database. + - When making a SuperAccess request, the user is warned if the plugin is not signed or if the signature is invalid. + - Adds a KeyGeneratorTool and PluginSigningTool to allow for easy generation of keys and signing of plugins respectively. + - Internal changes: + - Adds a PolymorphicAdapter class to allow for easy serialization while retaining type information. + - Improvements to the handling of JavaProcess. + ## [v3.7.1 - Better FTC VisionPortal support & Plugin System Fixes](https://github.com/deltacv/EOCV-Sim/releases/tag/v3.7.1) - This is the 24th release for EOCV-Sim - Changelog diff --git a/TeamCode/build.gradle b/TeamCode/build.gradle index be3aaf0e..264ec071 100644 --- a/TeamCode/build.gradle +++ b/TeamCode/build.gradle @@ -1,5 +1,3 @@ -import java.nio.file.Paths - plugins { id 'org.jetbrains.kotlin.jvm' } @@ -8,7 +6,6 @@ apply from: '../build.common.gradle' dependencies { implementation project(':EOCV-Sim') - implementation "com.github.deltacv.AprilTagDesktop:AprilTagDesktop:$apriltag_plugin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib" } diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagDetectionPipeline.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagDetectionPipeline.java index f16770d3..7302ad47 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagDetectionPipeline.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagDetectionPipeline.java @@ -1,329 +1,315 @@ -/* - * Copyright (c) 2021 OpenFTC Team - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.firstinspires.ftc.teamcode; - -import com.qualcomm.robotcore.eventloop.opmode.Disabled; -import org.firstinspires.ftc.robotcore.external.Telemetry; -import org.firstinspires.ftc.robotcore.external.navigation.*; -import org.opencv.calib3d.Calib3d; -import org.opencv.core.CvType; -import org.opencv.core.Mat; -import org.opencv.core.MatOfDouble; -import org.opencv.core.MatOfPoint2f; -import org.opencv.core.MatOfPoint3f; -import org.opencv.core.Point; -import org.opencv.core.Point3; -import org.opencv.core.Scalar; -import org.opencv.imgproc.Imgproc; -import org.openftc.apriltag.AprilTagDetection; -import org.openftc.apriltag.AprilTagDetectorJNI; -import org.openftc.easyopencv.OpenCvPipeline; - -import java.util.ArrayList; - -@Disabled -public class AprilTagDetectionPipeline extends OpenCvPipeline -{ - // STATIC CONSTANTS - - public static Scalar blue = new Scalar(7,197,235,255); - public static Scalar red = new Scalar(255,0,0,255); - public static Scalar green = new Scalar(0,255,0,255); - public static Scalar white = new Scalar(255,255,255,255); - - static final double FEET_PER_METER = 3.28084; - - // Lens intrinsics - // UNITS ARE PIXELS - // NOTE: this calibration is for the C920 webcam at 800x448. - // You will need to do your own calibration for other configurations! - public static double fx = 578.272; - public static double fy = 578.272; - public static double cx = 402.145; - public static double cy = 221.506; - - // UNITS ARE METERS - public static double TAG_SIZE = 0.166; - - // instance variables - - private long nativeApriltagPtr; - private Mat grey = new Mat(); - private ArrayList detections = new ArrayList<>(); - - private ArrayList detectionsUpdate = new ArrayList<>(); - private final Object detectionsUpdateSync = new Object(); - - Mat cameraMatrix; - - double tagsizeX = TAG_SIZE; - double tagsizeY = TAG_SIZE; - - private float decimation; - private boolean needToSetDecimation; - private final Object decimationSync = new Object(); - - Telemetry telemetry; - - public AprilTagDetectionPipeline(Telemetry telemetry) { - this.telemetry = telemetry; - constructMatrix(); - } - - @Override - public void init(Mat frame) - { - // Allocate a native context object. See the corresponding deletion in the finalizer - nativeApriltagPtr = AprilTagDetectorJNI.createApriltagDetector(AprilTagDetectorJNI.TagFamily.TAG_36h11.string, 3, 3); - } - - @Override - public void finalize() - { - // Delete the native context we created in the init() function - AprilTagDetectorJNI.releaseApriltagDetector(nativeApriltagPtr); - } - - @Override - public Mat processFrame(Mat input) - { - // Convert to greyscale - Imgproc.cvtColor(input, grey, Imgproc.COLOR_RGBA2GRAY); - - synchronized (decimationSync) - { - if(needToSetDecimation) - { - AprilTagDetectorJNI.setApriltagDetectorDecimation(nativeApriltagPtr, decimation); - needToSetDecimation = false; - } - } - - // Run AprilTag - detections = AprilTagDetectorJNI.runAprilTagDetectorSimple(nativeApriltagPtr, grey, TAG_SIZE, fx, fy, cx, cy); - - synchronized (detectionsUpdateSync) - { - detectionsUpdate = detections; - } - - // For fun, use OpenCV to draw 6DOF markers on the image. We actually recompute the pose using - // OpenCV because I haven't yet figured out how to re-use AprilTag's pose in OpenCV. - for(AprilTagDetection detection : detections) - { - Pose pose = poseFromTrapezoid(detection.corners, cameraMatrix, tagsizeX, tagsizeY); - drawAxisMarker(input, tagsizeY/2.0, 6, pose.rvec, pose.tvec, cameraMatrix); - draw3dCubeMarker(input, tagsizeX, tagsizeX, tagsizeY, 5, pose.rvec, pose.tvec, cameraMatrix); - - Orientation rot = Orientation.getOrientation(detection.pose.R, AxesReference.INTRINSIC, AxesOrder.YXZ, AngleUnit.DEGREES); - - telemetry.addLine(String.format("\nDetected tag ID=%d", detection.id)); - telemetry.addLine(String.format("Translation X: %.2f feet", detection.pose.x*FEET_PER_METER)); - telemetry.addLine(String.format("Translation Y: %.2f feet", detection.pose.y*FEET_PER_METER)); - telemetry.addLine(String.format("Translation Z: %.2f feet", detection.pose.z*FEET_PER_METER)); - - telemetry.addLine(String.format("Rotation Yaw: %.2f degrees", rot.firstAngle)); - telemetry.addLine(String.format("Rotation Pitch: %.2f degrees", rot.secondAngle)); - telemetry.addLine(String.format("Rotation Roll: %.2f degrees", rot.thirdAngle)); - } - - telemetry.update(); - - return input; - } - - public void setDecimation(float decimation) - { - synchronized (decimationSync) - { - this.decimation = decimation; - needToSetDecimation = true; - } - } - - public ArrayList getLatestDetections() - { - return detections; - } - - public ArrayList getDetectionsUpdate() - { - synchronized (detectionsUpdateSync) - { - ArrayList ret = detectionsUpdate; - detectionsUpdate = null; - return ret; - } - } - - void constructMatrix() - { - // Construct the camera matrix. - // - // -- -- - // | fx 0 cx | - // | 0 fy cy | - // | 0 0 1 | - // -- -- - // - - cameraMatrix = new Mat(3,3, CvType.CV_32FC1); - - cameraMatrix.put(0,0, fx); - cameraMatrix.put(0,1,0); - cameraMatrix.put(0,2, cx); - - cameraMatrix.put(1,0,0); - cameraMatrix.put(1,1,fy); - cameraMatrix.put(1,2,cy); - - cameraMatrix.put(2, 0, 0); - cameraMatrix.put(2,1,0); - cameraMatrix.put(2,2,1); - } - - /** - * Draw a 3D axis marker on a detection. (Similar to what Vuforia does) - * - * @param buf the RGB buffer on which to draw the marker - * @param length the length of each of the marker 'poles' - * @param rvec the rotation vector of the detection - * @param tvec the translation vector of the detection - * @param cameraMatrix the camera matrix used when finding the detection - */ - void drawAxisMarker(Mat buf, double length, int thickness, Mat rvec, Mat tvec, Mat cameraMatrix) - { - // The points in 3D space we wish to project onto the 2D image plane. - // The origin of the coordinate space is assumed to be in the center of the detection. - MatOfPoint3f axis = new MatOfPoint3f( - new Point3(0,0,0), - new Point3(length,0,0), - new Point3(0,length,0), - new Point3(0,0,-length) - ); - - // Project those points - MatOfPoint2f matProjectedPoints = new MatOfPoint2f(); - Calib3d.projectPoints(axis, rvec, tvec, cameraMatrix, new MatOfDouble(), matProjectedPoints); - Point[] projectedPoints = matProjectedPoints.toArray(); - - // Draw the marker! - Imgproc.line(buf, projectedPoints[0], projectedPoints[1], red, thickness); - Imgproc.line(buf, projectedPoints[0], projectedPoints[2], green, thickness); - Imgproc.line(buf, projectedPoints[0], projectedPoints[3], blue, thickness); - - Imgproc.circle(buf, projectedPoints[0], thickness, white, -1); - } - - void draw3dCubeMarker(Mat buf, double length, double tagWidth, double tagHeight, int thickness, Mat rvec, Mat tvec, Mat cameraMatrix) - { - //axis = np.float32([[0,0,0], [0,3,0], [3,3,0], [3,0,0], - // [0,0,-3],[0,3,-3],[3,3,-3],[3,0,-3] ]) - - // The points in 3D space we wish to project onto the 2D image plane. - // The origin of the coordinate space is assumed to be in the center of the detection. - MatOfPoint3f axis = new MatOfPoint3f( - new Point3(-tagWidth/2, tagHeight/2,0), - new Point3( tagWidth/2, tagHeight/2,0), - new Point3( tagWidth/2,-tagHeight/2,0), - new Point3(-tagWidth/2,-tagHeight/2,0), - new Point3(-tagWidth/2, tagHeight/2,-length), - new Point3( tagWidth/2, tagHeight/2,-length), - new Point3( tagWidth/2,-tagHeight/2,-length), - new Point3(-tagWidth/2,-tagHeight/2,-length)); - - // Project those points - MatOfPoint2f matProjectedPoints = new MatOfPoint2f(); - Calib3d.projectPoints(axis, rvec, tvec, cameraMatrix, new MatOfDouble(), matProjectedPoints); - Point[] projectedPoints = matProjectedPoints.toArray(); - - // Pillars - for(int i = 0; i < 4; i++) - { - Imgproc.line(buf, projectedPoints[i], projectedPoints[i+4], blue, thickness); - } - - // Base lines - //Imgproc.line(buf, projectedPoints[0], projectedPoints[1], blue, thickness); - //Imgproc.line(buf, projectedPoints[1], projectedPoints[2], blue, thickness); - //Imgproc.line(buf, projectedPoints[2], projectedPoints[3], blue, thickness); - //Imgproc.line(buf, projectedPoints[3], projectedPoints[0], blue, thickness); - - // Top lines - Imgproc.line(buf, projectedPoints[4], projectedPoints[5], green, thickness); - Imgproc.line(buf, projectedPoints[5], projectedPoints[6], green, thickness); - Imgproc.line(buf, projectedPoints[6], projectedPoints[7], green, thickness); - Imgproc.line(buf, projectedPoints[4], projectedPoints[7], green, thickness); - } - - /** - * Extracts 6DOF pose from a trapezoid, using a camera intrinsics matrix and the - * original size of the tag. - * - * @param points the points which form the trapezoid - * @param cameraMatrix the camera intrinsics matrix - * @param tagsizeX the original width of the tag - * @param tagsizeY the original height of the tag - * @return the 6DOF pose of the camera relative to the tag - */ - Pose poseFromTrapezoid(Point[] points, Mat cameraMatrix, double tagsizeX , double tagsizeY) - { - // The actual 2d points of the tag detected in the image - MatOfPoint2f points2d = new MatOfPoint2f(points); - - // The 3d points of the tag in an 'ideal projection' - Point3[] arrayPoints3d = new Point3[4]; - arrayPoints3d[0] = new Point3(-tagsizeX/2, tagsizeY/2, 0); - arrayPoints3d[1] = new Point3(tagsizeX/2, tagsizeY/2, 0); - arrayPoints3d[2] = new Point3(tagsizeX/2, -tagsizeY/2, 0); - arrayPoints3d[3] = new Point3(-tagsizeX/2, -tagsizeY/2, 0); - MatOfPoint3f points3d = new MatOfPoint3f(arrayPoints3d); - - // Using this information, actually solve for pose - Pose pose = new Pose(); - Calib3d.solvePnP(points3d, points2d, cameraMatrix, new MatOfDouble(), pose.rvec, pose.tvec, false); - - return pose; - } - - /* - * A simple container to hold both rotation and translation - * vectors, which together form a 6DOF pose. - */ - class Pose - { - Mat rvec; - Mat tvec; - - public Pose() - { - rvec = new Mat(); - tvec = new Mat(); - } - - public Pose(Mat rvec, Mat tvec) - { - this.rvec = rvec; - this.tvec = tvec; - } - } +/* + * Copyright (c) 2021 OpenFTC Team + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.firstinspires.ftc.teamcode; + +import com.qualcomm.robotcore.eventloop.opmode.Disabled; +import org.firstinspires.ftc.robotcore.external.Telemetry; +import org.firstinspires.ftc.robotcore.external.navigation.*; +import org.opencv.calib3d.Calib3d; +import org.opencv.core.*; +import org.opencv.imgproc.Imgproc; +import org.openftc.apriltag.AprilTagDetection; +import org.openftc.apriltag.AprilTagDetectorJNI; +import org.openftc.easyopencv.OpenCvPipeline; + +import java.util.ArrayList; + +@Disabled +public class AprilTagDetectionPipeline extends OpenCvPipeline +{ + // STATIC CONSTANTS + + public static Scalar blue = new Scalar(7,197,235,255); + public static Scalar red = new Scalar(255,0,0,255); + public static Scalar green = new Scalar(0,255,0,255); + public static Scalar white = new Scalar(255,255,255,255); + + static final double FEET_PER_METER = 3.28084; + + // Lens intrinsics + // UNITS ARE PIXELS + // NOTE: this calibration is for the C920 webcam at 800x448. + // You will need to do your own calibration for other configurations! + public static double fx = 578.272; + public static double fy = 578.272; + public static double cx = 402.145; + public static double cy = 221.506; + + // UNITS ARE METERS + public static double TAG_SIZE = 0.166; + + // instance variables + + private long nativeApriltagPtr; + private Mat grey = new Mat(); + private ArrayList detections = new ArrayList<>(); + + private ArrayList detectionsUpdate = new ArrayList<>(); + private final Object detectionsUpdateSync = new Object(); + + Mat cameraMatrix; + + double tagsizeX = TAG_SIZE; + double tagsizeY = TAG_SIZE; + + private float decimation; + private boolean needToSetDecimation; + private final Object decimationSync = new Object(); + + Telemetry telemetry; + + public AprilTagDetectionPipeline(Telemetry telemetry) { + this.telemetry = telemetry; + constructMatrix(); + } + + @Override + public void init(Mat frame) + { + // Allocate a native context object. + nativeApriltagPtr = AprilTagDetectorJNI.createApriltagDetector(AprilTagDetectorJNI.TagFamily.TAG_36h11.string, 3, 3); + } + + @Override + public Mat processFrame(Mat input) + { + // Convert to greyscale + Imgproc.cvtColor(input, grey, Imgproc.COLOR_RGBA2GRAY); + + synchronized (decimationSync) + { + if(needToSetDecimation) + { + AprilTagDetectorJNI.setApriltagDetectorDecimation(nativeApriltagPtr, decimation); + needToSetDecimation = false; + } + } + + // Run AprilTag + detections = AprilTagDetectorJNI.runAprilTagDetectorSimple(nativeApriltagPtr, grey, TAG_SIZE, fx, fy, cx, cy); + + synchronized (detectionsUpdateSync) + { + detectionsUpdate = detections; + } + + // For fun, use OpenCV to draw 6DOF markers on the image. We actually recompute the pose using + // OpenCV because I haven't yet figured out how to re-use AprilTag's pose in OpenCV. + for(AprilTagDetection detection : detections) + { + Pose pose = poseFromTrapezoid(detection.corners, cameraMatrix, tagsizeX, tagsizeY); + drawAxisMarker(input, tagsizeY/2.0, 6, pose.rvec, pose.tvec, cameraMatrix); + draw3dCubeMarker(input, tagsizeX, tagsizeX, tagsizeY, 5, pose.rvec, pose.tvec, cameraMatrix); + + Orientation rot = Orientation.getOrientation(detection.pose.R, AxesReference.INTRINSIC, AxesOrder.YXZ, AngleUnit.DEGREES); + + telemetry.addLine(String.format("\nDetected tag ID=%d", detection.id)); + telemetry.addLine(String.format("Translation X: %.2f feet", detection.pose.x*FEET_PER_METER)); + telemetry.addLine(String.format("Translation Y: %.2f feet", detection.pose.y*FEET_PER_METER)); + telemetry.addLine(String.format("Translation Z: %.2f feet", detection.pose.z*FEET_PER_METER)); + + telemetry.addLine(String.format("Rotation Yaw: %.2f degrees", rot.firstAngle)); + telemetry.addLine(String.format("Rotation Pitch: %.2f degrees", rot.secondAngle)); + telemetry.addLine(String.format("Rotation Roll: %.2f degrees", rot.thirdAngle)); + } + + telemetry.update(); + + return input; + } + + public void setDecimation(float decimation) + { + synchronized (decimationSync) + { + this.decimation = decimation; + needToSetDecimation = true; + } + } + + public ArrayList getLatestDetections() + { + return detections; + } + + public ArrayList getDetectionsUpdate() + { + synchronized (detectionsUpdateSync) + { + ArrayList ret = detectionsUpdate; + detectionsUpdate = null; + return ret; + } + } + + void constructMatrix() + { + // Construct the camera matrix. + // + // -- -- + // | fx 0 cx | + // | 0 fy cy | + // | 0 0 1 | + // -- -- + // + + cameraMatrix = new Mat(3,3, CvType.CV_32FC1); + + cameraMatrix.put(0,0, fx); + cameraMatrix.put(0,1,0); + cameraMatrix.put(0,2, cx); + + cameraMatrix.put(1,0,0); + cameraMatrix.put(1,1,fy); + cameraMatrix.put(1,2,cy); + + cameraMatrix.put(2, 0, 0); + cameraMatrix.put(2,1,0); + cameraMatrix.put(2,2,1); + } + + /** + * Draw a 3D axis marker on a detection. (Similar to what Vuforia does) + * + * @param buf the RGB buffer on which to draw the marker + * @param length the length of each of the marker 'poles' + * @param rvec the rotation vector of the detection + * @param tvec the translation vector of the detection + * @param cameraMatrix the camera matrix used when finding the detection + */ + void drawAxisMarker(Mat buf, double length, int thickness, Mat rvec, Mat tvec, Mat cameraMatrix) + { + // The points in 3D space we wish to project onto the 2D image plane. + // The origin of the coordinate space is assumed to be in the center of the detection. + MatOfPoint3f axis = new MatOfPoint3f( + new Point3(0,0,0), + new Point3(length,0,0), + new Point3(0,length,0), + new Point3(0,0,-length) + ); + + // Project those points + MatOfPoint2f matProjectedPoints = new MatOfPoint2f(); + Calib3d.projectPoints(axis, rvec, tvec, cameraMatrix, new MatOfDouble(), matProjectedPoints); + Point[] projectedPoints = matProjectedPoints.toArray(); + + // Draw the marker! + Imgproc.line(buf, projectedPoints[0], projectedPoints[1], red, thickness); + Imgproc.line(buf, projectedPoints[0], projectedPoints[2], green, thickness); + Imgproc.line(buf, projectedPoints[0], projectedPoints[3], blue, thickness); + + Imgproc.circle(buf, projectedPoints[0], thickness, white, -1); + } + + void draw3dCubeMarker(Mat buf, double length, double tagWidth, double tagHeight, int thickness, Mat rvec, Mat tvec, Mat cameraMatrix) + { + //axis = np.float32([[0,0,0], [0,3,0], [3,3,0], [3,0,0], + // [0,0,-3],[0,3,-3],[3,3,-3],[3,0,-3] ]) + + // The points in 3D space we wish to project onto the 2D image plane. + // The origin of the coordinate space is assumed to be in the center of the detection. + MatOfPoint3f axis = new MatOfPoint3f( + new Point3(-tagWidth/2, tagHeight/2,0), + new Point3( tagWidth/2, tagHeight/2,0), + new Point3( tagWidth/2,-tagHeight/2,0), + new Point3(-tagWidth/2,-tagHeight/2,0), + new Point3(-tagWidth/2, tagHeight/2,-length), + new Point3( tagWidth/2, tagHeight/2,-length), + new Point3( tagWidth/2,-tagHeight/2,-length), + new Point3(-tagWidth/2,-tagHeight/2,-length)); + + // Project those points + MatOfPoint2f matProjectedPoints = new MatOfPoint2f(); + Calib3d.projectPoints(axis, rvec, tvec, cameraMatrix, new MatOfDouble(), matProjectedPoints); + Point[] projectedPoints = matProjectedPoints.toArray(); + + // Pillars + for(int i = 0; i < 4; i++) + { + Imgproc.line(buf, projectedPoints[i], projectedPoints[i+4], blue, thickness); + } + + // Base lines + //Imgproc.line(buf, projectedPoints[0], projectedPoints[1], blue, thickness); + //Imgproc.line(buf, projectedPoints[1], projectedPoints[2], blue, thickness); + //Imgproc.line(buf, projectedPoints[2], projectedPoints[3], blue, thickness); + //Imgproc.line(buf, projectedPoints[3], projectedPoints[0], blue, thickness); + + // Top lines + Imgproc.line(buf, projectedPoints[4], projectedPoints[5], green, thickness); + Imgproc.line(buf, projectedPoints[5], projectedPoints[6], green, thickness); + Imgproc.line(buf, projectedPoints[6], projectedPoints[7], green, thickness); + Imgproc.line(buf, projectedPoints[4], projectedPoints[7], green, thickness); + } + + /** + * Extracts 6DOF pose from a trapezoid, using a camera intrinsics matrix and the + * original size of the tag. + * + * @param points the points which form the trapezoid + * @param cameraMatrix the camera intrinsics matrix + * @param tagsizeX the original width of the tag + * @param tagsizeY the original height of the tag + * @return the 6DOF pose of the camera relative to the tag + */ + Pose poseFromTrapezoid(Point[] points, Mat cameraMatrix, double tagsizeX , double tagsizeY) + { + // The actual 2d points of the tag detected in the image + MatOfPoint2f points2d = new MatOfPoint2f(points); + + // The 3d points of the tag in an 'ideal projection' + Point3[] arrayPoints3d = new Point3[4]; + arrayPoints3d[0] = new Point3(-tagsizeX/2, tagsizeY/2, 0); + arrayPoints3d[1] = new Point3(tagsizeX/2, tagsizeY/2, 0); + arrayPoints3d[2] = new Point3(tagsizeX/2, -tagsizeY/2, 0); + arrayPoints3d[3] = new Point3(-tagsizeX/2, -tagsizeY/2, 0); + MatOfPoint3f points3d = new MatOfPoint3f(arrayPoints3d); + + // Using this information, actually solve for pose + Pose pose = new Pose(); + Calib3d.solvePnP(points3d, points2d, cameraMatrix, new MatOfDouble(), pose.rvec, pose.tvec, false); + + return pose; + } + + /* + * A simple container to hold both rotation and translation + * vectors, which together form a 6DOF pose. + */ + class Pose + { + Mat rvec; + Mat tvec; + + public Pose() + { + rvec = new Mat(); + tvec = new Mat(); + } + + public Pose(Mat rvec, Mat tvec) + { + this.rvec = rvec; + this.tvec = tvec; + } + } } \ No newline at end of file diff --git a/Vision/build.gradle b/Vision/build.gradle index 18a23198..ad38e4b1 100644 --- a/Vision/build.gradle +++ b/Vision/build.gradle @@ -1,45 +1,48 @@ -plugins { - id 'kotlin' - id 'maven-publish' -} - -apply from: '../build.common.gradle' - -task sourcesJar(type: Jar) { - from sourceSets.main.allJava - archiveClassifier = "sources" -} - -publishing { - publications { - mavenJava(MavenPublication) { - from components.java - artifact sourcesJar - } - } -} - -configurations.all { - resolutionStrategy { - cacheChangingModulesFor 0, 'seconds' - } -} - -dependencies { - implementation project(':Common') - - implementation "com.github.deltacv.AprilTagDesktop:AprilTagDesktop:$apriltag_plugin_version" - - api "org.openpnp:opencv:$opencv_version" - - implementation "org.slf4j:slf4j-api:$slf4j_version" - implementation 'org.jetbrains.kotlin:kotlin-stdlib' - - // Compatibility: Skiko supports many platforms but we will only be adding - // those that are supported by AprilTagDesktop as well - - implementation("org.jetbrains.skiko:skiko-awt-runtime-windows-x64:$skiko_version") - implementation("org.jetbrains.skiko:skiko-awt-runtime-linux-x64:$skiko_version") - implementation("org.jetbrains.skiko:skiko-awt-runtime-macos-x64:$skiko_version") - implementation("org.jetbrains.skiko:skiko-awt-runtime-macos-arm64:$skiko_version") +plugins { + id 'kotlin' + id 'maven-publish' +} + +apply from: '../build.common.gradle' + +task sourcesJar(type: Jar) { + from sourceSets.main.allJava + archiveClassifier = "sources" +} + +publishing { + publications { + mavenJava(MavenPublication) { + from components.java + artifact sourcesJar + } + } +} + +configurations.all { + resolutionStrategy { + cacheChangingModulesFor 0, 'seconds' + } +} + +dependencies { + implementation project(':Common') + + api("com.github.deltacv:AprilTagDesktop:$apriltag_plugin_version") { + exclude group: 'com.github.deltacv.EOCVSim', module: 'Common' + } + + api "org.openpnp:opencv:$opencv_version" + + implementation "org.slf4j:slf4j-api:$slf4j_version" + implementation 'org.jetbrains.kotlin:kotlin-stdlib' + + // Compatibility: Skiko supports many platforms but we will only be adding + // those that are supported by AprilTagDesktop as well + + implementation("org.jetbrains.skiko:skiko-awt-runtime-windows-x64:$skiko_version") + implementation("org.jetbrains.skiko:skiko-awt-runtime-linux-x64:$skiko_version") + implementation("org.jetbrains.skiko:skiko-awt-runtime-linux-arm64:$skiko_version") + implementation("org.jetbrains.skiko:skiko-awt-runtime-macos-x64:$skiko_version") + implementation("org.jetbrains.skiko:skiko-awt-runtime-macos-arm64:$skiko_version") } \ No newline at end of file diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/FrameReceiverOpenCvCamera.java b/Vision/src/main/java/io/github/deltacv/vision/external/FrameReceiverOpenCvCamera.java index 5d37e5f7..91199b07 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/external/FrameReceiverOpenCvCamera.java +++ b/Vision/src/main/java/io/github/deltacv/vision/external/FrameReceiverOpenCvCamera.java @@ -1,174 +1,221 @@ -/* - * Copyright (c) 2023 OpenFTC & EOCV-Sim implementation by Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package io.github.deltacv.vision.external; - -import io.github.deltacv.vision.external.source.VisionSource; -import io.github.deltacv.vision.external.source.FrameReceiver; -import org.opencv.core.Core; -import org.opencv.core.Mat; -import org.opencv.core.Size; -import org.openftc.easyopencv.*; - -public class FrameReceiverOpenCvCamera extends OpenCvCameraBase implements OpenCvWebcam, FrameReceiver { - - private final VisionSource source; - OpenCvViewport handedViewport; - - boolean streaming = false; - - public FrameReceiverOpenCvCamera(VisionSource source, OpenCvViewport handedViewport, boolean viewportEnabled) { - super(handedViewport, viewportEnabled); - - this.source = source; - this.handedViewport = handedViewport; - } - - @Override - public int openCameraDevice() { - prepareForOpenCameraDevice(); - - return source.init(); - } - - @Override - public void openCameraDeviceAsync(AsyncCameraOpenListener cameraOpenListener) { - new Thread(() -> { - int code = openCameraDevice(); - - if(code == 0) { - cameraOpenListener.onOpened(); - } else { - cameraOpenListener.onError(code); - } - }).start(); - } - - @Override - public void closeCameraDevice() { - synchronized (source) { - source.close(); - } - } - - @Override - public void closeCameraDeviceAsync(AsyncCameraCloseListener cameraCloseListener) { - new Thread(() -> { - closeCameraDevice(); - cameraCloseListener.onClose(); - }).start(); - } - - @Override - public void startStreaming(int width, int height) { - startStreaming(width, height, getDefaultRotation()); - } - - @Override - public void startStreaming(int width, int height, OpenCvCameraRotation rotation) { - prepareForStartStreaming(width, height, rotation); - - synchronized (source) { - source.start(new Size(width, height)); - source.attach(this); - } - - streaming = true; - } - - @Override - public void stopStreaming() { - source.stop(); - cleanupForEndStreaming(); - } - - @Override - protected OpenCvCameraRotation getDefaultRotation() { - return OpenCvCameraRotation.UPRIGHT; - } - - @Override - protected int mapRotationEnumToOpenCvRotateCode(OpenCvCameraRotation rotation) - { - /* - * The camera sensor in a webcam is mounted in the logical manner, such - * that the raw image is upright when the webcam is used in its "normal" - * orientation. However, if the user is using it in any other orientation, - * we need to manually rotate the image. - */ - - if(rotation == OpenCvCameraRotation.SENSOR_NATIVE) - { - return -1; - } - else if(rotation == OpenCvCameraRotation.SIDEWAYS_LEFT) - { - return Core.ROTATE_90_COUNTERCLOCKWISE; - } - else if(rotation == OpenCvCameraRotation.SIDEWAYS_RIGHT) - { - return Core.ROTATE_90_CLOCKWISE; - } - else if(rotation == OpenCvCameraRotation.UPSIDE_DOWN) - { - return Core.ROTATE_180; - } - else - { - return -1; - } - } - - @Override - protected boolean cameraOrientationIsTiedToDeviceOrientation() { - return false; - } - - @Override - protected boolean isStreaming() { - return streaming; - } - - @Override - public void onFrameStart() { - if(!isStreaming()) return; - - notifyStartOfFrameProcessing(); - } - - // - // Inheritance from Sourced - // - @Override - public void onNewFrame(Mat frame, long timestamp) { - if(!isStreaming()) return; - if(frame == null || frame.empty()) return; - - handleFrameUserCrashable(frame, timestamp); - } - - @Override - public void stop() { - closeCameraDevice(); - } +/* + * Copyright (c) 2023 OpenFTC & EOCV-Sim implementation by Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package io.github.deltacv.vision.external; + +import io.github.deltacv.vision.external.source.VisionSource; +import io.github.deltacv.vision.external.source.FrameReceiver; +import org.firstinspires.ftc.robotcore.external.hardware.camera.CameraControls; +import org.firstinspires.ftc.robotcore.external.hardware.camera.controls.*; +import org.firstinspires.ftc.robotcore.internal.camera.calibration.CameraCalibrationIdentity; +import org.opencv.core.Core; +import org.opencv.core.Mat; +import org.opencv.core.Size; +import org.openftc.easyopencv.*; + +public class FrameReceiverOpenCvCamera extends OpenCvCameraBase implements OpenCvWebcam, FrameReceiver { + + private final VisionSource source; + OpenCvViewport handedViewport; + + boolean streaming = false; + + public FrameReceiverOpenCvCamera(VisionSource source, OpenCvViewport handedViewport, boolean viewportEnabled) { + super(handedViewport, viewportEnabled); + + this.source = source; + this.handedViewport = handedViewport; + } + + @Override + public int openCameraDevice() { + prepareForOpenCameraDevice(); + + return source.init(); + } + + @Override + public void openCameraDeviceAsync(AsyncCameraOpenListener cameraOpenListener) { + new Thread(() -> { + int code = openCameraDevice(); + + if(code == 0) { + cameraOpenListener.onOpened(); + } else { + cameraOpenListener.onError(code); + } + }).start(); + } + + @Override + public void closeCameraDevice() { + synchronized (source) { + source.close(); + } + } + + @Override + public void closeCameraDeviceAsync(AsyncCameraCloseListener cameraCloseListener) { + new Thread(() -> { + closeCameraDevice(); + cameraCloseListener.onClose(); + }).start(); + } + + @Override + public void startStreaming(int width, int height) { + startStreaming(width, height, getDefaultRotation()); + } + + @Override + public void startStreaming(int width, int height, OpenCvCameraRotation rotation) { + prepareForStartStreaming(width, height, rotation); + + synchronized (source) { + source.start(new Size(width, height)); + source.attach(this); + } + + streaming = true; + } + + @Override + public void stopStreaming() { + source.stop(); + cleanupForEndStreaming(); + } + + + @Override + protected OpenCvCameraRotation getDefaultRotation() { + return OpenCvCameraRotation.UPRIGHT; + } + + @Override + protected int mapRotationEnumToOpenCvRotateCode(OpenCvCameraRotation rotation) + { + /* + * The camera sensor in a webcam is mounted in the logical manner, such + * that the raw image is upright when the webcam is used in its "normal" + * orientation. However, if the user is using it in any other orientation, + * we need to manually rotate the image. + */ + + if(rotation == OpenCvCameraRotation.SENSOR_NATIVE) + { + return -1; + } + else if(rotation == OpenCvCameraRotation.SIDEWAYS_LEFT) + { + return Core.ROTATE_90_COUNTERCLOCKWISE; + } + else if(rotation == OpenCvCameraRotation.SIDEWAYS_RIGHT) + { + return Core.ROTATE_90_CLOCKWISE; + } + else if(rotation == OpenCvCameraRotation.UPSIDE_DOWN) + { + return Core.ROTATE_180; + } + else + { + return -1; + } + } + + @Override + protected boolean cameraOrientationIsTiedToDeviceOrientation() { + return false; + } + + @Override + protected boolean isStreaming() { + return streaming; + } + + @Override + public void onFrameStart() { + if(!isStreaming()) return; + + notifyStartOfFrameProcessing(); + } + + @Override + public void setMillisecondsPermissionTimeout(int ms) { } + + @Override + public void startStreaming(int width, int height, OpenCvCameraRotation rotation, StreamFormat streamFormat) { + startStreaming(width, height, rotation); + } + + @Override + public ExposureControl getExposureControl() { + return getControl(ExposureControl.class); + } + + @Override + public FocusControl getFocusControl() { + return getControl(FocusControl.class); + } + + @Override + public PtzControl getPtzControl() { + return getControl(PtzControl.class); + } + + @Override + public GainControl getGainControl() { + return getControl(GainControl.class); + } + + @Override + public WhiteBalanceControl getWhiteBalanceControl() { + return getControl(WhiteBalanceControl.class); + } + + @Override + public T getControl(Class controlType) { + return source.getControlMap().get(controlType); + } + + @Override + public CameraCalibrationIdentity getCalibrationIdentity() { + return null; // TODO + } + + // + // Inheritance from Sourced + // + @Override + public void onNewFrame(Mat frame, long timestamp) { + if(!isStreaming()) return; + if(frame == null || frame.empty()) return; + + handleFrameUserCrashable(frame, timestamp); + } + + @Override + public void stop() { + closeCameraDevice(); + } } \ No newline at end of file diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/source/CameraControlMap.java b/Vision/src/main/java/io/github/deltacv/vision/external/source/CameraControlMap.java new file mode 100644 index 00000000..b343037b --- /dev/null +++ b/Vision/src/main/java/io/github/deltacv/vision/external/source/CameraControlMap.java @@ -0,0 +1,9 @@ +package io.github.deltacv.vision.external.source; + +import org.firstinspires.ftc.robotcore.external.hardware.camera.controls.CameraControl; + +public interface CameraControlMap { + + T get(Class classType); + +} diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSource.java b/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSource.java index 5efe9d0e..e7a8afea 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSource.java +++ b/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSource.java @@ -1,41 +1,43 @@ -/* - * Copyright (c) 2023 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package io.github.deltacv.vision.external.source; - -import org.opencv.core.Size; - -public interface VisionSource { - - int init(); - - boolean start(Size requestedSize); - - boolean attach(FrameReceiver sourced); - boolean remove(FrameReceiver sourced); - - boolean stop(); - - boolean close(); - -} +/* + * Copyright (c) 2023 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package io.github.deltacv.vision.external.source; + +import org.opencv.core.Size; + +public interface VisionSource { + + int init(); + + CameraControlMap getControlMap(); + + boolean start(Size requestedSize); + + boolean attach(FrameReceiver sourced); + boolean remove(FrameReceiver sourced); + + boolean stop(); + + boolean close(); + +} diff --git a/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/controls/ExposureControl.java b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/controls/ExposureControl.java new file mode 100644 index 00000000..da28eb73 --- /dev/null +++ b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/controls/ExposureControl.java @@ -0,0 +1,142 @@ +/* +Copyright (c) 2018 Robert Atkinson + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted (subject to the limitations in the disclaimer below) provided that +the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Neither the name of Robert Atkinson nor the names of his contributors may be used to +endorse or promote products derived from this software without specific prior +written permission. + +NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS +LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +package org.firstinspires.ftc.robotcore.external.hardware.camera.controls; + +import org.firstinspires.ftc.robotcore.internal.collections.MutableReference; + +import java.util.concurrent.TimeUnit; + +public interface ExposureControl extends CameraControl +{ + //---------------------------------------------------------------------------------------------- + // Focus mode + //---------------------------------------------------------------------------------------------- + + enum Mode + { + Unknown, + Auto, // single trigger auto exposure + ContinuousAuto, // continuous auto exposure + Manual, + ShutterPriority, + AperturePriority; + + public static Mode fromId(int id) + { + if (id >= 0 && id < values().length) + { + return values()[id]; + } + return Unknown; + } + } + + /** + * Get the current exposure mode of the camera + * @return the current exposure mode of the camera + */ + Mode getMode(); + + /** + * Set the exposure mode of the camera + * @param mode the mode to enter + * @return whether the operation was successful + */ + boolean setMode(Mode mode); + + /** + * Check whether the camera supports a given exposure mode + * @param mode the mode in question + * @return whether the mode in question is supported + */ + boolean isModeSupported(Mode mode); + + //---------------------------------------------------------------------------------------------- + // Exposure duration + //---------------------------------------------------------------------------------------------- + + long unknownExposure = 0; + + /** + * Get the shortest exposure time supported by the camera + * @param resultUnit time units of your choosing + * @return the shortest supported exposure time, or 0 if unavailable + */ + long getMinExposure(TimeUnit resultUnit); + + /** + * Get the longest exposure time supported by the camera + * @param resultUnit time units of your choosing + * @return the longest supported exposure time, or 0 if unavailable + */ + long getMaxExposure(TimeUnit resultUnit); + + /** + * Get the camera's current exposure time + * @param resultUnit time units of your choosing + * @return the camera's current exposure time, or 0 if unavailable + */ + long getExposure(TimeUnit resultUnit); + + /** unknownExposure is returned if exposure unavailable */ + @Deprecated + long getCachedExposure(final TimeUnit resultUnit, MutableReference refreshed, final long permittedStaleness, final TimeUnit permittedStalenessUnit); + + /** + * Set the camera's exposure time. Only works if you're in manual mode. + * @param duration exposure time + * @param durationUnit time units of your choice + * @return whether the operation succeeded + */ + boolean setExposure(long duration, TimeUnit durationUnit); + + /** + * Check whether your camera supports control over exposure + * @return whether your camera supports control over exposure + */ + boolean isExposureSupported(); + + /** + * Check whether AE priority is enabled + * @return whether AE priority is enabled + */ + boolean getAePriority(); + + /** + * Chooses whether the camera may vary the frame rate for exposure control reasons. + * A priority value of false means the camera may not vary its frame rate. A value of true means the frame rate is variable + * This setting has no effect outside of the auto and shutter_priority auto-exposure modes. + * @return whether the operation was successful + */ + boolean setAePriority(boolean priority); +} diff --git a/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/controls/FocusControl.java b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/controls/FocusControl.java new file mode 100644 index 00000000..87c56189 --- /dev/null +++ b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/controls/FocusControl.java @@ -0,0 +1,117 @@ +/* +Copyright (c) 2018 Robert Atkinson + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted (subject to the limitations in the disclaimer below) provided that +the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Neither the name of Robert Atkinson nor the names of his contributors may be used to +endorse or promote products derived from this software without specific prior +written permission. + +NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS +LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +package org.firstinspires.ftc.robotcore.external.hardware.camera.controls; + +public interface FocusControl extends CameraControl +{ + //---------------------------------------------------------------------------------------------- + // Focus mode + //---------------------------------------------------------------------------------------------- + + enum Mode + { + Unknown, + Auto, // single trigger auto focus + ContinuousAuto, // continuous auto focus + Macro, // + Infinity, // + Fixed; // Fixed focus that can't be adjusted + + public static FocusControl.Mode fromId(int index) + { + if (index >= 0 && index < values().length) + { + return values()[index]; + } + return Unknown; + } + } + + /** + * Get the current focus mode of the camera + * @return the current focus mode of the camera + */ + Mode getMode(); + + + /** + * Set the focus mode of the camera + * @param mode the mode to enter + * @return whether the operation was successful + */ + boolean setMode(Mode mode); + + /** + * Check whether the camera supports a given focus mode + * @param mode the mode in question + * @return whether the mode in question is supported + */ + boolean isModeSupported(Mode mode); + + //---------------------------------------------------------------------------------------------- + // Focus length: units are in mm + //---------------------------------------------------------------------------------------------- + + double unknownFocusLength = -1.0; + + /** + * Get the minimum focal distance supported by the camera + * @return the minimum focal distance supported, or 0 if unavailable + */ + double getMinFocusLength(); + + /** + * Get the maximum focal distance supported by the camera + * @return the maximum focal distance supported, or 0 if unavailable + */ + double getMaxFocusLength(); + + /** + * Get the camera's current focus length + * @return the camera's current focus length, or 0 if unavailable + */ + double getFocusLength(); + + /** + * Set the camera's focus length. Only works if you're in fixed mode. + * @param focusLength the desired focus length + * @return whether the operation succeeded + */ + boolean setFocusLength(double focusLength); + + /** + * Check whether your camera supports control over focus + * @return whether your camera supports control over focus + */ + boolean isFocusLengthSupported(); +} diff --git a/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/controls/GainControl.java b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/controls/GainControl.java new file mode 100644 index 00000000..2d0b08d3 --- /dev/null +++ b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/controls/GainControl.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2020 Michael Hoogasian + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted (subject to the limitations in the disclaimer below) provided that + * the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * Neither the name of Michael Hoogasian nor the names of his contributors may be used to + * endorse or promote products derived from this software without specific prior + * written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS + * LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.firstinspires.ftc.robotcore.external.hardware.camera.controls; + +public interface GainControl extends CameraControl +{ + /*** + * Get the minimum supported gain for this camera + * + * @return the minimum supported gain for this camera + */ + int getMinGain(); + + /*** + * Get the maximum supported gain for this camera + * + * @return the maximum supported gain for this camera + */ + int getMaxGain(); + + /*** + * Get the current gain of this camera + * + * @return the current gain of this camera + */ + int getGain(); + + /*** + * Set the gain for this camera. + * Gain can be adjusted only if ExposureControl Mode is set to MANUAL (typically not the default) + * Further, gain will be actively managed if in AUTO exposure mode. + * + * @param gain the gain to set + * @return whether the operation was successful or not + */ + boolean setGain(int gain); +} diff --git a/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/controls/PtzControl.java b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/controls/PtzControl.java new file mode 100644 index 00000000..d8da2cdc --- /dev/null +++ b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/controls/PtzControl.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2020 Michael Hoogasian + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted (subject to the limitations in the disclaimer below) provided that + * the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * Neither the name of Michael Hoogasian nor the names of his contributors may be used to + * endorse or promote products derived from this software without specific prior + * written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS + * LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + + +package org.firstinspires.ftc.robotcore.external.hardware.camera.controls; + +public interface PtzControl extends CameraControl +{ + class PanTiltHolder + { + public int pan; + public int tilt; + } + + /** + * Gets the current virtual pan/tilt of the camera + * + * @return the current virtual pan/tilt of the camera + */ + PanTiltHolder getPanTilt(); + + /** + * Set's the camera's virtual pan/tilt + * + * @param panTiltHolder the virtual pan/tilt the camera should assume + * @return whether the operation was successful + */ + boolean setPanTilt(PanTiltHolder panTiltHolder); + + /** + * Gets the minimum virtual pan/tilt values of the camera + * + * @return the minimum virtual pan/tilt values of the camera + */ + PanTiltHolder getMinPanTilt(); + + /** + * Gets the maximum virtual pan/tilt values of the camera + * + * @return the maximum virtual pan/tilt values of the camera + */ + PanTiltHolder getMaxPanTilt(); + + /** + * Gets the current virtual zoom level of the camera + * + * @return the current virtual zoom level of the camera + */ + int getZoom(); + + /** + * Sets the camera's virtual zoom level + * + * @param zoom the zoom level the camera should assume + * @return whether the operation was successful + */ + boolean setZoom(int zoom); + + /** + * Gets the minimum virtual zoom level of the camera + * + * @return the minimum virtual zoom level of the camera + */ + int getMinZoom(); + + /** + * Gets the maximum virtual zoom level of the camera + * + * @return the maximum virtual zoom level of the camera + */ + int getMaxZoom(); +} \ No newline at end of file diff --git a/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/controls/WhiteBalanceControl.java b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/controls/WhiteBalanceControl.java new file mode 100644 index 00000000..565a7210 --- /dev/null +++ b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/controls/WhiteBalanceControl.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2021 Michael Hoogasian + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted (subject to the limitations in the disclaimer below) provided that + * the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * Neither the name of Michael Hoogasian nor the names of his contributors may be used to + * endorse or promote products derived from this software without specific prior + * written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS + * LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.firstinspires.ftc.robotcore.external.hardware.camera.controls; + +public interface WhiteBalanceControl extends CameraControl +{ + enum Mode /* If you change this order, remember to update jni_devicehandle.cpp! */ + { + UNKNOWN, + AUTO, + MANUAL + } + + /** + * Get the current white balance mode of the camera + * @return the current white balance mode of the camera + */ + Mode getMode(); + + /** + * Set the white balance mode for the camera + * @param mode the mode to enter + * @return whether the operation was successful + */ + boolean setMode(Mode mode); + + /*** + * Get the minimum white balance temperature for this camera + * + * @return min white balance temp, in degrees Kelvin + */ + int getMinWhiteBalanceTemperature(); + + /*** + * Get the maximum white balance temperature for this camera + * + * @return max white balance temp, in degrees Kelvin + */ + int getMaxWhiteBalanceTemperature(); + + /*** + * Get the current white balance temperature for this camera + * + * @return current white balance temp, in degrees Kelvin + */ + int getWhiteBalanceTemperature(); + + /*** + * Set the white balance temperature for this camera + * + * @param temperature temperature to set, in degrees Kelvin + * @return whether the operation was successful + */ + boolean setWhiteBalanceTemperature(int temperature); +} diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.java b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.java index a0455ccd..ce1275e7 100644 --- a/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.java +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.java @@ -1,412 +1,419 @@ -/* - * Copyright (c) 2023 FIRST - * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted (subject to the limitations in the disclaimer below) provided that - * the following conditions are met: - * - * Redistributions of source code must retain the above copyright notice, this list - * of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, this - * list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * Neither the name of FIRST nor the names of its contributors may be used to - * endorse or promote products derived from this software without specific prior - * written permission. - * - * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS - * LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, - * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE - * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR - * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF - * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package org.firstinspires.ftc.vision; - -import android.graphics.Canvas; -import android.util.Size; - -import com.qualcomm.robotcore.util.RobotLog; - -import io.github.deltacv.vision.internal.source.ftc.SourcedCameraName; -import org.firstinspires.ftc.robotcore.external.hardware.camera.CameraName; -import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; -import org.firstinspires.ftc.robotcore.external.hardware.camera.controls.CameraControl; -import org.firstinspires.ftc.robotcore.internal.camera.calibration.CameraCalibration; -import org.opencv.core.Mat; -import org.openftc.easyopencv.OpenCvCamera; -import org.openftc.easyopencv.OpenCvCameraFactory; -import org.openftc.easyopencv.OpenCvCameraRotation; -import org.openftc.easyopencv.OpenCvWebcam; -import org.openftc.easyopencv.TimestampedOpenCvPipeline; - -public class VisionPortalImpl extends VisionPortal -{ - protected OpenCvCamera camera; - protected volatile CameraState cameraState = CameraState.CAMERA_DEVICE_CLOSED; - protected VisionProcessor[] processors; - protected volatile boolean[] processorsEnabled; - protected volatile CameraCalibration calibration; - protected final boolean autoPauseCameraMonitor; - protected final Object userStateMtx = new Object(); - protected final Size cameraResolution; - protected final StreamFormat webcamStreamFormat; - protected static final OpenCvCameraRotation CAMERA_ROTATION = OpenCvCameraRotation.SENSOR_NATIVE; - protected String captureNextFrame; - protected final Object captureFrameMtx = new Object(); - - public VisionPortalImpl(CameraName camera, int cameraMonitorViewId, boolean autoPauseCameraMonitor, Size cameraResolution, StreamFormat webcamStreamFormat, VisionProcessor[] processors) - { - this.processors = processors; - this.cameraResolution = cameraResolution; - this.webcamStreamFormat = webcamStreamFormat; - processorsEnabled = new boolean[processors.length]; - - for (int i = 0; i < processors.length; i++) - { - processorsEnabled[i] = true; - } - - this.autoPauseCameraMonitor = autoPauseCameraMonitor; - - createCamera(camera, cameraMonitorViewId); - startCamera(); - } - - protected void startCamera() - { - if (camera == null) - { - throw new IllegalStateException("This should never happen"); - } - - if (cameraResolution == null) // was the user a silly silly - { - throw new IllegalArgumentException("parameters.cameraResolution == null"); - } - - camera.setViewportRenderer(OpenCvCamera.ViewportRenderer.NATIVE_VIEW); - - if(!(camera instanceof OpenCvWebcam)) - { - camera.setViewportRenderingPolicy(OpenCvCamera.ViewportRenderingPolicy.OPTIMIZE_VIEW); - } - - cameraState = CameraState.OPENING_CAMERA_DEVICE; - camera.openCameraDeviceAsync(new OpenCvCamera.AsyncCameraOpenListener() - { - @Override - public void onOpened() - { - cameraState = CameraState.CAMERA_DEVICE_READY; - cameraState = CameraState.STARTING_STREAM; - - camera.startStreaming(cameraResolution.getWidth(), cameraResolution.getHeight(), CAMERA_ROTATION); - - camera.setPipeline(new ProcessingPipeline()); - cameraState = CameraState.STREAMING; - } - - @Override - public void onError(int errorCode) - { - cameraState = CameraState.ERROR; - RobotLog.ee("VisionPortalImpl", "Camera opening failed."); - } - }); - } - - protected void createCamera(CameraName cameraName, int cameraMonitorViewId) - { - if (cameraName == null) // was the user a silly silly - { - throw new IllegalArgumentException("parameters.camera == null"); - } - else if (cameraName instanceof SourcedCameraName) // Webcams - { - camera = OpenCvCameraFactory.getInstance().createWebcam((WebcamName) cameraName, cameraMonitorViewId); - } - else // ¯\_(ツ)_/¯ - { - throw new IllegalArgumentException("Unknown camera name"); - } - } - - @Override - public void setProcessorEnabled(VisionProcessor processor, boolean enabled) - { - int numProcessorsEnabled = 0; - boolean ok = false; - - for (int i = 0; i < processors.length; i++) - { - if (processor == processors[i]) - { - processorsEnabled[i] = enabled; - ok = true; - } - - if (processorsEnabled[i]) - { - numProcessorsEnabled++; - } - } - - if (ok) - { - if (autoPauseCameraMonitor) - { - if (numProcessorsEnabled == 0) - { - camera.pauseViewport(); - } - else - { - camera.resumeViewport(); - } - } - } - else - { - throw new IllegalArgumentException("Processor not attached to this helper!"); - } - } - - @Override - public boolean getProcessorEnabled(VisionProcessor processor) - { - for (int i = 0; i < processors.length; i++) - { - if (processor == processors[i]) - { - return processorsEnabled[i]; - } - } - - throw new IllegalArgumentException("Processor not attached to this helper!"); - } - - @Override - public CameraState getCameraState() - { - return cameraState; - } - - @Override - public void setActiveCamera(WebcamName webcamName) - { - throw new UnsupportedOperationException("setActiveCamera is only supported for switchable webcams"); - } - - @Override - public WebcamName getActiveCamera() - { - throw new UnsupportedOperationException("getActiveCamera is only supported for switchable webcams"); - } - - @Override - public T getCameraControl(Class controlType) - { - if (cameraState == CameraState.STREAMING) - { - throw new UnsupportedOperationException("Getting controls is not yet supported in EOCV-Sim"); - } - else - { - throw new IllegalStateException("You cannot use camera controls until the camera is streaming"); - } - } - - class ProcessingPipeline extends TimestampedOpenCvPipeline - { - @Override - public void init(Mat firstFrame) - { - for (VisionProcessor processor : processors) - { - processor.init(firstFrame.width(), firstFrame.height(), calibration); - } - } - - @Override - public Mat processFrame(Mat input, long captureTimeNanos) - { - synchronized (captureFrameMtx) - { - if (captureNextFrame != null) - { - // saveMatToDiskFullPath(input, "/sdcard/VisionPortal-" + captureNextFrame + ".png"); - } - - captureNextFrame = null; - } - - Object[] processorDrawCtxes = new Object[processors.length]; // cannot re-use frome to frame - - for (int i = 0; i < processors.length; i++) - { - if (processorsEnabled[i]) - { - processorDrawCtxes[i] = processors[i].processFrame(input, captureTimeNanos); - } - } - - requestViewportDrawHook(processorDrawCtxes); - - return input; - } - - @Override - public void onDrawFrame(Canvas canvas, int onscreenWidth, int onscreenHeight, float scaleBmpPxToCanvasPx, float scaleCanvasDensity, Object userContext) - { - Object[] ctx = (Object[]) userContext; - - for (int i = 0; i < processors.length; i++) - { - if (processorsEnabled[i]) - { - processors[i].onDrawFrame(canvas, onscreenWidth, onscreenHeight, scaleBmpPxToCanvasPx, scaleCanvasDensity, ctx[i]); - } - } - } - } - - @Override - public void saveNextFrameRaw(String filepath) - { - synchronized (captureFrameMtx) - { - captureNextFrame = filepath; - } - } - - @Override - public void stopStreaming() - { - synchronized (userStateMtx) - { - if (cameraState == CameraState.STREAMING || cameraState == CameraState.STARTING_STREAM) - { - cameraState = CameraState.STOPPING_STREAM; - new Thread(() -> - { - synchronized (userStateMtx) - { - camera.stopStreaming(); - cameraState = CameraState.CAMERA_DEVICE_READY; - } - }).start(); - } - else if (cameraState == CameraState.STOPPING_STREAM - || cameraState == CameraState.CAMERA_DEVICE_READY - || cameraState == CameraState.CLOSING_CAMERA_DEVICE) - { - // be idempotent - } - else - { - throw new RuntimeException("Illegal CameraState when calling stopStreaming()"); - } - } - } - - @Override - public void resumeStreaming() - { - synchronized (userStateMtx) - { - if (cameraState == CameraState.CAMERA_DEVICE_READY || cameraState == CameraState.STOPPING_STREAM) - { - cameraState = CameraState.STARTING_STREAM; - new Thread(() -> - { - synchronized (userStateMtx) - { - if (camera instanceof OpenCvWebcam) - { - ((OpenCvWebcam)camera).startStreaming(cameraResolution.getWidth(), cameraResolution.getHeight(), CAMERA_ROTATION, webcamStreamFormat.eocvStreamFormat); - } - else - { - camera.startStreaming(cameraResolution.getWidth(), cameraResolution.getHeight(), CAMERA_ROTATION); - } - cameraState = CameraState.STREAMING; - } - }).start(); - } - else if (cameraState == CameraState.STREAMING - || cameraState == CameraState.STARTING_STREAM - || cameraState == CameraState.OPENING_CAMERA_DEVICE) // we start streaming automatically after we open - { - // be idempotent - } - else - { - throw new RuntimeException("Illegal CameraState when calling stopStreaming()"); - } - } - } - - @Override - public void stopLiveView() - { - OpenCvCamera cameraSafe = camera; - - if (cameraSafe != null) - { - camera.pauseViewport(); - } - } - - @Override - public void resumeLiveView() - { - OpenCvCamera cameraSafe = camera; - - if (cameraSafe != null) - { - camera.resumeViewport(); - } - } - - @Override - public float getFps() - { - OpenCvCamera cameraSafe = camera; - - if (cameraSafe != null) - { - return cameraSafe.getFps(); - } - else - { - return 0; - } - } - - @Override - public void close() - { - synchronized (userStateMtx) - { - cameraState = CameraState.CLOSING_CAMERA_DEVICE; - - if (camera != null) - { - camera.closeCameraDeviceAsync(() -> cameraState = CameraState.CAMERA_DEVICE_CLOSED); - } - - camera = null; - } - } +/* + * Copyright (c) 2023 FIRST + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted (subject to the limitations in the disclaimer below) provided that + * the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * Neither the name of FIRST nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior + * written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS + * LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.firstinspires.ftc.vision; + +import android.graphics.Canvas; +import android.util.Size; + +import com.qualcomm.robotcore.util.RobotLog; + +import io.github.deltacv.vision.internal.source.ftc.SourcedCameraName; +import org.firstinspires.ftc.robotcore.external.hardware.camera.CameraName; +import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; +import org.firstinspires.ftc.robotcore.external.hardware.camera.controls.CameraControl; +import org.firstinspires.ftc.robotcore.internal.camera.calibration.CameraCalibration; +import org.opencv.core.Mat; +import org.openftc.easyopencv.OpenCvCamera; +import org.openftc.easyopencv.OpenCvCameraFactory; +import org.openftc.easyopencv.OpenCvCameraRotation; +import org.openftc.easyopencv.OpenCvWebcam; +import org.openftc.easyopencv.TimestampedOpenCvPipeline; + +public class VisionPortalImpl extends VisionPortal +{ + protected OpenCvCamera camera; + protected volatile CameraState cameraState = CameraState.CAMERA_DEVICE_CLOSED; + protected VisionProcessor[] processors; + protected volatile boolean[] processorsEnabled; + protected volatile CameraCalibration calibration; + protected final boolean autoPauseCameraMonitor; + protected final Object userStateMtx = new Object(); + protected final Size cameraResolution; + protected final StreamFormat webcamStreamFormat; + protected static final OpenCvCameraRotation CAMERA_ROTATION = OpenCvCameraRotation.SENSOR_NATIVE; + protected String captureNextFrame; + protected final Object captureFrameMtx = new Object(); + + public VisionPortalImpl(CameraName camera, int cameraMonitorViewId, boolean autoPauseCameraMonitor, Size cameraResolution, StreamFormat webcamStreamFormat, VisionProcessor[] processors) + { + this.processors = processors; + this.cameraResolution = cameraResolution; + this.webcamStreamFormat = webcamStreamFormat; + processorsEnabled = new boolean[processors.length]; + + for (int i = 0; i < processors.length; i++) + { + processorsEnabled[i] = true; + } + + this.autoPauseCameraMonitor = autoPauseCameraMonitor; + + createCamera(camera, cameraMonitorViewId); + startCamera(); + } + + protected void startCamera() + { + if (camera == null) + { + throw new IllegalStateException("This should never happen"); + } + + if (cameraResolution == null) // was the user a silly silly + { + throw new IllegalArgumentException("parameters.cameraResolution == null"); + } + + camera.setViewportRenderer(OpenCvCamera.ViewportRenderer.NATIVE_VIEW); + + if(!(camera instanceof OpenCvWebcam)) + { + camera.setViewportRenderingPolicy(OpenCvCamera.ViewportRenderingPolicy.OPTIMIZE_VIEW); + } + + cameraState = CameraState.OPENING_CAMERA_DEVICE; + camera.openCameraDeviceAsync(new OpenCvCamera.AsyncCameraOpenListener() + { + @Override + public void onOpened() + { + cameraState = CameraState.CAMERA_DEVICE_READY; + cameraState = CameraState.STARTING_STREAM; + + camera.startStreaming(cameraResolution.getWidth(), cameraResolution.getHeight(), CAMERA_ROTATION); + + camera.setPipeline(new ProcessingPipeline()); + cameraState = CameraState.STREAMING; + } + + @Override + public void onError(int errorCode) + { + cameraState = CameraState.ERROR; + RobotLog.ee("VisionPortalImpl", "Camera opening failed."); + } + }); + } + + protected void createCamera(CameraName cameraName, int cameraMonitorViewId) + { + if (cameraName == null) // was the user a silly silly + { + throw new IllegalArgumentException("parameters.camera == null"); + } + else if (cameraName instanceof SourcedCameraName) // Webcams + { + camera = OpenCvCameraFactory.getInstance().createWebcam((WebcamName) cameraName, cameraMonitorViewId); + } + else // ¯\_(ツ)_/¯ + { + throw new IllegalArgumentException("Unknown camera name"); + } + } + + @Override + public void setProcessorEnabled(VisionProcessor processor, boolean enabled) + { + int numProcessorsEnabled = 0; + boolean ok = false; + + for (int i = 0; i < processors.length; i++) + { + if (processor == processors[i]) + { + processorsEnabled[i] = enabled; + ok = true; + } + + if (processorsEnabled[i]) + { + numProcessorsEnabled++; + } + } + + if (ok) + { + if (autoPauseCameraMonitor) + { + if (numProcessorsEnabled == 0) + { + camera.pauseViewport(); + } + else + { + camera.resumeViewport(); + } + } + } + else + { + throw new IllegalArgumentException("Processor not attached to this helper!"); + } + } + + @Override + public boolean getProcessorEnabled(VisionProcessor processor) + { + for (int i = 0; i < processors.length; i++) + { + if (processor == processors[i]) + { + return processorsEnabled[i]; + } + } + + throw new IllegalArgumentException("Processor not attached to this helper!"); + } + + @Override + public CameraState getCameraState() + { + return cameraState; + } + + @Override + public void setActiveCamera(WebcamName webcamName) + { + throw new UnsupportedOperationException("setActiveCamera is only supported for switchable webcams"); + } + + @Override + public WebcamName getActiveCamera() + { + throw new UnsupportedOperationException("getActiveCamera is only supported for switchable webcams"); + } + + @Override + public T getCameraControl(Class controlType) + { + if (cameraState == CameraState.STREAMING) + { + if (camera instanceof OpenCvWebcam) + { + return ((OpenCvWebcam) camera).getControl(controlType); + } + else + { + throw new UnsupportedOperationException("Getting controls is only supported for webcams"); + } + } + else + { + throw new IllegalStateException("You cannot use camera controls until the camera is streaming"); + } + } + + class ProcessingPipeline extends TimestampedOpenCvPipeline + { + @Override + public void init(Mat firstFrame) + { + for (VisionProcessor processor : processors) + { + processor.init(firstFrame.width(), firstFrame.height(), calibration); + } + } + + @Override + public Mat processFrame(Mat input, long captureTimeNanos) + { + synchronized (captureFrameMtx) + { + if (captureNextFrame != null) + { + // saveMatToDiskFullPath(input, "/sdcard/VisionPortal-" + captureNextFrame + ".png"); + } + + captureNextFrame = null; + } + + Object[] processorDrawCtxes = new Object[processors.length]; // cannot re-use frome to frame + + for (int i = 0; i < processors.length; i++) + { + if (processorsEnabled[i]) + { + processorDrawCtxes[i] = processors[i].processFrame(input, captureTimeNanos); + } + } + + requestViewportDrawHook(processorDrawCtxes); + + return input; + } + + @Override + public void onDrawFrame(Canvas canvas, int onscreenWidth, int onscreenHeight, float scaleBmpPxToCanvasPx, float scaleCanvasDensity, Object userContext) + { + Object[] ctx = (Object[]) userContext; + + for (int i = 0; i < processors.length; i++) + { + if (processorsEnabled[i]) + { + processors[i].onDrawFrame(canvas, onscreenWidth, onscreenHeight, scaleBmpPxToCanvasPx, scaleCanvasDensity, ctx[i]); + } + } + } + } + + @Override + public void saveNextFrameRaw(String filepath) + { + synchronized (captureFrameMtx) + { + captureNextFrame = filepath; + } + } + + @Override + public void stopStreaming() + { + synchronized (userStateMtx) + { + if (cameraState == CameraState.STREAMING || cameraState == CameraState.STARTING_STREAM) + { + cameraState = CameraState.STOPPING_STREAM; + new Thread(() -> + { + synchronized (userStateMtx) + { + camera.stopStreaming(); + cameraState = CameraState.CAMERA_DEVICE_READY; + } + }).start(); + } + else if (cameraState == CameraState.STOPPING_STREAM + || cameraState == CameraState.CAMERA_DEVICE_READY + || cameraState == CameraState.CLOSING_CAMERA_DEVICE) + { + // be idempotent + } + else + { + throw new RuntimeException("Illegal CameraState when calling stopStreaming()"); + } + } + } + + @Override + public void resumeStreaming() + { + synchronized (userStateMtx) + { + if (cameraState == CameraState.CAMERA_DEVICE_READY || cameraState == CameraState.STOPPING_STREAM) + { + cameraState = CameraState.STARTING_STREAM; + new Thread(() -> + { + synchronized (userStateMtx) + { + if (camera instanceof OpenCvWebcam) + { + ((OpenCvWebcam)camera).startStreaming(cameraResolution.getWidth(), cameraResolution.getHeight(), CAMERA_ROTATION, webcamStreamFormat.eocvStreamFormat); + } + else + { + camera.startStreaming(cameraResolution.getWidth(), cameraResolution.getHeight(), CAMERA_ROTATION); + } + cameraState = CameraState.STREAMING; + } + }).start(); + } + else if (cameraState == CameraState.STREAMING + || cameraState == CameraState.STARTING_STREAM + || cameraState == CameraState.OPENING_CAMERA_DEVICE) // we start streaming automatically after we open + { + // be idempotent + } + else + { + throw new RuntimeException("Illegal CameraState when calling stopStreaming()"); + } + } + } + + @Override + public void stopLiveView() + { + OpenCvCamera cameraSafe = camera; + + if (cameraSafe != null) + { + camera.pauseViewport(); + } + } + + @Override + public void resumeLiveView() + { + OpenCvCamera cameraSafe = camera; + + if (cameraSafe != null) + { + camera.resumeViewport(); + } + } + + @Override + public float getFps() + { + OpenCvCamera cameraSafe = camera; + + if (cameraSafe != null) + { + return cameraSafe.getFps(); + } + else + { + return 0; + } + } + + @Override + public void close() + { + synchronized (userStateMtx) + { + cameraState = CameraState.CLOSING_CAMERA_DEVICE; + + if (camera != null) + { + camera.closeCameraDeviceAsync(() -> cameraState = CameraState.CAMERA_DEVICE_CLOSED); + } + + camera = null; + } + } } \ No newline at end of file diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagProcessorImpl.java b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagProcessorImpl.java index 023284ed..a9fad3ec 100644 --- a/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagProcessorImpl.java +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagProcessorImpl.java @@ -1,612 +1,612 @@ -/* - * Copyright (c) 2023 FIRST - * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted (subject to the limitations in the disclaimer below) provided that - * the following conditions are met: - * - * Redistributions of source code must retain the above copyright notice, this list - * of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, this - * list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * Neither the name of FIRST nor the names of its contributors may be used to - * endorse or promote products derived from this software without specific prior - * written permission. - * - * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS - * LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, - * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE - * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR - * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF - * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package org.firstinspires.ftc.vision.apriltag; - -import android.graphics.Canvas; - -import com.qualcomm.robotcore.util.MovingStatistics; -import com.qualcomm.robotcore.util.RobotLog; - -import org.firstinspires.ftc.robotcore.external.matrices.GeneralMatrixF; -import org.firstinspires.ftc.robotcore.external.matrices.OpenGLMatrix; -import org.firstinspires.ftc.robotcore.external.matrices.VectorF; -import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit; -import org.firstinspires.ftc.robotcore.external.navigation.Pose3D; -import org.firstinspires.ftc.robotcore.external.navigation.AxesOrder; -import org.firstinspires.ftc.robotcore.external.navigation.AxesReference; -import org.firstinspires.ftc.robotcore.external.navigation.DistanceUnit; -import org.firstinspires.ftc.robotcore.external.navigation.Orientation; -import org.firstinspires.ftc.robotcore.external.navigation.Position; -import org.firstinspires.ftc.robotcore.external.navigation.YawPitchRollAngles; -import org.firstinspires.ftc.robotcore.internal.camera.calibration.CameraCalibration; -import org.opencv.calib3d.Calib3d; -import org.opencv.core.CvType; -import org.opencv.core.Mat; -import org.opencv.core.MatOfDouble; -import org.opencv.core.MatOfPoint2f; -import org.opencv.core.MatOfPoint3f; -import org.opencv.core.Point; -import org.opencv.core.Point3; -import org.opencv.imgproc.Imgproc; -import org.openftc.apriltag.AprilTagDetectorJNI; -import org.openftc.apriltag.ApriltagDetectionJNI; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; - -public class AprilTagProcessorImpl extends AprilTagProcessor -{ - public static final String TAG = "AprilTagProcessorImpl"; - - Logger logger = LoggerFactory.getLogger(TAG); - - private long nativeApriltagPtr; - private Mat grey = new Mat(); - private ArrayList detections = new ArrayList<>(); - - private ArrayList detectionsUpdate = new ArrayList<>(); - private final Object detectionsUpdateSync = new Object(); - private boolean drawAxes; - private boolean drawCube; - private boolean drawOutline; - private boolean drawTagID; - - private Mat cameraMatrix; - - private double fx; - private double fy; - private double cx; - private double cy; - private final boolean suppressCalibrationWarnings; - - private final AprilTagLibrary tagLibrary; - - private float decimation; - private boolean needToSetDecimation; - private final Object decimationSync = new Object(); - - private AprilTagCanvasAnnotator canvasAnnotator; - - private final DistanceUnit outputUnitsLength; - private final AngleUnit outputUnitsAngle; - - private volatile PoseSolver poseSolver = PoseSolver.APRILTAG_BUILTIN; - - private OpenGLMatrix robotInCameraFrame; - - public AprilTagProcessorImpl(OpenGLMatrix robotInCameraFrame, double fx, double fy, double cx, double cy, DistanceUnit outputUnitsLength, AngleUnit outputUnitsAngle, AprilTagLibrary tagLibrary, - boolean drawAxes, boolean drawCube, boolean drawOutline, boolean drawTagID, TagFamily tagFamily, int threads, boolean suppressCalibrationWarnings) - { - this.robotInCameraFrame = robotInCameraFrame; - - this.fx = fx; - this.fy = fy; - this.cx = cx; - this.cy = cy; - - this.tagLibrary = tagLibrary; - this.outputUnitsLength = outputUnitsLength; - this.outputUnitsAngle = outputUnitsAngle; - this.suppressCalibrationWarnings = suppressCalibrationWarnings; - this.drawAxes = drawAxes; - this.drawCube = drawCube; - this.drawOutline = drawOutline; - this.drawTagID = drawTagID; - - // Allocate a native context object. See the corresponding deletion in the finalizer - nativeApriltagPtr = AprilTagDetectorJNI.createApriltagDetector(tagFamily.ATLibTF.string, 3, threads); - } - - @Override - protected void finalize() - { - // Might be null if createApriltagDetector() threw an exception - if(nativeApriltagPtr != 0) - { - // Delete the native context we created in the constructor - AprilTagDetectorJNI.releaseApriltagDetector(nativeApriltagPtr); - nativeApriltagPtr = 0; - } - else - { - System.out.println("AprilTagDetectionPipeline.finalize(): nativeApriltagPtr was NULL"); - } - } - - @Override - public void init(int width, int height, CameraCalibration calibration) - { - // ATTEMPT 1 - If the user provided their own calibration, use that - if (fx != 0 && fy != 0 && cx != 0 && cy != 0) - { - logger.debug(String.format("User provided their own camera calibration fx=%7.3f fy=%7.3f cx=%7.3f cy=%7.3f", - fx, fy, cx, cy)); - } - - // ATTEMPT 2 - If we have valid calibration we can use, use it - else if (calibration != null && !calibration.isDegenerate()) // needed because we may get an all zero calibration to indicate none, instead of null - { - fx = calibration.focalLengthX; - fy = calibration.focalLengthY; - cx = calibration.principalPointX; - cy = calibration.principalPointY; - - // Note that this might have been a scaled calibration - inform the user if so - if (calibration.resolutionScaledFrom != null) - { - String msg = String.format("Camera has not been calibrated for [%dx%d]; applying a scaled calibration from [%dx%d].", width, height, calibration.resolutionScaledFrom.getWidth(), calibration.resolutionScaledFrom.getHeight()); - - if (!suppressCalibrationWarnings) - { - logger.warn(msg); - } - } - // Nope, it was a full up proper calibration - no need to pester the user about anything - else - { - logger.debug(String.format("User did not provide a camera calibration; but we DO have a built in calibration we can use.\n [%dx%d] (NOT scaled) %s\nfx=%7.3f fy=%7.3f cx=%7.3f cy=%7.3f", - calibration.getSize().getWidth(), calibration.getSize().getHeight(), calibration.getIdentity().toString(), fx, fy, cx, cy)); - } - } - - // Okay, we aren't going to have any calibration data we can use, but there are 2 cases to check - else - { - // NO-OP, we cannot implement this for EOCV-Sim in the same way as the FTC SDK - - /* - // If we have a calibration on file, but with a wrong aspect ratio, - // we can't use it, but hey at least we can let the user know about it. - if (calibration instanceof PlaceholderCalibratedAspectRatioMismatch) - { - StringBuilder supportedResBuilder = new StringBuilder(); - - for (CameraCalibration cal : CameraCalibrationHelper.getInstance().getCalibrations(calibration.getIdentity())) - { - supportedResBuilder.append(String.format("[%dx%d],", cal.getSize().getWidth(), cal.getSize().getHeight())); - } - - String msg = String.format("Camera has not been calibrated for [%dx%d]. Pose estimates will likely be inaccurate. However, there are built in calibrations for resolutions: %s", - width, height, supportedResBuilder.toString()); - - if (!suppressCalibrationWarnings) - { - logger.warn(msg); - } - - - // Nah, we got absolutely nothing - else*/ - { - String warning = "User did not provide a camera calibration, nor was a built-in calibration found for this camera. Pose estimates will likely be inaccurate."; - - if (!suppressCalibrationWarnings) - { - logger.warn(warning); - } - } - - // IN EITHER CASE, set it to *something* so we don't crash the native code - fx = 578.272; - fy = 578.272; - cx = width/2; - cy = height/2; - } - - constructMatrix(); - canvasAnnotator = new AprilTagCanvasAnnotator(cameraMatrix); - } - - @Override - public Object processFrame(Mat input, long captureTimeNanos) - { - // Convert to greyscale - Imgproc.cvtColor(input, grey, Imgproc.COLOR_RGBA2GRAY); - - synchronized (decimationSync) - { - if(needToSetDecimation) - { - AprilTagDetectorJNI.setApriltagDetectorDecimation(nativeApriltagPtr, decimation); - needToSetDecimation = false; - } - } - - // Run AprilTag - detections = runAprilTagDetectorForMultipleTagSizes(captureTimeNanos); - - synchronized (detectionsUpdateSync) - { - detectionsUpdate = detections; - } - - // TODO do we need to deep copy this so the user can't mess with it before use in onDrawFrame()? - return detections; - } - - private MovingStatistics solveTime = new MovingStatistics(50); - - // We cannot use runAprilTagDetectorSimple because we cannot assume tags are all the same size - ArrayList runAprilTagDetectorForMultipleTagSizes(long captureTimeNanos) - { - long ptrDetectionArray = AprilTagDetectorJNI.runApriltagDetector(nativeApriltagPtr, grey.dataAddr(), grey.width(), grey.height()); - if (ptrDetectionArray != 0) - { - long[] detectionPointers = ApriltagDetectionJNI.getDetectionPointers(ptrDetectionArray); - ArrayList detections = new ArrayList<>(detectionPointers.length); - - for (long ptrDetection : detectionPointers) - { - AprilTagMetadata metadata = tagLibrary.lookupTag(ApriltagDetectionJNI.getId(ptrDetection)); - - double[][] corners = ApriltagDetectionJNI.getCorners(ptrDetection); - - Point[] cornerPts = new Point[4]; - for (int p = 0; p < 4; p++) - { - cornerPts[p] = new Point(corners[p][0], corners[p][1]); - } - - AprilTagPoseRaw rawPose; - AprilTagPoseFtc ftcPose; - Pose3D robotPose; - - if (metadata != null) - { - PoseSolver solver = poseSolver; // snapshot, can change - - long startSolveTime = System.currentTimeMillis(); - - if (solver == PoseSolver.APRILTAG_BUILTIN) - { - double[] pose = ApriltagDetectionJNI.getPoseEstimate( - ptrDetection, - outputUnitsLength.fromUnit(metadata.distanceUnit, metadata.tagsize), - fx, fy, cx, cy); - - // Build rotation matrix - float[] rotMtxVals = new float[3 * 3]; - for (int i = 0; i < 9; i++) - { - rotMtxVals[i] = (float) pose[3 + i]; - } - - rawPose = new AprilTagPoseRaw( - pose[0], pose[1], pose[2], // x y z - new GeneralMatrixF(3, 3, rotMtxVals)); // R - } - else - { - Pose opencvPose = poseFromTrapezoid( - cornerPts, - cameraMatrix, - outputUnitsLength.fromUnit(metadata.distanceUnit, metadata.tagsize), - solver.code); - - // Build rotation matrix - Mat R = new Mat(3, 3, CvType.CV_32F); - Calib3d.Rodrigues(opencvPose.rvec, R); - float[] tmp2 = new float[9]; - R.get(0,0, tmp2); - - rawPose = new AprilTagPoseRaw( - opencvPose.tvec.get(0,0)[0], // x - opencvPose.tvec.get(1,0)[0], // y - opencvPose.tvec.get(2,0)[0], // z - new GeneralMatrixF(3,3, tmp2)); // R - } - - long endSolveTime = System.currentTimeMillis(); - solveTime.add(endSolveTime-startSolveTime); - } - else - { - // We don't know anything about the tag size so we can't solve the pose - rawPose = null; - } - - if (rawPose != null) - { - Orientation rot = Orientation.getOrientation(rawPose.R, AxesReference.INTRINSIC, AxesOrder.YXZ, outputUnitsAngle); - - ftcPose = new AprilTagPoseFtc( - rawPose.x, // x NB: These are *intentionally* not matched directly; - rawPose.z, // y this is the mapping between the AprilTag coordinate - -rawPose.y, // z system and the FTC coordinate system - -rot.firstAngle, // yaw - rot.secondAngle, // pitch - rot.thirdAngle, // roll - Math.hypot(rawPose.x, rawPose.z), // range - outputUnitsAngle.fromUnit(AngleUnit.RADIANS, Math.atan2(-rawPose.x, rawPose.z)), // bearing - outputUnitsAngle.fromUnit(AngleUnit.RADIANS, Math.atan2(-rawPose.y, rawPose.z))); // elevation - - robotPose = computeRobotPose(rawPose, metadata, captureTimeNanos); - } - else - { - ftcPose = null; - robotPose = null; - } - - double[] center = ApriltagDetectionJNI.getCenterpoint(ptrDetection); - - detections.add(new AprilTagDetection( - ApriltagDetectionJNI.getId(ptrDetection), - ApriltagDetectionJNI.getHamming(ptrDetection), - ApriltagDetectionJNI.getDecisionMargin(ptrDetection), - new Point(center[0], center[1]), cornerPts, metadata, ftcPose, rawPose, robotPose, captureTimeNanos)); - } - - ApriltagDetectionJNI.freeDetectionList(ptrDetectionArray); - return detections; - } - - return new ArrayList<>(); - } - - private Pose3D computeRobotPose(AprilTagPoseRaw rawPose, AprilTagMetadata metadata, long acquisitionTime) - { - // Compute transformation matrix of tag pose in field reference frame - float tagInFieldX = metadata.fieldPosition.get(0); - float tagInFieldY = metadata.fieldPosition.get(1); - float tagInFieldZ = metadata.fieldPosition.get(2); - OpenGLMatrix tagInFieldR = new OpenGLMatrix(metadata.fieldOrientation.toMatrix()); - OpenGLMatrix tagInFieldFrame = OpenGLMatrix.identityMatrix() - .translated(tagInFieldX, tagInFieldY, tagInFieldZ) - .multiplied(tagInFieldR); - - // Compute transformation matrix of camera pose in tag reference frame - float tagInCameraX = (float) DistanceUnit.INCH.fromUnit(outputUnitsLength, rawPose.x); - float tagInCameraY = (float) DistanceUnit.INCH.fromUnit(outputUnitsLength, rawPose.y); - float tagInCameraZ = (float) DistanceUnit.INCH.fromUnit(outputUnitsLength, rawPose.z); - OpenGLMatrix tagInCameraR = new OpenGLMatrix((rawPose.R)); - OpenGLMatrix cameraInTagFrame = OpenGLMatrix.identityMatrix() - .translated(tagInCameraX, tagInCameraY, tagInCameraZ) - .multiplied(tagInCameraR) - .inverted(); - - // Compute transformation matrix of robot pose in field frame - OpenGLMatrix robotInFieldFrame = - tagInFieldFrame - .multiplied(cameraInTagFrame) - .multiplied(robotInCameraFrame); - - // Extract robot location - VectorF robotInFieldTranslation = robotInFieldFrame.getTranslation(); - Position robotPosition = new Position(DistanceUnit.INCH, - robotInFieldTranslation.get(0), - robotInFieldTranslation.get(1), - robotInFieldTranslation.get(2), - acquisitionTime).toUnit(outputUnitsLength); - - // Extract robot orientation - Orientation robotInFieldOrientation = Orientation.getOrientation(robotInFieldFrame, - AxesReference.INTRINSIC, AxesOrder.ZXY, outputUnitsAngle); - YawPitchRollAngles robotOrientation = new YawPitchRollAngles(outputUnitsAngle, - robotInFieldOrientation.firstAngle, - robotInFieldOrientation.secondAngle, - robotInFieldOrientation.thirdAngle, - acquisitionTime); - - return new Pose3D(robotPosition, robotOrientation); - } - - private final Object drawSync = new Object(); - - @Override - public void onDrawFrame(Canvas canvas, int onscreenWidth, int onscreenHeight, float scaleBmpPxToCanvasPx, float scaleCanvasDensity, Object userContext) - { - // Only one draw operation at a time thank you very much. - // (we could be called from two different threads - viewport or camera stream) - synchronized (drawSync) - { - if ((drawAxes || drawCube || drawOutline || drawTagID) && userContext != null) - { - canvasAnnotator.noteDrawParams(scaleBmpPxToCanvasPx, scaleCanvasDensity); - - ArrayList dets = (ArrayList) userContext; - - // For fun, draw 6DOF markers on the image. - for(AprilTagDetection detection : dets) - { - if (drawTagID) - { - canvasAnnotator.drawTagID(detection, canvas); - } - - // Could be null if we couldn't solve the pose earlier due to not knowing tag size - if (detection.rawPose != null) - { - AprilTagMetadata metadata = tagLibrary.lookupTag(detection.id); - double tagSize = outputUnitsLength.fromUnit(metadata.distanceUnit, metadata.tagsize); - - if (drawOutline) - { - canvasAnnotator.drawOutlineMarker(detection, canvas, tagSize); - } - if (drawAxes) - { - canvasAnnotator.drawAxisMarker(detection, canvas, tagSize); - } - if (drawCube) - { - canvasAnnotator.draw3dCubeMarker(detection, canvas, tagSize); - } - } - } - } - } - } - - public void setDecimation(float decimation) - { - synchronized (decimationSync) - { - this.decimation = decimation; - needToSetDecimation = true; - } - } - - @Override - public void setPoseSolver(PoseSolver poseSolver) - { - this.poseSolver = poseSolver; - } - - @Override - public int getPerTagAvgPoseSolveTime() - { - return (int) Math.round(solveTime.getMean()); - } - - public ArrayList getDetections() - { - return detections; - } - - public ArrayList getFreshDetections() - { - synchronized (detectionsUpdateSync) - { - ArrayList ret = detectionsUpdate; - detectionsUpdate = null; - return ret; - } - } - - void constructMatrix() - { - // Construct the camera matrix. - // - // -- -- - // | fx 0 cx | - // | 0 fy cy | - // | 0 0 1 | - // -- -- - // - - cameraMatrix = new Mat(3,3, CvType.CV_32FC1); - - cameraMatrix.put(0,0, fx); - cameraMatrix.put(0,1,0); - cameraMatrix.put(0,2, cx); - - cameraMatrix.put(1,0,0); - cameraMatrix.put(1,1,fy); - cameraMatrix.put(1,2,cy); - - cameraMatrix.put(2, 0, 0); - cameraMatrix.put(2,1,0); - cameraMatrix.put(2,2,1); - } - - /** - * Converts an AprilTag pose to an OpenCV pose - * @param aprilTagPose pose to convert - * @return OpenCV output pose - */ - static Pose aprilTagPoseToOpenCvPose(AprilTagPoseRaw aprilTagPose) - { - Pose pose = new Pose(); - pose.tvec.put(0,0, aprilTagPose.x); - pose.tvec.put(1,0, aprilTagPose.y); - pose.tvec.put(2,0, aprilTagPose.z); - - Mat R = new Mat(3, 3, CvType.CV_32F); - - for (int i = 0; i < 3; i++) - { - for (int j = 0; j < 3; j++) - { - R.put(i,j, aprilTagPose.R.get(i,j)); - } - } - - Calib3d.Rodrigues(R, pose.rvec); - - return pose; - } - - /** - * Extracts 6DOF pose from a trapezoid, using a camera intrinsics matrix and the - * original size of the tag. - * - * @param points the points which form the trapezoid - * @param cameraMatrix the camera intrinsics matrix - * @param tagsize the original length of the tag - * @return the 6DOF pose of the camera relative to the tag - */ - static Pose poseFromTrapezoid(Point[] points, Mat cameraMatrix, double tagsize, int solveMethod) - { - // The actual 2d points of the tag detected in the image - MatOfPoint2f points2d = new MatOfPoint2f(points); - - // The 3d points of the tag in an 'ideal projection' - Point3[] arrayPoints3d = new Point3[4]; - arrayPoints3d[0] = new Point3(-tagsize/2, tagsize/2, 0); - arrayPoints3d[1] = new Point3(tagsize/2, tagsize/2, 0); - arrayPoints3d[2] = new Point3(tagsize/2, -tagsize/2, 0); - arrayPoints3d[3] = new Point3(-tagsize/2, -tagsize/2, 0); - MatOfPoint3f points3d = new MatOfPoint3f(arrayPoints3d); - - // Using this information, actually solve for pose - Pose pose = new Pose(); - Calib3d.solvePnP(points3d, points2d, cameraMatrix, new MatOfDouble(), pose.rvec, pose.tvec, false, solveMethod); - - return pose; - } - - /* - * A simple container to hold both rotation and translation - * vectors, which together form a 6DOF pose. - */ - static class Pose - { - Mat rvec; - Mat tvec; - - public Pose() - { - rvec = new Mat(3, 1, CvType.CV_32F); - tvec = new Mat(3, 1, CvType.CV_32F); - } - - public Pose(Mat rvec, Mat tvec) - { - this.rvec = rvec; - this.tvec = tvec; - } - } +/* + * Copyright (c) 2023 FIRST + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted (subject to the limitations in the disclaimer below) provided that + * the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * Neither the name of FIRST nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior + * written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS + * LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.firstinspires.ftc.vision.apriltag; + +import android.graphics.Canvas; + +import com.qualcomm.robotcore.util.MovingStatistics; +import com.qualcomm.robotcore.util.RobotLog; + +import org.firstinspires.ftc.robotcore.external.matrices.GeneralMatrixF; +import org.firstinspires.ftc.robotcore.external.matrices.OpenGLMatrix; +import org.firstinspires.ftc.robotcore.external.matrices.VectorF; +import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit; +import org.firstinspires.ftc.robotcore.external.navigation.Pose3D; +import org.firstinspires.ftc.robotcore.external.navigation.AxesOrder; +import org.firstinspires.ftc.robotcore.external.navigation.AxesReference; +import org.firstinspires.ftc.robotcore.external.navigation.DistanceUnit; +import org.firstinspires.ftc.robotcore.external.navigation.Orientation; +import org.firstinspires.ftc.robotcore.external.navigation.Position; +import org.firstinspires.ftc.robotcore.external.navigation.YawPitchRollAngles; +import org.firstinspires.ftc.robotcore.internal.camera.calibration.CameraCalibration; +import org.opencv.calib3d.Calib3d; +import org.opencv.core.CvType; +import org.opencv.core.Mat; +import org.opencv.core.MatOfDouble; +import org.opencv.core.MatOfPoint2f; +import org.opencv.core.MatOfPoint3f; +import org.opencv.core.Point; +import org.opencv.core.Point3; +import org.opencv.imgproc.Imgproc; +import org.openftc.apriltag.AprilTagDetectorJNI; +import org.openftc.apriltag.ApriltagDetectionJNI; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; + +public class AprilTagProcessorImpl extends AprilTagProcessor +{ + public static final String TAG = "AprilTagProcessorImpl"; + + Logger logger = LoggerFactory.getLogger(TAG); + + private long nativeApriltagPtr; + private Mat grey = new Mat(); + private ArrayList detections = new ArrayList<>(); + + private ArrayList detectionsUpdate = new ArrayList<>(); + private final Object detectionsUpdateSync = new Object(); + private boolean drawAxes; + private boolean drawCube; + private boolean drawOutline; + private boolean drawTagID; + + private Mat cameraMatrix; + + private double fx; + private double fy; + private double cx; + private double cy; + private final boolean suppressCalibrationWarnings; + + private final AprilTagLibrary tagLibrary; + + private float decimation; + private boolean needToSetDecimation; + private final Object decimationSync = new Object(); + + private AprilTagCanvasAnnotator canvasAnnotator; + + private final DistanceUnit outputUnitsLength; + private final AngleUnit outputUnitsAngle; + + private volatile PoseSolver poseSolver = PoseSolver.APRILTAG_BUILTIN; + + private OpenGLMatrix robotInCameraFrame; + + public AprilTagProcessorImpl(OpenGLMatrix robotInCameraFrame, double fx, double fy, double cx, double cy, DistanceUnit outputUnitsLength, AngleUnit outputUnitsAngle, AprilTagLibrary tagLibrary, + boolean drawAxes, boolean drawCube, boolean drawOutline, boolean drawTagID, TagFamily tagFamily, int threads, boolean suppressCalibrationWarnings) + { + this.robotInCameraFrame = robotInCameraFrame; + + this.fx = fx; + this.fy = fy; + this.cx = cx; + this.cy = cy; + + this.tagLibrary = tagLibrary; + this.outputUnitsLength = outputUnitsLength; + this.outputUnitsAngle = outputUnitsAngle; + this.suppressCalibrationWarnings = suppressCalibrationWarnings; + this.drawAxes = drawAxes; + this.drawCube = drawCube; + this.drawOutline = drawOutline; + this.drawTagID = drawTagID; + + // Allocate a native context object. See the corresponding deletion in the finalizer + nativeApriltagPtr = AprilTagDetectorJNI.createApriltagDetector(tagFamily.ATLibTF.string, 3, threads); + } + + @Override + protected void finalize() + { + // Might be null if createApriltagDetector() threw an exception + if(nativeApriltagPtr != 0) + { + // Delete the native context we created in the constructor + AprilTagDetectorJNI.releaseApriltagDetector(nativeApriltagPtr); + nativeApriltagPtr = 0; + } + else + { + logger.debug("AprilTagDetectionPipeline.finalize(): nativeApriltagPtr was NULL"); + } + } + + @Override + public void init(int width, int height, CameraCalibration calibration) + { + // ATTEMPT 1 - If the user provided their own calibration, use that + if (fx != 0 && fy != 0 && cx != 0 && cy != 0) + { + logger.debug(String.format("User provided their own camera calibration fx=%7.3f fy=%7.3f cx=%7.3f cy=%7.3f", + fx, fy, cx, cy)); + } + + // ATTEMPT 2 - If we have valid calibration we can use, use it + else if (calibration != null && !calibration.isDegenerate()) // needed because we may get an all zero calibration to indicate none, instead of null + { + fx = calibration.focalLengthX; + fy = calibration.focalLengthY; + cx = calibration.principalPointX; + cy = calibration.principalPointY; + + // Note that this might have been a scaled calibration - inform the user if so + if (calibration.resolutionScaledFrom != null) + { + String msg = String.format("Camera has not been calibrated for [%dx%d]; applying a scaled calibration from [%dx%d].", width, height, calibration.resolutionScaledFrom.getWidth(), calibration.resolutionScaledFrom.getHeight()); + + if (!suppressCalibrationWarnings) + { + logger.warn(msg); + } + } + // Nope, it was a full up proper calibration - no need to pester the user about anything + else + { + logger.debug(String.format("User did not provide a camera calibration; but we DO have a built in calibration we can use.\n [%dx%d] (NOT scaled) %s\nfx=%7.3f fy=%7.3f cx=%7.3f cy=%7.3f", + calibration.getSize().getWidth(), calibration.getSize().getHeight(), calibration.getIdentity().toString(), fx, fy, cx, cy)); + } + } + + // Okay, we aren't going to have any calibration data we can use, but there are 2 cases to check + else + { + // NO-OP, we cannot implement this for EOCV-Sim in the same way as the FTC SDK + + /* + // If we have a calibration on file, but with a wrong aspect ratio, + // we can't use it, but hey at least we can let the user know about it. + if (calibration instanceof PlaceholderCalibratedAspectRatioMismatch) + { + StringBuilder supportedResBuilder = new StringBuilder(); + + for (CameraCalibration cal : CameraCalibrationHelper.getInstance().getCalibrations(calibration.getIdentity())) + { + supportedResBuilder.append(String.format("[%dx%d],", cal.getSize().getWidth(), cal.getSize().getHeight())); + } + + String msg = String.format("Camera has not been calibrated for [%dx%d]. Pose estimates will likely be inaccurate. However, there are built in calibrations for resolutions: %s", + width, height, supportedResBuilder.toString()); + + if (!suppressCalibrationWarnings) + { + logger.warn(msg); + } + + + // Nah, we got absolutely nothing + else*/ + { + String warning = "User did not provide a camera calibration, nor was a built-in calibration found for this camera. Pose estimates will likely be inaccurate."; + + if (!suppressCalibrationWarnings) + { + logger.warn(warning); + } + } + + // IN EITHER CASE, set it to *something* so we don't crash the native code + fx = 578.272; + fy = 578.272; + cx = width/2; + cy = height/2; + } + + constructMatrix(); + canvasAnnotator = new AprilTagCanvasAnnotator(cameraMatrix); + } + + @Override + public Object processFrame(Mat input, long captureTimeNanos) + { + // Convert to greyscale + Imgproc.cvtColor(input, grey, Imgproc.COLOR_RGBA2GRAY); + + synchronized (decimationSync) + { + if(needToSetDecimation) + { + AprilTagDetectorJNI.setApriltagDetectorDecimation(nativeApriltagPtr, decimation); + needToSetDecimation = false; + } + } + + // Run AprilTag + detections = runAprilTagDetectorForMultipleTagSizes(captureTimeNanos); + + synchronized (detectionsUpdateSync) + { + detectionsUpdate = detections; + } + + // TODO do we need to deep copy this so the user can't mess with it before use in onDrawFrame()? + return detections; + } + + private MovingStatistics solveTime = new MovingStatistics(50); + + // We cannot use runAprilTagDetectorSimple because we cannot assume tags are all the same size + ArrayList runAprilTagDetectorForMultipleTagSizes(long captureTimeNanos) + { + long ptrDetectionArray = AprilTagDetectorJNI.runApriltagDetector(nativeApriltagPtr, grey.dataAddr(), grey.width(), grey.height()); + if (ptrDetectionArray != 0) + { + long[] detectionPointers = ApriltagDetectionJNI.getDetectionPointers(ptrDetectionArray); + ArrayList detections = new ArrayList<>(detectionPointers.length); + + for (long ptrDetection : detectionPointers) + { + AprilTagMetadata metadata = tagLibrary.lookupTag(ApriltagDetectionJNI.getId(ptrDetection)); + + double[][] corners = ApriltagDetectionJNI.getCorners(ptrDetection); + + Point[] cornerPts = new Point[4]; + for (int p = 0; p < 4; p++) + { + cornerPts[p] = new Point(corners[p][0], corners[p][1]); + } + + AprilTagPoseRaw rawPose; + AprilTagPoseFtc ftcPose; + Pose3D robotPose; + + if (metadata != null) + { + PoseSolver solver = poseSolver; // snapshot, can change + + long startSolveTime = System.currentTimeMillis(); + + if (solver == PoseSolver.APRILTAG_BUILTIN) + { + double[] pose = ApriltagDetectionJNI.getPoseEstimate( + ptrDetection, + outputUnitsLength.fromUnit(metadata.distanceUnit, metadata.tagsize), + fx, fy, cx, cy); + + // Build rotation matrix + float[] rotMtxVals = new float[3 * 3]; + for (int i = 0; i < 9; i++) + { + rotMtxVals[i] = (float) pose[3 + i]; + } + + rawPose = new AprilTagPoseRaw( + pose[0], pose[1], pose[2], // x y z + new GeneralMatrixF(3, 3, rotMtxVals)); // R + } + else + { + Pose opencvPose = poseFromTrapezoid( + cornerPts, + cameraMatrix, + outputUnitsLength.fromUnit(metadata.distanceUnit, metadata.tagsize), + solver.code); + + // Build rotation matrix + Mat R = new Mat(3, 3, CvType.CV_32F); + Calib3d.Rodrigues(opencvPose.rvec, R); + float[] tmp2 = new float[9]; + R.get(0,0, tmp2); + + rawPose = new AprilTagPoseRaw( + opencvPose.tvec.get(0,0)[0], // x + opencvPose.tvec.get(1,0)[0], // y + opencvPose.tvec.get(2,0)[0], // z + new GeneralMatrixF(3,3, tmp2)); // R + } + + long endSolveTime = System.currentTimeMillis(); + solveTime.add(endSolveTime-startSolveTime); + } + else + { + // We don't know anything about the tag size so we can't solve the pose + rawPose = null; + } + + if (rawPose != null) + { + Orientation rot = Orientation.getOrientation(rawPose.R, AxesReference.INTRINSIC, AxesOrder.YXZ, outputUnitsAngle); + + ftcPose = new AprilTagPoseFtc( + rawPose.x, // x NB: These are *intentionally* not matched directly; + rawPose.z, // y this is the mapping between the AprilTag coordinate + -rawPose.y, // z system and the FTC coordinate system + -rot.firstAngle, // yaw + rot.secondAngle, // pitch + rot.thirdAngle, // roll + Math.hypot(rawPose.x, rawPose.z), // range + outputUnitsAngle.fromUnit(AngleUnit.RADIANS, Math.atan2(-rawPose.x, rawPose.z)), // bearing + outputUnitsAngle.fromUnit(AngleUnit.RADIANS, Math.atan2(-rawPose.y, rawPose.z))); // elevation + + robotPose = computeRobotPose(rawPose, metadata, captureTimeNanos); + } + else + { + ftcPose = null; + robotPose = null; + } + + double[] center = ApriltagDetectionJNI.getCenterpoint(ptrDetection); + + detections.add(new AprilTagDetection( + ApriltagDetectionJNI.getId(ptrDetection), + ApriltagDetectionJNI.getHamming(ptrDetection), + ApriltagDetectionJNI.getDecisionMargin(ptrDetection), + new Point(center[0], center[1]), cornerPts, metadata, ftcPose, rawPose, robotPose, captureTimeNanos)); + } + + ApriltagDetectionJNI.freeDetectionList(ptrDetectionArray); + return detections; + } + + return new ArrayList<>(); + } + + private Pose3D computeRobotPose(AprilTagPoseRaw rawPose, AprilTagMetadata metadata, long acquisitionTime) + { + // Compute transformation matrix of tag pose in field reference frame + float tagInFieldX = metadata.fieldPosition.get(0); + float tagInFieldY = metadata.fieldPosition.get(1); + float tagInFieldZ = metadata.fieldPosition.get(2); + OpenGLMatrix tagInFieldR = new OpenGLMatrix(metadata.fieldOrientation.toMatrix()); + OpenGLMatrix tagInFieldFrame = OpenGLMatrix.identityMatrix() + .translated(tagInFieldX, tagInFieldY, tagInFieldZ) + .multiplied(tagInFieldR); + + // Compute transformation matrix of camera pose in tag reference frame + float tagInCameraX = (float) DistanceUnit.INCH.fromUnit(outputUnitsLength, rawPose.x); + float tagInCameraY = (float) DistanceUnit.INCH.fromUnit(outputUnitsLength, rawPose.y); + float tagInCameraZ = (float) DistanceUnit.INCH.fromUnit(outputUnitsLength, rawPose.z); + OpenGLMatrix tagInCameraR = new OpenGLMatrix((rawPose.R)); + OpenGLMatrix cameraInTagFrame = OpenGLMatrix.identityMatrix() + .translated(tagInCameraX, tagInCameraY, tagInCameraZ) + .multiplied(tagInCameraR) + .inverted(); + + // Compute transformation matrix of robot pose in field frame + OpenGLMatrix robotInFieldFrame = + tagInFieldFrame + .multiplied(cameraInTagFrame) + .multiplied(robotInCameraFrame); + + // Extract robot location + VectorF robotInFieldTranslation = robotInFieldFrame.getTranslation(); + Position robotPosition = new Position(DistanceUnit.INCH, + robotInFieldTranslation.get(0), + robotInFieldTranslation.get(1), + robotInFieldTranslation.get(2), + acquisitionTime).toUnit(outputUnitsLength); + + // Extract robot orientation + Orientation robotInFieldOrientation = Orientation.getOrientation(robotInFieldFrame, + AxesReference.INTRINSIC, AxesOrder.ZXY, outputUnitsAngle); + YawPitchRollAngles robotOrientation = new YawPitchRollAngles(outputUnitsAngle, + robotInFieldOrientation.firstAngle, + robotInFieldOrientation.secondAngle, + robotInFieldOrientation.thirdAngle, + acquisitionTime); + + return new Pose3D(robotPosition, robotOrientation); + } + + private final Object drawSync = new Object(); + + @Override + public void onDrawFrame(Canvas canvas, int onscreenWidth, int onscreenHeight, float scaleBmpPxToCanvasPx, float scaleCanvasDensity, Object userContext) + { + // Only one draw operation at a time thank you very much. + // (we could be called from two different threads - viewport or camera stream) + synchronized (drawSync) + { + if ((drawAxes || drawCube || drawOutline || drawTagID) && userContext != null) + { + canvasAnnotator.noteDrawParams(scaleBmpPxToCanvasPx, scaleCanvasDensity); + + ArrayList dets = (ArrayList) userContext; + + // For fun, draw 6DOF markers on the image. + for(AprilTagDetection detection : dets) + { + if (drawTagID) + { + canvasAnnotator.drawTagID(detection, canvas); + } + + // Could be null if we couldn't solve the pose earlier due to not knowing tag size + if (detection.rawPose != null) + { + AprilTagMetadata metadata = tagLibrary.lookupTag(detection.id); + double tagSize = outputUnitsLength.fromUnit(metadata.distanceUnit, metadata.tagsize); + + if (drawOutline) + { + canvasAnnotator.drawOutlineMarker(detection, canvas, tagSize); + } + if (drawAxes) + { + canvasAnnotator.drawAxisMarker(detection, canvas, tagSize); + } + if (drawCube) + { + canvasAnnotator.draw3dCubeMarker(detection, canvas, tagSize); + } + } + } + } + } + } + + public void setDecimation(float decimation) + { + synchronized (decimationSync) + { + this.decimation = decimation; + needToSetDecimation = true; + } + } + + @Override + public void setPoseSolver(PoseSolver poseSolver) + { + this.poseSolver = poseSolver; + } + + @Override + public int getPerTagAvgPoseSolveTime() + { + return (int) Math.round(solveTime.getMean()); + } + + public ArrayList getDetections() + { + return detections; + } + + public ArrayList getFreshDetections() + { + synchronized (detectionsUpdateSync) + { + ArrayList ret = detectionsUpdate; + detectionsUpdate = null; + return ret; + } + } + + void constructMatrix() + { + // Construct the camera matrix. + // + // -- -- + // | fx 0 cx | + // | 0 fy cy | + // | 0 0 1 | + // -- -- + // + + cameraMatrix = new Mat(3,3, CvType.CV_32FC1); + + cameraMatrix.put(0,0, fx); + cameraMatrix.put(0,1,0); + cameraMatrix.put(0,2, cx); + + cameraMatrix.put(1,0,0); + cameraMatrix.put(1,1,fy); + cameraMatrix.put(1,2,cy); + + cameraMatrix.put(2, 0, 0); + cameraMatrix.put(2,1,0); + cameraMatrix.put(2,2,1); + } + + /** + * Converts an AprilTag pose to an OpenCV pose + * @param aprilTagPose pose to convert + * @return OpenCV output pose + */ + static Pose aprilTagPoseToOpenCvPose(AprilTagPoseRaw aprilTagPose) + { + Pose pose = new Pose(); + pose.tvec.put(0,0, aprilTagPose.x); + pose.tvec.put(1,0, aprilTagPose.y); + pose.tvec.put(2,0, aprilTagPose.z); + + Mat R = new Mat(3, 3, CvType.CV_32F); + + for (int i = 0; i < 3; i++) + { + for (int j = 0; j < 3; j++) + { + R.put(i,j, aprilTagPose.R.get(i,j)); + } + } + + Calib3d.Rodrigues(R, pose.rvec); + + return pose; + } + + /** + * Extracts 6DOF pose from a trapezoid, using a camera intrinsics matrix and the + * original size of the tag. + * + * @param points the points which form the trapezoid + * @param cameraMatrix the camera intrinsics matrix + * @param tagsize the original length of the tag + * @return the 6DOF pose of the camera relative to the tag + */ + static Pose poseFromTrapezoid(Point[] points, Mat cameraMatrix, double tagsize, int solveMethod) + { + // The actual 2d points of the tag detected in the image + MatOfPoint2f points2d = new MatOfPoint2f(points); + + // The 3d points of the tag in an 'ideal projection' + Point3[] arrayPoints3d = new Point3[4]; + arrayPoints3d[0] = new Point3(-tagsize/2, tagsize/2, 0); + arrayPoints3d[1] = new Point3(tagsize/2, tagsize/2, 0); + arrayPoints3d[2] = new Point3(tagsize/2, -tagsize/2, 0); + arrayPoints3d[3] = new Point3(-tagsize/2, -tagsize/2, 0); + MatOfPoint3f points3d = new MatOfPoint3f(arrayPoints3d); + + // Using this information, actually solve for pose + Pose pose = new Pose(); + Calib3d.solvePnP(points3d, points2d, cameraMatrix, new MatOfDouble(), pose.rvec, pose.tvec, false, solveMethod); + + return pose; + } + + /* + * A simple container to hold both rotation and translation + * vectors, which together form a 6DOF pose. + */ + static class Pose + { + Mat rvec; + Mat tvec; + + public Pose() + { + rvec = new Mat(3, 1, CvType.CV_32F); + tvec = new Mat(3, 1, CvType.CV_32F); + } + + public Pose(Mat rvec, Mat tvec) + { + this.rvec = rvec; + this.tvec = tvec; + } + } } \ No newline at end of file diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraBase.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraBase.java index 66abc6a0..61fadca5 100644 --- a/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraBase.java +++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraBase.java @@ -1,359 +1,359 @@ -/* - * Copyright (c) 2019 OpenFTC Team - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.openftc.easyopencv; - -import android.graphics.Bitmap; -import android.graphics.Canvas; -import com.qualcomm.robotcore.util.ElapsedTime; -import com.qualcomm.robotcore.util.MovingStatistics; -import io.github.deltacv.common.pipeline.util.PipelineStatisticsCalculator; -import io.github.deltacv.vision.external.PipelineRenderHook; -import org.opencv.android.Utils; -import org.opencv.core.*; -import org.opencv.imgproc.Imgproc; - -public abstract class OpenCvCameraBase implements OpenCvCamera { - - private OpenCvPipeline pipeline = null; - - private OpenCvViewport viewport; - private OpenCvCameraRotation rotation; - - private final Object pipelineChangeLock = new Object(); - - private Mat rotatedMat = new Mat(); - private Mat matToUseIfPipelineReturnedCropped; - private Mat croppedColorCvtedMat = new Mat(); - - private boolean isStreaming = false; - private boolean viewportEnabled = true; - - private ViewportRenderer desiredViewportRenderer = ViewportRenderer.SOFTWARE; - ViewportRenderingPolicy desiredRenderingPolicy = ViewportRenderingPolicy.MAXIMIZE_EFFICIENCY; - boolean fpsMeterDesired = true; - - private Scalar brown = new Scalar(82, 61, 46, 255); - - private int frameCount = 0; - - private PipelineStatisticsCalculator statistics = new PipelineStatisticsCalculator(); - - private double width; - private double height; - - public OpenCvCameraBase(OpenCvViewport viewport, boolean viewportEnabled) { - this.viewport = viewport; - this.rotation = getDefaultRotation(); - - this.viewportEnabled = viewportEnabled; - } - - @Override - public void showFpsMeterOnViewport(boolean show) { - viewport.setFpsMeterEnabled(show); - } - - @Override - public void pauseViewport() { - viewport.pause(); - } - - @Override - public void resumeViewport() { - viewport.resume(); - } - - @Override - public void setViewportRenderingPolicy(ViewportRenderingPolicy policy) { - viewport.setRenderingPolicy(policy); - } - - @Override - public void setViewportRenderer(ViewportRenderer renderer) { - this.desiredViewportRenderer = renderer; - } - - @Override - public void setPipeline(OpenCvPipeline pipeline) { - this.pipeline = pipeline; - } - - @Override - public int getFrameCount() { - return frameCount; - } - - @Override - public float getFps() { - return statistics.getAvgFps(); - } - - @Override - public int getPipelineTimeMs() { - return statistics.getAvgPipelineTime(); - } - - @Override - public int getOverheadTimeMs() { - return statistics.getAvgOverheadTime(); - } - - @Override - public int getTotalFrameTimeMs() { - return getTotalFrameTimeMs(); - } - - @Override - public int getCurrentPipelineMaxFps() { - return 0; - } - - @Override - public void startRecordingPipeline(PipelineRecordingParameters parameters) { - } - - @Override - public void stopRecordingPipeline() { - } - - protected void notifyStartOfFrameProcessing() { - statistics.newInputFrameStart(); - } - - public synchronized final void prepareForOpenCameraDevice() - { - if (viewportEnabled) - { - setupViewport(); - viewport.setRenderingPolicy(desiredRenderingPolicy); - } - } - - public synchronized final void prepareForStartStreaming(int width, int height, OpenCvCameraRotation rotation) - { - this.rotation = rotation; - this.statistics = new PipelineStatisticsCalculator(); - this.statistics.init(); - - Size sizeAfterRotation = getFrameSizeAfterRotation(width, height, rotation); - - this.width = sizeAfterRotation.width; - this.height = sizeAfterRotation.height; - - if(viewport != null) - { - // viewport.setSize(width, height); - viewport.setOptimizedViewRotation(getOptimizedViewportRotation(rotation)); - viewport.activate(); - } - } - - public synchronized final void cleanupForEndStreaming() { - matToUseIfPipelineReturnedCropped = null; - - if (viewport != null) { - viewport.deactivate(); - } - } - - protected synchronized void handleFrameUserCrashable(Mat frame, long timestamp) { - statistics.newPipelineFrameStart(); - - Mat userProcessedFrame = null; - - int rotateCode = mapRotationEnumToOpenCvRotateCode(rotation); - - if (rotateCode != -1) { - /* - * Rotate onto another Mat rather than doing so in-place. - * - * This does two things: - * 1) It seems that rotating by 90 or 270 in-place - * causes the backing buffer to be re-allocated - * since the width/height becomes swapped. This - * causes a problem for user code which makes a - * submat from the input Mat, because after the - * parent Mat is re-allocated the submat is no - * longer tied to it. Thus, by rotating onto - * another Mat (which is never re-allocated) we - * remove that issue. - * - * 2) Since the backing buffer does need need to be - * re-allocated for each frame, we reduce overhead - * time by about 1ms. - */ - Core.rotate(frame, rotatedMat, rotateCode); - frame = rotatedMat; - } - - final OpenCvPipeline pipelineSafe; - - // Grab a safe reference to what the pipeline currently is, - // since the user is allowed to change it at any time - synchronized (pipelineChangeLock) { - pipelineSafe = pipeline; - } - - if (pipelineSafe != null) { - if (pipelineSafe instanceof TimestampedOpenCvPipeline) { - ((TimestampedOpenCvPipeline) pipelineSafe).setTimestamp(timestamp); - } - - statistics.beforeProcessFrame(); - - userProcessedFrame = pipelineSafe.processFrameInternal(frame); - - statistics.afterProcessFrame(); - } - - // Will point to whatever mat we end up deciding to send to the screen - final Mat matForDisplay; - - if (pipelineSafe == null) { - matForDisplay = frame; - } else if (userProcessedFrame == null) { - throw new OpenCvCameraException("User pipeline returned null"); - } else if (userProcessedFrame.empty()) { - throw new OpenCvCameraException("User pipeline returned empty mat"); - } else if (userProcessedFrame.cols() != frame.cols() || userProcessedFrame.rows() != frame.rows()) { - /* - * The user didn't return the same size image from their pipeline as we gave them, - * ugh. This makes our lives interesting because we can't just send an arbitrary - * frame size to the viewport. It re-uses framebuffers that are of a fixed resolution. - * So, we copy the user's Mat onto a Mat of the correct size, and then send that other - * Mat to the viewport. - */ - - if (userProcessedFrame.cols() > frame.cols() || userProcessedFrame.rows() > frame.rows()) { - /* - * What on earth was this user thinking?! They returned a Mat that's BIGGER in - * a dimension than the one we gave them! - */ - - throw new OpenCvCameraException("User pipeline returned frame of unexpected size"); - } - - //We re-use this buffer, only create if needed - if (matToUseIfPipelineReturnedCropped == null) { - matToUseIfPipelineReturnedCropped = frame.clone(); - } - - //Set to brown to indicate to the user the areas which they cropped off - matToUseIfPipelineReturnedCropped.setTo(brown); - - int usrFrmTyp = userProcessedFrame.type(); - - if (usrFrmTyp == CvType.CV_8UC1) { - /* - * Handle 8UC1 returns (masks and single channels of images); - * - * We have to color convert onto a different mat (rather than - * doing so in place) to avoid breaking any of the user's submats - */ - Imgproc.cvtColor(userProcessedFrame, croppedColorCvtedMat, Imgproc.COLOR_GRAY2RGBA); - userProcessedFrame = croppedColorCvtedMat; //Doesn't affect user's handle, only ours - } else if (usrFrmTyp != CvType.CV_8UC4 && usrFrmTyp != CvType.CV_8UC3) { - /* - * Oof, we don't know how to handle the type they gave us - */ - throw new OpenCvCameraException("User pipeline returned a frame of an illegal type. Valid types are CV_8UC1, CV_8UC3, and CV_8UC4"); - } - - //Copy the user's frame onto a Mat of the correct size - userProcessedFrame.copyTo(matToUseIfPipelineReturnedCropped.submat( - new Rect(0, 0, userProcessedFrame.cols(), userProcessedFrame.rows()))); - - //Send that correct size Mat to the viewport - matForDisplay = matToUseIfPipelineReturnedCropped; - } else { - /* - * Yay, smart user! They gave us the frame size we were expecting! - * Go ahead and send it right on over to the viewport. - */ - matForDisplay = userProcessedFrame; - } - - if (viewport != null) { - viewport.post(matForDisplay, new OpenCvViewport.FrameContext(pipelineSafe, pipelineSafe != null ? pipelineSafe.getUserContextForDrawHook() : null)); - } - - statistics.endFrame(); - - if (viewport != null) { - viewport.notifyStatistics(statistics.getAvgFps(), statistics.getAvgPipelineTime(), statistics.getAvgOverheadTime()); - } - - frameCount++; - } - - - protected OpenCvViewport.OptimizedRotation getOptimizedViewportRotation(OpenCvCameraRotation streamRotation) { - if (!cameraOrientationIsTiedToDeviceOrientation()) { - return OpenCvViewport.OptimizedRotation.NONE; - } - - if (streamRotation == OpenCvCameraRotation.SIDEWAYS_LEFT || streamRotation == OpenCvCameraRotation.SENSOR_NATIVE) { - return OpenCvViewport.OptimizedRotation.ROT_90_COUNTERCLOCWISE; - } else if (streamRotation == OpenCvCameraRotation.SIDEWAYS_RIGHT) { - return OpenCvViewport.OptimizedRotation.ROT_90_CLOCKWISE; - } else if (streamRotation == OpenCvCameraRotation.UPSIDE_DOWN) { - return OpenCvViewport.OptimizedRotation.ROT_180; - } else { - return OpenCvViewport.OptimizedRotation.NONE; - } - } - - protected void setupViewport() { - viewport.setFpsMeterEnabled(fpsMeterDesired); - viewport.setRenderHook(PipelineRenderHook.INSTANCE); - } - - protected abstract OpenCvCameraRotation getDefaultRotation(); - - protected abstract int mapRotationEnumToOpenCvRotateCode(OpenCvCameraRotation rotation); - - protected abstract boolean cameraOrientationIsTiedToDeviceOrientation(); - - protected abstract boolean isStreaming(); - - protected Size getFrameSizeAfterRotation(int width, int height, OpenCvCameraRotation rotation) - { - int screenRenderedWidth, screenRenderedHeight; - int openCvRotateCode = mapRotationEnumToOpenCvRotateCode(rotation); - - if(openCvRotateCode == Core.ROTATE_90_CLOCKWISE || openCvRotateCode == Core.ROTATE_90_COUNTERCLOCKWISE) - { - //noinspection SuspiciousNameCombination - screenRenderedWidth = height; - //noinspection SuspiciousNameCombination - screenRenderedHeight = width; - } - else - { - screenRenderedWidth = width; - screenRenderedHeight = height; - } - - return new Size(screenRenderedWidth, screenRenderedHeight); - } - -} +/* + * Copyright (c) 2019 OpenFTC Team + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.openftc.easyopencv; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import com.qualcomm.robotcore.util.ElapsedTime; +import com.qualcomm.robotcore.util.MovingStatistics; +import io.github.deltacv.common.pipeline.util.PipelineStatisticsCalculator; +import io.github.deltacv.vision.external.PipelineRenderHook; +import org.opencv.android.Utils; +import org.opencv.core.*; +import org.opencv.imgproc.Imgproc; + +public abstract class OpenCvCameraBase implements OpenCvCamera { + + private OpenCvPipeline pipeline = null; + + private OpenCvViewport viewport; + private OpenCvCameraRotation rotation; + + private final Object pipelineChangeLock = new Object(); + + private Mat rotatedMat = new Mat(); + private Mat matToUseIfPipelineReturnedCropped; + private Mat croppedColorCvtedMat = new Mat(); + + private boolean isStreaming = false; + private boolean viewportEnabled = true; + + private ViewportRenderer desiredViewportRenderer = ViewportRenderer.SOFTWARE; + ViewportRenderingPolicy desiredRenderingPolicy = ViewportRenderingPolicy.MAXIMIZE_EFFICIENCY; + boolean fpsMeterDesired = true; + + private Scalar brown = new Scalar(82, 61, 46, 255); + + private int frameCount = 0; + + private PipelineStatisticsCalculator statistics = new PipelineStatisticsCalculator(); + + private double width; + private double height; + + public OpenCvCameraBase(OpenCvViewport viewport, boolean viewportEnabled) { + this.viewport = viewport; + this.rotation = getDefaultRotation(); + + this.viewportEnabled = viewportEnabled; + } + + @Override + public void showFpsMeterOnViewport(boolean show) { + viewport.setFpsMeterEnabled(show); + } + + @Override + public void pauseViewport() { + viewport.pause(); + } + + @Override + public void resumeViewport() { + viewport.resume(); + } + + @Override + public void setViewportRenderingPolicy(ViewportRenderingPolicy policy) { + viewport.setRenderingPolicy(policy); + } + + @Override + public void setViewportRenderer(ViewportRenderer renderer) { + this.desiredViewportRenderer = renderer; + } + + @Override + public void setPipeline(OpenCvPipeline pipeline) { + this.pipeline = pipeline; + } + + @Override + public int getFrameCount() { + return frameCount; + } + + @Override + public float getFps() { + return statistics.getAvgFps(); + } + + @Override + public int getPipelineTimeMs() { + return statistics.getAvgPipelineTime(); + } + + @Override + public int getOverheadTimeMs() { + return statistics.getAvgOverheadTime(); + } + + @Override + public int getTotalFrameTimeMs() { + return getTotalFrameTimeMs(); + } + + @Override + public int getCurrentPipelineMaxFps() { + return 0; + } + + @Override + public void startRecordingPipeline(PipelineRecordingParameters parameters) { + } + + @Override + public void stopRecordingPipeline() { + } + + protected void notifyStartOfFrameProcessing() { + statistics.newInputFrameStart(); + } + + public synchronized final void prepareForOpenCameraDevice() + { + if (viewportEnabled) + { + setupViewport(); + viewport.setRenderingPolicy(desiredRenderingPolicy); + } + } + + public synchronized final void prepareForStartStreaming(int width, int height, OpenCvCameraRotation rotation) + { + this.rotation = rotation; + this.statistics = new PipelineStatisticsCalculator(); + this.statistics.init(); + + Size sizeAfterRotation = getFrameSizeAfterRotation(width, height, rotation); + + this.width = sizeAfterRotation.width; + this.height = sizeAfterRotation.height; + + if(viewport != null) + { + // viewport.setSize(width, height); + viewport.setOptimizedViewRotation(getOptimizedViewportRotation(rotation)); + viewport.activate(); + } + } + + public synchronized final void cleanupForEndStreaming() { + matToUseIfPipelineReturnedCropped = null; + + if (viewport != null) { + viewport.deactivate(); + } + } + + protected synchronized void handleFrameUserCrashable(Mat frame, long timestamp) { + statistics.newPipelineFrameStart(); + + Mat userProcessedFrame = null; + + int rotateCode = mapRotationEnumToOpenCvRotateCode(rotation); + + if (rotateCode != -1) { + /* + * Rotate onto another Mat rather than doing so in-place. + * + * This does two things: + * 1) It seems that rotating by 90 or 270 in-place + * causes the backing buffer to be re-allocated + * since the width/height becomes swapped. This + * causes a problem for user code which makes a + * submat from the input Mat, because after the + * parent Mat is re-allocated the submat is no + * longer tied to it. Thus, by rotating onto + * another Mat (which is never re-allocated) we + * remove that issue. + * + * 2) Since the backing buffer does need need to be + * re-allocated for each frame, we reduce overhead + * time by about 1ms. + */ + Core.rotate(frame, rotatedMat, rotateCode); + frame = rotatedMat; + } + + final OpenCvPipeline pipelineSafe; + + // Grab a safe reference to what the pipeline currently is, + // since the user is allowed to change it at any time + synchronized (pipelineChangeLock) { + pipelineSafe = pipeline; + } + + if (pipelineSafe != null) { + if (pipelineSafe instanceof TimestampedOpenCvPipeline) { + ((TimestampedOpenCvPipeline) pipelineSafe).setTimestamp(timestamp); + } + + statistics.beforeProcessFrame(); + + userProcessedFrame = pipelineSafe.processFrameInternal(frame); + + statistics.afterProcessFrame(); + } + + // Will point to whatever mat we end up deciding to send to the screen + final Mat matForDisplay; + + if (pipelineSafe == null) { + matForDisplay = frame; + } else if (userProcessedFrame == null) { + throw new OpenCvCameraException("User pipeline returned null"); + } else if (userProcessedFrame.empty()) { + throw new OpenCvCameraException("User pipeline returned empty mat"); + } else if (userProcessedFrame.cols() != frame.cols() || userProcessedFrame.rows() != frame.rows()) { + /* + * The user didn't return the same size image from their pipeline as we gave them, + * ugh. This makes our lives interesting because we can't just send an arbitrary + * frame size to the viewport. It re-uses framebuffers that are of a fixed resolution. + * So, we copy the user's Mat onto a Mat of the correct size, and then send that other + * Mat to the viewport. + */ + + if (userProcessedFrame.cols() > frame.cols() || userProcessedFrame.rows() > frame.rows()) { + /* + * What on earth was this user thinking?! They returned a Mat that's BIGGER in + * a dimension than the one we gave them! + */ + + throw new OpenCvCameraException("User pipeline returned frame of unexpected size"); + } + + //We re-use this buffer, only create if needed + if (matToUseIfPipelineReturnedCropped == null) { + matToUseIfPipelineReturnedCropped = frame.clone(); + } + + //Set to brown to indicate to the user the areas which they cropped off + matToUseIfPipelineReturnedCropped.setTo(brown); + + int usrFrmTyp = userProcessedFrame.type(); + + if (usrFrmTyp == CvType.CV_8UC1) { + /* + * Handle 8UC1 returns (masks and single channels of images); + * + * We have to color convert onto a different mat (rather than + * doing so in place) to avoid breaking any of the user's submats + */ + Imgproc.cvtColor(userProcessedFrame, croppedColorCvtedMat, Imgproc.COLOR_GRAY2RGBA); + userProcessedFrame = croppedColorCvtedMat; //Doesn't affect user's handle, only ours + } else if (usrFrmTyp != CvType.CV_8UC4 && usrFrmTyp != CvType.CV_8UC3) { + /* + * Oof, we don't know how to handle the type they gave us + */ + throw new OpenCvCameraException("User pipeline returned a frame of an illegal type. Valid types are CV_8UC1, CV_8UC3, and CV_8UC4"); + } + + //Copy the user's frame onto a Mat of the correct size + userProcessedFrame.copyTo(matToUseIfPipelineReturnedCropped.submat( + new Rect(0, 0, userProcessedFrame.cols(), userProcessedFrame.rows()))); + + //Send that correct size Mat to the viewport + matForDisplay = matToUseIfPipelineReturnedCropped; + } else { + /* + * Yay, smart user! They gave us the frame size we were expecting! + * Go ahead and send it right on over to the viewport. + */ + matForDisplay = userProcessedFrame; + } + + if (viewport != null) { + viewport.post(matForDisplay, new OpenCvViewport.FrameContext(pipelineSafe, pipelineSafe != null ? pipelineSafe.getUserContextForDrawHook() : null)); + } + + statistics.endFrame(); + + if (viewport != null) { + viewport.notifyStatistics(statistics.getAvgFps(), statistics.getAvgPipelineTime(), statistics.getAvgOverheadTime()); + } + + frameCount++; + } + + + protected OpenCvViewport.OptimizedRotation getOptimizedViewportRotation(OpenCvCameraRotation streamRotation) { + if (!cameraOrientationIsTiedToDeviceOrientation()) { + return OpenCvViewport.OptimizedRotation.NONE; + } + + if (streamRotation == OpenCvCameraRotation.SIDEWAYS_LEFT || streamRotation == OpenCvCameraRotation.SENSOR_NATIVE) { + return OpenCvViewport.OptimizedRotation.ROT_90_COUNTERCLOCWISE; + } else if (streamRotation == OpenCvCameraRotation.SIDEWAYS_RIGHT) { + return OpenCvViewport.OptimizedRotation.ROT_90_CLOCKWISE; + } else if (streamRotation == OpenCvCameraRotation.UPSIDE_DOWN) { + return OpenCvViewport.OptimizedRotation.ROT_180; + } else { + return OpenCvViewport.OptimizedRotation.NONE; + } + } + + protected void setupViewport() { + viewport.setFpsMeterEnabled(fpsMeterDesired); + viewport.setRenderHook(PipelineRenderHook.INSTANCE); + } + + protected abstract OpenCvCameraRotation getDefaultRotation(); + + protected abstract int mapRotationEnumToOpenCvRotateCode(OpenCvCameraRotation rotation); + + protected abstract boolean cameraOrientationIsTiedToDeviceOrientation(); + + protected abstract boolean isStreaming(); + + protected Size getFrameSizeAfterRotation(int width, int height, OpenCvCameraRotation rotation) + { + int screenRenderedWidth, screenRenderedHeight; + int openCvRotateCode = mapRotationEnumToOpenCvRotateCode(rotation); + + if(openCvRotateCode == Core.ROTATE_90_CLOCKWISE || openCvRotateCode == Core.ROTATE_90_COUNTERCLOCKWISE) + { + //noinspection SuspiciousNameCombination + screenRenderedWidth = height; + //noinspection SuspiciousNameCombination + screenRenderedHeight = width; + } + else + { + screenRenderedWidth = width; + screenRenderedHeight = height; + } + + return new Size(screenRenderedWidth, screenRenderedHeight); + } + +} diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvWebcam.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvWebcam.java index 924116cc..d4d73b01 100644 --- a/Vision/src/main/java/org/openftc/easyopencv/OpenCvWebcam.java +++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvWebcam.java @@ -1,44 +1,124 @@ -/* - * Copyright (c) 2020 OpenFTC Team - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.openftc.easyopencv; - -public interface OpenCvWebcam extends OpenCvCamera { - - default void startStreaming(int width, int height, OpenCvCameraRotation cameraRotation, StreamFormat eocvStreamFormat) { - startStreaming(width, height, cameraRotation); - } - - enum StreamFormat - { - // The only format that was supported historically; it is uncompressed but - // chroma subsampled and uses lots of bandwidth - this limits frame rate - // at higher resolutions and also limits the ability to use two cameras - // on the same bus to lower resolutions - YUY2, - - // Compressed motion JPEG stream format; allows for higher resolutions at - // full frame rate, and better ability to use two cameras on the same bus. - // Requires extra CPU time to run decompression routine. - MJPEG; - } - -} +/* + * Copyright (c) 2020 OpenFTC Team + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.openftc.easyopencv; + +import org.firstinspires.ftc.robotcore.external.hardware.camera.controls.CameraControl; +import org.firstinspires.ftc.robotcore.external.hardware.camera.controls.ExposureControl; +import org.firstinspires.ftc.robotcore.external.hardware.camera.controls.FocusControl; +import org.firstinspires.ftc.robotcore.external.hardware.camera.controls.GainControl; +import org.firstinspires.ftc.robotcore.external.hardware.camera.controls.PtzControl; +import org.firstinspires.ftc.robotcore.external.hardware.camera.controls.WhiteBalanceControl; +import org.firstinspires.ftc.robotcore.internal.camera.calibration.CameraCalibrationIdentity; + +public interface OpenCvWebcam extends OpenCvCamera +{ + /** + * Set how long to wait for permission to open the + * camera before giving up + * @param ms milliseconds to wait for + */ + void setMillisecondsPermissionTimeout(int ms); + + enum StreamFormat + { + // The only format that was supported historically; it is uncompressed but + // chroma subsampled and uses lots of bandwidth - this limits frame rate + // at higher resolutions and also limits the ability to use two cameras + // on the same bus to lower resolutions + YUY2, + + // Compressed motion JPEG stream format; allows for higher resolutions at + // full frame rate, and better ability to use two cameras on the same bus. + // Requires extra CPU time to run decompression routine. + MJPEG; + } + + /** + * Same as {@link #startStreaming(int, int, OpenCvCameraRotation)} except for + * @param streamFormat indicates the desired stream format. + */ + void startStreaming(int width, int height, OpenCvCameraRotation rotation, StreamFormat streamFormat); + + /*** + * Gets the {@link ExposureControl} for this webcam. + * Please see that interface's javadoc for how to use + * it. It is an interface provided directly by the SDK + * UVC driver, not EasyOpenCV. + * + * @return the ExposureControl for this webcam + */ + ExposureControl getExposureControl(); + + /*** + * Gets the {@link FocusControl} for this webcam. + * Please see that interface's javadoc for how to use + * it. It is an interface provided directly by the SDK + * UVC driver, not EasyOpenCV. + * + * @return the FocusControl for this webcam + */ + FocusControl getFocusControl(); + + /*** + * Gets the {@link PtzControl} for this webcam. + * Please see that interface's javadoc for how to use + * it. It is an interface provided directly by the SDK + * UVC driver, not EasyOpenCV. + * + * @return the PtzControl for this webcam + */ + PtzControl getPtzControl(); + + /*** + * Gets the {@link GainControl} for this webcam. + * Please see that interface's javadoc for how to use + * it. It is an interface provided directly by the SDK + * UVC driver, not EasyOpenCV. + * + * @return the GainControl for this webcam + */ + GainControl getGainControl(); + + /*** + * Gets the {@link WhiteBalanceControl} for this webcam. + * Please see that interface's javadoc for how to use + * it. It is an interface provided directly by the SDK + * UVC driver, not EasyOpenCV. + * + * @return the WhiteBalanceControl for this webcam + */ + WhiteBalanceControl getWhiteBalanceControl(); + + /** + * Gets a control for this webcam + * @param controlType the class of the control to get + * @return the requested control + */ + T getControl(Class controlType); + + /** + * Gets the {@link CameraCalibrationIdentity} for this webcam. + * Note: the camera must be opened before calling this! + * @return the CameraCalibrationIdentity for this webcam + */ + CameraCalibrationIdentity getCalibrationIdentity(); +} \ No newline at end of file diff --git a/build.common.gradle b/build.common.gradle index 07f92ee5..043571e7 100644 --- a/build.common.gradle +++ b/build.common.gradle @@ -1,5 +1,5 @@ -java { - toolchain { - languageVersion = JavaLanguageVersion.of(10) - } +java { + toolchain { + languageVersion = JavaLanguageVersion.of(11) + } } \ No newline at end of file diff --git a/build.gradle b/build.gradle index 33730078..e5d86bef 100644 --- a/build.gradle +++ b/build.gradle @@ -6,16 +6,22 @@ buildscript { ext { kotlin_version = "1.9.0" kotlinx_coroutines_version = "1.5.0-native-mt" - slf4j_version = "1.7.32" - log4j_version = "2.17.1" + slf4j_version = "2.0.16" + log4j_version = "2.24.1" opencv_version = "4.7.0-0" - apriltag_plugin_version = "2.0.0-C" + apriltag_plugin_version = "2.1.0-C" + skiko_version = "0.8.15" - classgraph_version = "4.8.108" + classgraph_version = "4.8.112" opencsv_version = "5.5.2" - env = findProperty('env') == 'release' ? 'release' : 'dev' + Penv = findProperty('env') + if(Penv != null && (Penv != 'dev' && Penv != 'release')) { + throw new GradleException("Invalid env property, must be 'dev' or 'release'") + } + + env = Penv == 'release' ? 'release' : 'dev' println("Current build is: $env") } @@ -28,6 +34,7 @@ buildscript { dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'gradle.plugin.com.github.johnrengelman:shadow:7.1.1' + classpath 'io.github.fvarrui:javapackager:1.7.6' } } @@ -37,10 +44,15 @@ plugins { allprojects { group 'com.github.deltacv' - version '3.7.1' + version '3.8.0' apply plugin: 'java' + configurations { + // do not pull in EOCV-Sim's Common module from a repository, it's already in the project + runtime.exclude group: "com.github.deltacv.EOCV-Sim", module: "Common" + } + ext { standardVersion = version } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index cf5dd7d2..731f0325 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists