Added kotlin support and docs
Adds a "kotlin support" page within the docs that provides a kotlin replacement for the AutoLog annotation. Resolves #64 and #86.
Note: this probably should be merged after the 2025-docs-dev branch is merged.
I was able to get Kotlin to work with the annotations and minimal changes with KAPT and this build.gradle:
plugins {
id "java"
id "edu.wpi.first.GradleRIO" version "2025.1.1"
id "com.diffplug.spotless" version "7.0.1" // formatting
id "org.jetbrains.kotlin.jvm" version "2.1.10" // kotlin support
id "com.peterabeles.gversion" version "1.10" // for advantagekit
// id "com.google.devtools.ksp" version "2.1.10-1.0.31" // for annotation processing
id "org.jetbrains.kotlin.kapt" version "2.1.10"
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
def ROBOT_MAIN_CLASS = "com.eaganrobotics.frc.Main"
// Define my targets (RoboRIO) and artifacts (deployable files)
// This is added by GradleRIO"s backing project DeployUtils.
deploy {
targets {
roborio(getTargetTypeClass("RoboRIO")) {
// Team number is loaded either from the .wpilib/wpilib_preferences.json
// or from command line. If not found an exception will be thrown.
// You can use getTeamOrDefault(team) instead of getTeamNumber if you
// want to store a team number in this file.
team = project.frc.getTeamNumber()
debug = project.frc.getDebugOrDefault(false)
artifacts {
// First part is artifact name, 2nd is artifact type
// getTargetTypeClass is a shortcut to get the class type using a string
frcJava(getArtifactTypeClass("FRCJavaArtifact")) {
jvmArgs.add("-XX:+UnlockExperimentalVMOptions")
jvmArgs.add("-XX:GCTimeRatio=5")
jvmArgs.add("-XX:+UseSerialGC")
jvmArgs.add("-XX:MaxGCPauseMillis=50")
final MAX_JAVA_HEAP_SIZE_MB = 100;
jvmArgs.add("-Xmx" + MAX_JAVA_HEAP_SIZE_MB + "M")
jvmArgs.add("-Xms" + MAX_JAVA_HEAP_SIZE_MB + "M")
jvmArgs.add("-XX:+AlwaysPreTouch")
}
// Static files artifact
frcStaticFileDeploy(getArtifactTypeClass("FileTreeArtifact")) {
files = project.fileTree("src/main/deploy")
directory = "/home/lvuser/deploy"
deleteOldFiles = true
}
}
}
}
}
def deployArtifact = deploy.targets.roborio.artifacts.frcJava
// Set to true to use debug for JNI.
wpi.java.debugJni = false
// Set this to true to enable desktop support.
def includeDesktopSupport = true
// Defining my dependencies. In this case, WPILib (+ friends), and vendor libraries.
// Also defines JUnit 5.
dependencies {
annotationProcessor wpi.java.deps.wpilibAnnotations()
implementation wpi.java.deps.wpilib()
implementation wpi.java.vendor.java()
roborioDebug wpi.java.deps.wpilibJniDebug(wpi.platforms.roborio)
roborioDebug wpi.java.vendor.jniDebug(wpi.platforms.roborio)
roborioRelease wpi.java.deps.wpilibJniRelease(wpi.platforms.roborio)
roborioRelease wpi.java.vendor.jniRelease(wpi.platforms.roborio)
nativeDebug wpi.java.deps.wpilibJniDebug(wpi.platforms.desktop)
nativeDebug wpi.java.vendor.jniDebug(wpi.platforms.desktop)
simulationDebug wpi.sim.enableDebug()
nativeRelease wpi.java.deps.wpilibJniRelease(wpi.platforms.desktop)
nativeRelease wpi.java.vendor.jniRelease(wpi.platforms.desktop)
simulationRelease wpi.sim.enableRelease()
testImplementation "org.junit.jupiter:junit-jupiter:5.10.1"
testRuntimeOnly "org.junit.platform:junit-platform-launcher"
implementation "org.jetbrains.kotlin:kotlin-reflect"
implementation "org.jetbrains.kotlin:kotlin-stdlib"
// advantagekit
def akitJson = new groovy.json.JsonSlurper().parseText(new File(projectDir.getAbsolutePath() + "/vendordeps/AdvantageKit.json").text)
// implementation "org.littletonrobotics.akit:akit-autolog:$akitJson.version"
kapt "org.littletonrobotics.akit:akit-autolog:$akitJson.version"
}
test {
useJUnitPlatform()
systemProperty "junit.jupiter.extensions.autodetection.enabled", "true"
}
// Simulation configuration (e.g. environment variables).
wpi.sim.addGui().defaultEnabled = providers.environmentVariable("AKIT_SIM_MODE").map(env -> env.equals("SIM")).orElse(true)
wpi.sim.addDriverstation()
// Setting up my Jar File. In this case, adding all libraries into the main jar ("fat jar")
// in order to make them all available at runtime. Also adding the manifest so WPILib
// knows where to look for our Robot Class.
jar {
from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } }
from sourceSets.main.allSource
manifest edu.wpi.first.gradlerio.GradleRIOPlugin.javaManifest(ROBOT_MAIN_CLASS)
duplicatesStrategy = DuplicatesStrategy.INCLUDE
}
// Configure jar and deploy tasks
deployArtifact.jarTask = jar
wpi.java.configureExecutableTasks(jar)
wpi.java.configureTestTasks(test)
// Configure string concat to always inline compile
tasks.withType(JavaCompile) {
options.compilerArgs.add "-XDstringConcat=inline"
}
// advantagekit replay watch
task(replayWatch, type: JavaExec) {
mainClass = "org.littletonrobotics.junction.ReplayWatch"
classpath = sourceSets.main.runtimeClasspath
}
// version control for advantagekit for event autodeploy
project.compileJava.dependsOn(createVersionFile)
gversion {
srcDir = "src/main/kotlin/"
classPackage = "com.eaganrobotics.frc"
className = "BuildConstants"
dateFormat = "yyyy-MM-dd HH:mm:ss z"
timeZone = "America/Central" // Use preferred time zone
indent = " "
language = "kotlin"
}
// event auto commit when working in branches prefixed with "event/"
task(eventDeploy) {
doLast {
if (project.gradle.startParameter.taskNames.any({ it.toLowerCase().contains("deploy") })) {
def branchPrefix = "event/"
def branch = "git branch --show-current".execute().text.trim()
def commitMessage = "Update at "${new Date().toString()}""
if (branch.startsWith(branchPrefix)) {
exec {
workingDir(projectDir)
executable "git"
args "add", "-A"
}
exec {
workingDir(projectDir)
executable "git"
args "commit", "-m", commitMessage
ignoreExitValue = true
}
println "Committed to branch: "$branch""
println "Commit message: "$commitMessage""
} else {
println "Not on an event branch, skipping commit"
}
} else {
println "Not running deploy task, skipping commit"
}
}
}
createVersionFile.dependsOn(eventDeploy)
// formatting task
spotless {
java {
googleJavaFormat()
formatJavadoc(true)
reorderImports(true)
targetExclude("**/build/**", "**/build-*/**", "src/main/java/frc/robot/BuildConstants.java")
removeUnusedImports()
trimTrailingWhitespace()
endWithNewline()
licenseHeader("// Copyright (c) 2025 FRC 2220\n// http://github.com/EaganRobotics\n//\n// Use of this source code is governed by an MIT-style\n// license that can be found in the LICENSE file at\n// the root directory of this project.\n\n")
manageTrailingCommas(true)
}
kotlin {
// version, style and all configurations here are optional
ktfmt("0.54") {
googleStyle()
formatJavadoc(true)
reorderImports(true)
targetExclude("**/build/**", "**/build-*/**", "src/main/kotlin/com/eaganarobotics/frc/BuildConstants.kt")
removeUnusedImports()
trimTrailingWhitespace()
endWithNewline()
licenseHeader("// Copyright (c) 2025 FRC 2220\n// http://github.com/EaganRobotics\n//\n// Use of this source code is governed by an MIT-style\n// license that can be found in the LICENSE file at\n// the root directory of this project.\n\n")
manageTrailingCommas(true)
}
}
groovyGradle {
target fileTree(".") {
include "**/*.gradle"
exclude "**/build/**", "**/build-*/**", "northstar/**"
}
greclipse()
indentWithSpaces(4)
trimTrailingWhitespace()
endWithNewline()
}
json {
target fileTree(".") {
include "**/*.json"
exclude "**/build/**", "**/build-*/**", "northstar/**"
}
gson().indentWithSpaces(2)
}
enforceCheck = false
}
and here's an IO class example:
interface LiftIO {
companion object {
@AutoLog
open class LiftIOInputs {
@JvmField var lowerLimit = false
@JvmField var winchPosition = Radians.of(0.0)
@JvmField var winchVelocity = RadiansPerSecond.of(0.0)
@JvmField var winchAppliedVoltage = Volts.of(0.0)
@JvmField var winchCurrent = Amps.of(0.0)
}
}
fun updateInputs(inputs: LiftIOInputs): Unit {}
fun setWinchOpenLoop(output: Voltage): Unit {}
fun setWinchClosedLoop(position: Angle): Unit {}
fun zeroWinchEncoder(): Unit {}
}
Just @ me if there are any questions! We're switching to kotlin for next season and AKit support was a hard stop for us.
Yeah I was aware of the workaround where u use kapt and @JvmField your properties. I don't like it as much though because typing out @JvmField for everything feels annoying(and might not be intuitive for new teams) but it would be supported better.
Second thing: your inputs class shouldn't be in a companion object. Classes defined directly within other classes are static by default, so you can do this:
interface LiftIO {
@AutoLog
open class LiftIOInputs {
@JvmField var lowerLimit = false
@JvmField var winchPosition = Radians.of(0.0)
@JvmField var winchVelocity = RadiansPerSecond.of(0.0)
@JvmField var winchAppliedVoltage = Volts.of(0.0)
@JvmField var winchCurrent = Amps.of(0.0)
}
fun updateInputs(inputs: LiftIOInputs) {}
fun setWinchOpenLoop(output: Voltage) {}
fun setWinchClosedLoop(position: Angle) {}
fun zeroWinchEncoder() {}
}
(you also don't need the Unit return type since its basically 'void' in java, and it is returned by default if you dont specify a return type). I personally prefer the style where the lift inputs and lift io are separate classes defined within the same file, so
@AutoLog
open class LiftIOInputs {
@JvmField var lowerLimit = false
@JvmField var winchPosition = Radians.of(0.0)
@JvmField var winchVelocity = RadiansPerSecond.of(0.0)
@JvmField var winchAppliedVoltage = Volts.of(0.0)
@JvmField var winchCurrent = Amps.of(0.0)
}
interface LiftIO {
fun updateInputs(inputs: LiftIOInputs) {}
fun setWinchOpenLoop(output: Voltage) {}
fun setWinchClosedLoop(position: Angle) {}
fun zeroWinchEncoder() {}
}
Thank you for the work on that; it has been helpful to us.
I was wondering if there's a way to enable @AutoLogOutput for variables created in files outside classes?