Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions robot/src/main/kotlin/dev/nextftc/robot/Exceptions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright (c) 2026 NextFTC Team
*
* Use of this source code is governed by an BSD-3-clause
* license that can be found in the LICENSE.md file at the root of this repository or at
* https://opensource.org/license/bsd-3-clause.
*/

package dev.nextftc.robot

/**
* Base class for all exceptions thrown by NextFTC.
*/
open class NextFTCException(message: String, cause: Throwable? = null) : RuntimeException(message, cause)

/**
* Thrown when the [NextRobot] implementation in the user's project could not be found,
* instantiated, or unambiguously resolved by [RobotScanner].
*/
class RobotScanException(message: String, cause: Throwable? = null) : NextFTCException(message, cause)
83 changes: 83 additions & 0 deletions robot/src/main/kotlin/dev/nextftc/robot/RobotLog.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package dev.nextftc.robot

import dev.frozenmilk.sinister.util.log.Logger
import com.qualcomm.robotcore.util.RobotLog as SDKRobotLog

/**
* Logs to the robot log under the `NextFTC` tag.
*
* Use [Global] for messages that should also surface on the Driver Station.
*/
internal object RobotLog {
/** Logs [message] at debug level. */
fun debug(message: String) {
Logger.d("NextFTC", message)
}

/** Logs [message] at info level. */
fun info(message: String) {
Logger.i("NextFTC", message)
}

/** Logs [message] at warn level. */
fun warn(message: String) {
Logger.w("NextFTC", message)
}

/** Logs [message] at warn level, along with the stack trace of [throwable]. */
fun warn(message: String, throwable: Throwable) {
Logger.w("NextFTC", message, throwable)
}

/** Logs [message] at error level. */
fun error(message: String) {
Logger.e("NextFTC", message)
}

/** Logs [message] at error level, along with the stack trace of [throwable]. */
fun error(message: String, throwable: Throwable) {
Logger.e("NextFTC", message, throwable)
}

/**
* Messages shown to the driver on the Driver Station, prefixed with `NextFTC:`.
*/
object Global {
/** Replaces the global error message with [message]. */
fun setError(message: String) {
this@RobotLog.error(message)
SDKRobotLog.setGlobalErrorMsg("NextFTC: $message")
}

/**
* Replaces the global error message with [message], logging the stack trace of [throwable]
* alongside it.
*/
fun setError(message: String, throwable: Throwable) {
this@RobotLog.error(message, throwable)
SDKRobotLog.setGlobalErrorMsg("NextFTC: $message")
}

/**
* Replaces the global error message with [message] and throws [exception] (defaulting to a [NextFTCException] with [message]).
*/
fun setErrorAndThrow(
message: String,
exception: NextFTCException = NextFTCException(message),
): Nothing {
setError(message)
throw exception
}

/** Clears the global error message, if any. */
fun clearError() {
SDKRobotLog.clearGlobalErrorMsg()
}

/** Adds [message] to the list of global warnings. */
fun addWarning(message: String) {
warn(message)
SDKRobotLog.addGlobalWarningMessage("NextFTC: $message")
}
}
}
112 changes: 60 additions & 52 deletions robot/src/main/kotlin/dev/nextftc/robot/RobotScanner.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,17 @@ package dev.nextftc.robot
import android.content.Context
import com.qualcomm.ftccommon.FtcEventLoop
import com.qualcomm.robotcore.eventloop.opmode.Disabled
import com.qualcomm.robotcore.util.RobotLog
import dev.frozenmilk.sinister.Scanner
import dev.frozenmilk.sinister.sdk.apphooks.OnCreateEventLoop
import dev.frozenmilk.sinister.sdk.apphooks.OnCreateEventLoopScanner
import dev.frozenmilk.sinister.targeting.SearchTarget
import dev.frozenmilk.sinister.targeting.WideSearch
import dev.frozenmilk.sinister.util.log.Logger
import dev.frozenmilk.util.graph.Graph
import dev.frozenmilk.util.graph.rule.AdjacencyRule
import dev.frozenmilk.util.graph.rule.dependsOn
import java.lang.reflect.Modifier
import kotlin.reflect.KClass
import kotlin.reflect.KVisibility
import kotlin.reflect.full.hasAnnotation

/**
Expand All @@ -31,13 +30,9 @@ import kotlin.reflect.full.hasAnnotation
* [dev.nextftc.robot.opmode.NextFTCOpModeScanner] can properly inject the robot instance into OpModes.
*/
internal object RobotScanner : Scanner {
internal var robotClass: KClass<out NextRobot>? = null
internal var allRobotClasses: MutableList<KClass<out NextRobot>> = mutableListOf()
private var robotConstructor: (() -> NextRobot)? = null
private var robotLoader: ClassLoader? = null
internal var robotInstance: NextRobot? = null

var foundRobot = false
var foundMultiple = false

override val loadAdjacencyRule: AdjacencyRule<Scanner, Graph<Scanner>> =
Scanner.INDEPENDENT and dependsOn(OnCreateEventLoopScanner)
Expand All @@ -54,78 +49,86 @@ internal object RobotScanner : Scanner {
val kcls = cls.kotlin

if (kcls.hasAnnotation<Disabled>()) {
Logger.i("NextFTC", "Skipping disabled NextFTC robot class: $kcls")
RobotLog.setGlobalErrorMsg("Skipping disabled NextFTC robot class: $kcls")
RobotLog.info("Skipping disabled NextFTC robot class: $kcls")
return
}

Logger.i("NextFTC", "Found NextFTC robot class: $kcls")
RobotLog.setGlobalErrorMsg("Found NextFTC robot class: $kcls")
RobotLog.info("Found NextFTC robot class: $kcls")
allRobotClasses.add(cls.asSubclass(NextRobot::class.java).kotlin)
// Recorded for every candidate, not just usable ones, so that [beforeUnload] always clears the
// state this scan added.
robotLoader = loader

val objectInstance = kcls.objectInstance

if (objectInstance != null) {
robotConstructor = { objectInstance as NextRobot }
robotClass = cls.asSubclass(NextRobot::class.java).kotlin
robotLoader = loader

if (foundRobot) {
foundMultiple = true
}
foundRobot = true
return
}

val constructor = kcls.constructors.find { it.parameters.isEmpty() }
val constructor = kcls.constructors.find {
it.parameters.isEmpty() && it.visibility == KVisibility.PUBLIC
}
if (constructor != null) {
robotConstructor = { constructor.call() as NextRobot }
robotClass = cls.asSubclass(NextRobot::class.java).kotlin
robotLoader = loader

if (foundRobot) {
foundMultiple = true
}
foundRobot = true
return
}

Logger.w(
"NextFTC",
buildString {
append("Unable to find appropriate constructor for $cls. ")
append("Ensure it is either a singleton object or has a public no-argument constructor.")
},
)
RobotLog.setGlobalErrorMsg(
"Unable to find appropriate constructor for $cls. " +
"Ensure it is either a singleton object or has a public no-argument constructor.",
)
val message = "Unable to find appropriate constructor for $cls. " +
"Ensure it is either a singleton object or has a public no-argument constructor."
RobotLog.Global.addWarning(message)
}

override fun afterScan(loader: ClassLoader) {
check(!foundMultiple) {
"Found multiple NextFTC robot classes. Please ensure that there is only one in your project."
if (allRobotClasses.isEmpty()) {
RobotLog.Global.setError(
"No NextFTC robot class found. Ensure you have a class that implements NextRobot.",
)
return
}
check(foundRobot) {
"Unable to find a NextFTC robot class. Please ensure that there is one in your project " +
"(a class or object implementing NextRobot with a public no-argument constructor)."

if (allRobotClasses.size > 1) {
RobotLog.Global.addWarning(
"Multiple NextFTC robot classes found: $allRobotClasses. " +
"Ensure you have only one class that implements NextRobot.",
)
return
}

Logger.i("NextFTC", "Found NextFTC robot class: $robotClass")
RobotLog.setGlobalErrorMsg("Found NextFTC robot class: $robotClass")
val robotClass = allRobotClasses[0]
val constructor = robotConstructor

check(robotConstructor != null) { "Robot constructor is null after scan" }
robotInstance = robotConstructor?.invoke()
if (constructor == null) {
RobotLog.Global.setError(
"No usable constructor was found for $robotClass, so it could not be created. " +
"Ensure it is either a singleton object or has a public no-argument constructor.",
)
return
}

RobotLog.info("Using NextFTC robot class: $robotClass")
RobotState.robotClass = robotClass

RobotState.robotInstance = try {
constructor()
} catch (throwable: Throwable) {
RobotLog.Global.setError(
"Failed to create an instance of $robotClass. " +
"Check its constructor and property initializers for code that throws.",
throwable,
)
RobotState.robotClass = null
return
}
}

override fun beforeUnload(loader: ClassLoader) {
if (loader == robotLoader) {
foundRobot = false
foundMultiple = false
robotClass = null
robotConstructor = null
robotLoader = null
robotInstance = null
allRobotClasses.clear()
RobotState.robotClass = null
RobotState.robotInstance = null
}
}

Expand All @@ -137,8 +140,13 @@ internal object RobotScanner : Scanner {
* The robot instance is created using the constructor found by [RobotScanner].
*/
object RobotState : OnCreateEventLoop {
val robot get() = RobotScanner.robotInstance ?: error("Robot instance not initialized")
internal val robotClass get() = RobotScanner.robotClass ?: error("Robot class not initialized")
internal var robotInstance: NextRobot? = null
internal var robotClass: KClass<out NextRobot>? = null
val robot: NextRobot
get() = robotInstance ?: RobotLog.Global.setErrorAndThrow(
"Robot instance not initialized",
RobotScanException("Robot instance not initialized"),
)

override fun onCreateEventLoop(context: Context, ftcEventLoop: FtcEventLoop) {
ftcEventLoop.opModeManager.registerListener(DriverStationTelemetry)
Expand Down
Loading
Loading