diff --git a/.gitignore b/.gitignore
index c0a0119..e771d84 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,8 +1,13 @@
+*.iml
+*.ipr
+*.iws
.classpath
.gradle/
+.idea/
.project
.settings/
.vscode/
bin/
build/
+out/
virtualenv_run/
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 0000000..9707df0
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..11c5dcb
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..dcb6b8c
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
new file mode 100644
index 0000000..a558a08
--- /dev/null
+++ b/.idea/workspace.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/README.md b/README.md
index 755878e..f1bcbec 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,4 @@
-# MonkeyLib
-
-[](https://travis-ci.org/team5499/MonkeyLib)
-[](https://codecov.io/gh/team5499/MonkeyLib)
-
-MonkeyLib is team 5499's base kotlin code used in all of our robots. Mostly contains math and utilities
+# Fault
+
+My own branch of Fault
+
diff --git a/build.gradle b/build.gradle
index cc267dd..fda9532 100644
--- a/build.gradle
+++ b/build.gradle
@@ -8,6 +8,7 @@ plugins {
id 'jacoco'
id 'maven'
id 'org.jetbrains.dokka' version '0.9.17'
+ id 'idea'
}
repositories {
@@ -32,10 +33,12 @@ dependencies {
testCompile 'org.powermock:powermock-module-junit4:1.6.6'
testCompile 'org.powermock:powermock-api-mockito2:1.6.6'
+ testCompile group: 'org.knowm.xchart', name: 'xchart', version: '3.5.4'
+
}
-group = 'org.team5499'
-version = '2.11.0'
+group = 'org.team5419'
+version = '3.0.0-SNAPSHOT'
task sourcesJar(type: Jar) {
@@ -106,14 +109,14 @@ task install_hooks(dependsOn: 'tox')
publishing {
publications {
library(MavenPublication) {
- artifactId = 'monkey-lib'
+ artifactId = 'fault'
from components.java
artifact sourcesJar
artifact javadocJar
pom {
- name = 'MonkeyLib'
- description = 'Commonly used utilities in FRC Team 5499\'s codebase'
- url = 'https://github.com/team5499/MonkeyLib'
+ name = 'Fault'
+ description = 'Commonly used utilities in FRC Team 5419\'s codebase'
+ url = 'https://github.com/team5419/fault'
}
}
}
diff --git a/src/main/kotlin/org/team5419/fault/BerkeliumRobot.kt b/src/main/kotlin/org/team5419/fault/BerkeliumRobot.kt
new file mode 100644
index 0000000..7b92167
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/BerkeliumRobot.kt
@@ -0,0 +1,90 @@
+package org.team5419.fault
+
+import edu.wpi.first.hal.FRCNetComm
+import edu.wpi.first.hal.HAL
+import edu.wpi.first.wpilibj.RobotBase
+import edu.wpi.first.wpilibj.TimedRobot
+import org.team5419.fault.math.units.SIUnit
+import org.team5419.fault.math.units.Second
+import org.team5419.fault.subsystems.Subsystem
+import org.team5419.fault.subsystems.SubsystemManager
+
+@Suppress("TooManyFunctions")
+abstract class BerkeliumRobot(period: SIUnit) {
+
+ protected val wrappedValue = WpiTimedRobot(period.value)
+
+ protected inner class WpiTimedRobot(period: Double = 0.05) : TimedRobot(period) {
+
+ private val kLanguageKotlin = 6
+
+ init {
+ HAL.report(FRCNetComm.tResourceType.kResourceType_Language, kLanguageKotlin)
+ }
+
+ override fun robotInit() {
+ this@BerkeliumRobot.robotInit()
+ SubsystemManager.init()
+ }
+
+ override fun autonomousInit() {
+ this@BerkeliumRobot.autonomousInit()
+ SubsystemManager.autoReset()
+ }
+
+ override fun teleopInit() {
+ this@BerkeliumRobot.teleopInit()
+ SubsystemManager.teleopReset()
+ }
+
+ override fun disabledInit() {
+ this@BerkeliumRobot.disabledInit()
+ SubsystemManager.zeroOutputs()
+ }
+
+ override fun testInit() {
+ this@BerkeliumRobot.testInit()
+ }
+
+ override fun robotPeriodic() {
+ this@BerkeliumRobot.robotPeriodic()
+ if (!isDisabled) SubsystemManager.periodic()
+ }
+
+ override fun autonomousPeriodic() {
+ this@BerkeliumRobot.autonomousPeriodic()
+ }
+
+ override fun teleopPeriodic() {
+ this@BerkeliumRobot.teleopPeriodic()
+ }
+
+ override fun disabledPeriodic() {
+ this@BerkeliumRobot.disabledPeriodic()
+ }
+
+ override fun testPeriodic() {
+ this@BerkeliumRobot.testPeriodic()
+ }
+ }
+
+ protected open fun robotInit() {}
+ protected open fun autonomousInit() {}
+ protected open fun teleopInit() {}
+ protected open fun disabledInit() {}
+ protected open fun testInit() {}
+
+ protected open fun robotPeriodic() {}
+ protected open fun autonomousPeriodic() {}
+ protected open fun teleopPeriodic() {}
+ protected open fun disabledPeriodic() {}
+ protected open fun testPeriodic() {}
+
+ open operator fun Subsystem.unaryPlus() {
+ SubsystemManager.addSubsystem(this)
+ }
+
+ fun start() {
+ RobotBase.startRobot { wrappedValue }
+ }
+}
diff --git a/src/main/kotlin/org/team5419/fault/Controller.kt b/src/main/kotlin/org/team5419/fault/Controller.kt
new file mode 100644
index 0000000..39dbf01
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/Controller.kt
@@ -0,0 +1,10 @@
+package org.team5419.fault
+
+interface Controller {
+
+ fun start()
+
+ fun update()
+
+ fun reset()
+}
diff --git a/src/main/kotlin/org/team5419/fault/auto/Action.kt b/src/main/kotlin/org/team5419/fault/auto/Action.kt
new file mode 100644
index 0000000..e00e887
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/auto/Action.kt
@@ -0,0 +1,91 @@
+package org.team5419.fault.auto
+
+import org.team5419.fault.math.units.SIUnit
+import org.team5419.fault.math.units.SIUnitBuilder
+import org.team5419.fault.math.units.Second
+import org.team5419.fault.util.BooleanSource
+import org.team5419.fault.util.Source
+import org.team5419.fault.util.or
+import org.team5419.fault.util.time.ITimer
+import org.team5419.fault.util.time.WPITimer
+
+typealias NothingAction = Action
+
+open class Action(
+ private val timer: ITimer = WPITimer()
+) {
+
+ protected var mTimeout = SIUnitBuilder(0.0).seconds
+
+ protected var finishCondition = FinishCondition(Source(false))
+
+ open fun start() {
+ timer.stop()
+ timer.reset()
+ timer.start()
+ }
+
+ open fun update() {}
+
+ protected fun timedOut() = (timer.get() >= mTimeout)
+
+ open fun next(): Boolean {
+ return finishCondition()
+ }
+
+ open fun finish() {}
+
+ protected class FinishCondition(
+ private var source: BooleanSource
+ ) : BooleanSource {
+ override fun invoke() = source()
+
+ operator fun plusAssign(other: BooleanSource) {
+ source = source or other
+ }
+
+ fun set(other: BooleanSource) {
+ source = other
+ }
+ }
+
+ fun withExit(condition: BooleanSource) = also { finishCondition += condition }
+
+ fun overrideExit(condition: BooleanSource) = also { finishCondition.set(condition) }
+
+ fun withTimeout(time: SIUnit) = also {
+ this.mTimeout = time
+ finishCondition += { timedOut() }
+ }
+}
+
+open class ConditionalAction(
+ private val condition: BooleanSource,
+ private val ifTrue: Action? = null,
+ private val ifFalse: Action? = null
+) : Action() {
+
+ private var selectedAction: Action? = null
+ private val selectedActionDone = { if (selectedAction != null) selectedAction!!.next() else true }
+
+ init {
+ finishCondition += { timedOut() }
+ finishCondition += selectedActionDone
+ }
+
+ override fun start() {
+ super.start()
+ selectedAction = if (condition()) ifTrue else ifFalse
+ if (selectedAction != null) selectedAction!!.start()
+ }
+
+ override fun update() {
+ super.update()
+ if (selectedAction != null) selectedAction!!.update()
+ }
+
+ override fun finish() {
+ if (selectedAction != null) selectedAction!!.finish()
+ selectedAction = null
+ }
+}
diff --git a/src/main/kotlin/org/team5419/fault/auto/ActionGroup.kt b/src/main/kotlin/org/team5419/fault/auto/ActionGroup.kt
new file mode 100644
index 0000000..1e4a311
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/auto/ActionGroup.kt
@@ -0,0 +1,94 @@
+package org.team5419.fault.auto
+
+abstract class ActionGroup internal constructor() : Action() {
+
+ internal abstract val actions: List
+}
+
+open class ParallelAction(actions: List) : ActionGroup() {
+
+ override val actions: List = actions
+
+ protected val actionMap = mutableMapOf()
+
+ constructor(vararg actions: Action) : this(actions.toList())
+
+ init {
+ finishCondition += {
+ var allDone = true
+ actions.forEach { allDone = allDone && it.next() }
+ allDone
+ }
+
+ actions.forEach { actionMap.put(it, false) }
+ }
+
+ override fun start() {
+ super.start()
+ actionMap.keys.forEach {
+ actionMap.set(it, false)
+ it.start()
+ }
+ }
+
+ override fun update() {
+ val iterator = actionMap.iterator()
+ while (iterator.hasNext()) {
+ val (action, finished) = iterator.next()
+ if (finished) continue
+ if (!finished && action.next()) {
+ action.finish()
+ actionMap.set(action, true)
+ } else {
+ action.update()
+ }
+ }
+ }
+
+ override fun finish() {
+ actionMap.forEach {
+ if (!it.value) it.key.finish()
+ }
+ }
+}
+
+open class SerialAction(actions: List) : ActionGroup() {
+
+ constructor(vararg actions: Action) : this(actions.toList())
+
+ private var index = 0
+ private val isLastAction = { index == (actions.size - 1) }
+ private val isLastActionDone = { actions.last().next() }
+
+ override val actions: List
+
+ init {
+ finishCondition += { isLastAction() }
+ finishCondition += { isLastActionDone() }
+ this.actions = actions.toMutableList()
+ assert(this.actions.size < 1) { "No actions added to command group" }
+ index = 0
+ }
+
+ override fun start() {
+ super.start()
+ index = 0
+ actions[index].start()
+ }
+
+ override fun update() {
+ super.update()
+ if (isLastAction() && isLastActionDone()) return
+ else if (actions[index].next() && !isLastAction()) {
+ actions[index].finish()
+ index++
+ actions[index].start()
+ }
+ actions[index].update()
+ }
+
+ override fun finish() {
+ super.finish()
+ actions.last().finish()
+ }
+}
diff --git a/src/main/kotlin/org/team5419/fault/auto/ActionUtil.kt b/src/main/kotlin/org/team5419/fault/auto/ActionUtil.kt
new file mode 100644
index 0000000..e95aff9
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/auto/ActionUtil.kt
@@ -0,0 +1,26 @@
+package org.team5419.fault.auto
+
+enum class GroupType { Parallel, Serial }
+
+interface IActionGroupBuilder {
+
+ fun build(): ActionGroup
+}
+
+class ActionGroupBuilder(private val type: GroupType) : IActionGroupBuilder {
+ private val actions = mutableListOf()
+
+ operator fun Action.unaryPlus() = actions.add(this)
+
+ override fun build() = when (type) {
+ GroupType.Serial -> SerialAction(actions)
+ GroupType.Parallel -> ParallelAction(actions)
+ }
+}
+
+fun serial(block: ActionGroupBuilder.() -> Unit) = commandGroup(GroupType.Serial, block)
+
+fun parallel(block: ActionGroupBuilder.() -> Unit) = commandGroup(GroupType.Parallel, block)
+
+private fun commandGroup(type: GroupType, block: ActionGroupBuilder.() -> Unit) =
+ ActionGroupBuilder(type).apply(block).build()
diff --git a/src/main/kotlin/org/team5419/fault/auto/DriveStraightAction.kt b/src/main/kotlin/org/team5419/fault/auto/DriveStraightAction.kt
new file mode 100644
index 0000000..c794369
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/auto/DriveStraightAction.kt
@@ -0,0 +1,39 @@
+package org.team5419.fault.auto
+
+import org.team5419.fault.math.units.Meter
+import org.team5419.fault.math.units.SIUnit
+import org.team5419.fault.math.units.derived.LinearVelocity
+import org.team5419.fault.math.units.derived.velocity
+import org.team5419.fault.math.units.inches
+import org.team5419.fault.subsystems.drivetrain.AbstractTankDrive
+import kotlin.math.absoluteValue
+
+class DriveStraightAction(
+ private val drivetrain: AbstractTankDrive,
+ private val distance: SIUnit,
+ private val acceptableDistanceError: SIUnit = 2.inches, // inches
+ private val acceptableVelocityError: SIUnit = 2.inches.velocity // inches / s
+) : Action() {
+
+ private val distanceErrorAcceptable = {
+ drivetrain.leftDistanceError.absoluteValue < acceptableDistanceError &&
+ drivetrain.rightDistanceError.absoluteValue < acceptableDistanceError
+ }
+
+ private val velocityErrorAcceptable = {
+ drivetrain.leftMasterMotor.encoder.velocity.absoluteValue < acceptableVelocityError.value &&
+ drivetrain.rightMasterMotor.encoder.velocity.absoluteValue < acceptableVelocityError.value
+ }
+
+ init {
+ finishCondition += distanceErrorAcceptable
+ finishCondition += velocityErrorAcceptable
+ }
+
+ override fun start() {
+ super.start()
+ drivetrain.setPosition(distance)
+ }
+
+ override fun finish() = drivetrain.zeroOutputs()
+}
diff --git a/src/main/kotlin/org/team5419/fault/auto/DriveTrajectoryAction.kt b/src/main/kotlin/org/team5419/fault/auto/DriveTrajectoryAction.kt
new file mode 100644
index 0000000..1b43870
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/auto/DriveTrajectoryAction.kt
@@ -0,0 +1,36 @@
+package org.team5419.fault.auto
+
+import org.team5419.fault.trajectory.types.Trajectory
+import org.team5419.fault.trajectory.types.TimedEntry
+import org.team5419.fault.math.geometry.Pose2dWithCurvature
+import org.team5419.fault.math.units.SIUnit
+import org.team5419.fault.math.units.Second
+import org.team5419.fault.subsystems.drivetrain.AbstractTankDrive
+import org.team5419.fault.trajectory.followers.TrajectoryFollower
+
+public class DriveTrajectoryAction(
+ private val drivetrain: AbstractTankDrive,
+ private val trajectoryFollower: TrajectoryFollower,
+ private val trajectory: Trajectory, TimedEntry>
+) : Action() {
+
+ init {
+ finishCondition += { trajectoryFollower.isFinished }
+ }
+
+ override fun start() {
+ trajectoryFollower.reset(trajectory)
+ }
+
+ override fun update() {
+ drivetrain.setOutput(trajectoryFollower.nextState(drivetrain.robotPosition))
+// val referencePoint = trajectoryFollower.referencePoint
+// if (referencePoint != null) {
+// val referencePose = referencePoint.state.state.pose
+// }
+ }
+
+ override fun finish() {
+ drivetrain.zeroOutputs()
+ }
+}
diff --git a/src/main/kotlin/org/team5419/fault/auto/DriveTurnAction.kt b/src/main/kotlin/org/team5419/fault/auto/DriveTurnAction.kt
new file mode 100644
index 0000000..9749c78
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/auto/DriveTurnAction.kt
@@ -0,0 +1,36 @@
+package org.team5419.fault.auto
+
+import org.team5419.fault.math.geometry.Rotation2d
+import org.team5419.fault.math.units.SIUnit
+import org.team5419.fault.math.units.derived.Radian
+import org.team5419.fault.math.units.derived.degrees
+import org.team5419.fault.math.units.derived.velocity
+import org.team5419.fault.math.units.derived.AngularVelocity
+
+import org.team5419.fault.subsystems.drivetrain.AbstractTankDrive
+
+// ALLOW for absolute turn in future (using rotation2d math maybe)
+class DriveTurnAction(
+ private val drivetrain: AbstractTankDrive,
+ private val rotation: Rotation2d,
+ private val turnType: AbstractTankDrive.TurnType = AbstractTankDrive.TurnType.Relative,
+ private val acceptableTurnError: SIUnit = 3.degrees, // degrees
+ private val acceptableVelocityError: SIUnit = 5.0.degrees.velocity // inches / s
+) : Action() {
+
+ private val turnErrorAcceptable = { drivetrain.turnError < acceptableTurnError }
+ private val angularVelocityAcceptable = { drivetrain.angularVelocity < acceptableVelocityError }
+
+ override fun start() {
+ super.start()
+ drivetrain.setTurn(rotation, turnType)
+
+ finishCondition += { turnErrorAcceptable() }
+ finishCondition += { angularVelocityAcceptable() }
+ }
+
+ override fun finish() {
+ super.finish()
+ drivetrain.zeroOutputs()
+ }
+}
diff --git a/src/main/kotlin/org/team5419/fault/auto/Routine.kt b/src/main/kotlin/org/team5419/fault/auto/Routine.kt
new file mode 100644
index 0000000..c96d8e3
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/auto/Routine.kt
@@ -0,0 +1,32 @@
+package org.team5419.fault.auto
+
+import org.team5419.fault.math.geometry.Pose2d
+import org.team5419.fault.math.geometry.Vector2
+import org.team5419.fault.math.units.derived.degrees
+import org.team5419.fault.math.units.meters
+import java.util.concurrent.atomic.AtomicLong
+
+class Routine(
+ name: String? = null,
+ val startPose: Pose2d = Pose2d(Vector2(0.0.meters, 0.0.meters), 0.degrees),
+ override val actions: MutableList
+) : SerialAction(actions) {
+
+ constructor(
+ name: String? = null,
+ startPose: Pose2d = Pose2d(Vector2(0.0.meters, 0.0.meters), 0.degrees),
+ vararg actions: Action
+ ) : this(name, startPose, actions.toMutableList())
+
+ val name = name ?: "Routine ${routineId.incrementAndGet()}"
+
+ companion object {
+ private val routineId = AtomicLong()
+ }
+}
+
+fun T.toRoutine(name: String? = null, startPose: Pose2d = Pose2d()) = Routine(
+ name,
+ startPose,
+ this.actions.toMutableList()
+)
diff --git a/src/main/kotlin/org/team5419/fault/hardware/AbstractBerkeliumEncoder.kt b/src/main/kotlin/org/team5419/fault/hardware/AbstractBerkeliumEncoder.kt
new file mode 100644
index 0000000..474501a
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/hardware/AbstractBerkeliumEncoder.kt
@@ -0,0 +1,13 @@
+package org.team5419.fault.hardware
+
+import org.team5419.fault.math.units.SIKey
+import org.team5419.fault.math.units.SIUnit
+import org.team5419.fault.math.units.derived.Velocity
+import org.team5419.fault.math.units.native.NativeUnitModel
+
+abstract class AbstractBerkeliumEncoder(
+ val model: NativeUnitModel
+) : BerkeliumEncoder {
+ override val position: SIUnit get() = model.fromNativeUnitPosition(rawPosition)
+ override val velocity: SIUnit> get() = model.fromNativeUnitVelocity(rawVelocity)
+}
diff --git a/src/main/kotlin/org/team5419/fault/hardware/AbstractBerkeliumMotor.kt b/src/main/kotlin/org/team5419/fault/hardware/AbstractBerkeliumMotor.kt
new file mode 100644
index 0000000..b393266
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/hardware/AbstractBerkeliumMotor.kt
@@ -0,0 +1,13 @@
+package org.team5419.fault.hardware
+
+import org.team5419.fault.math.units.SIKey
+
+abstract class AbstractBerkeliumMotor : BerkeliumMotor {
+
+ override var useMotionProfileForPosition = false
+
+ override fun follow(motor: BerkeliumMotor<*>): Boolean {
+ println("Cross brand following not implemented yet!")
+ return false
+ }
+}
diff --git a/src/main/kotlin/org/team5419/fault/hardware/BerkeliumEncoder.kt b/src/main/kotlin/org/team5419/fault/hardware/BerkeliumEncoder.kt
new file mode 100644
index 0000000..9351b15
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/hardware/BerkeliumEncoder.kt
@@ -0,0 +1,32 @@
+package org.team5419.fault.hardware
+
+import org.team5419.fault.math.units.SIKey
+import org.team5419.fault.math.units.SIUnit
+import org.team5419.fault.math.units.derived.Velocity
+import org.team5419.fault.math.units.native.NativeUnit
+import org.team5419.fault.math.units.native.NativeUnitVelocity
+
+interface BerkeliumEncoder {
+
+ /**
+ * The velocity of the encoder in [T]/s
+ */
+ val velocity: SIUnit>
+ /**
+ * The position of the encoder in [T]
+ */
+ val position: SIUnit
+
+ /**
+ * The velocity of the encoder in NativeUnits/s
+ */
+ val rawVelocity: SIUnit
+ /**
+ * The position of the encoder in NativeUnits
+ */
+ val rawPosition: SIUnit
+
+ fun resetPosition(newPosition: SIUnit = SIUnit(0.0))
+
+ fun resetPositionRaw(newPosition: SIUnit = SIUnit(0.0))
+}
diff --git a/src/main/kotlin/org/team5419/fault/hardware/BerkeliumMotor.kt b/src/main/kotlin/org/team5419/fault/hardware/BerkeliumMotor.kt
new file mode 100644
index 0000000..38e26dd
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/hardware/BerkeliumMotor.kt
@@ -0,0 +1,81 @@
+package org.team5419.fault.hardware
+
+import org.team5419.fault.math.units.Meter
+import org.team5419.fault.math.units.SIKey
+import org.team5419.fault.math.units.SIUnit
+import org.team5419.fault.math.units.derived.Acceleration
+import org.team5419.fault.math.units.derived.Radian
+import org.team5419.fault.math.units.derived.Velocity
+import org.team5419.fault.math.units.derived.Volt
+
+typealias LinearBerkeliumMotor = BerkeliumMotor
+typealias AngularBerkeliumMotor = BerkeliumMotor
+
+interface BerkeliumMotor {
+
+ /**
+ * The encoder attached to the motor
+ */
+ val encoder: BerkeliumEncoder
+ /**
+ * The voltage output of the motor controller in volts
+ */
+ val voltageOutput: SIUnit
+
+ /**
+ * Inverts the output given to the motor
+ */
+ var outputInverted: Boolean
+
+ /**
+ * When enabled, motor leads are commonized electrically to reduce motion
+ */
+ var brakeMode: Boolean
+
+ /**
+ * Configures the max voltage output given to the motor
+ */
+ var voltageCompSaturation: SIUnit
+
+ /**
+ * Peak target velocity that the on board motion profile generator will use
+ * Unit is [T]/s
+ */
+ var motionProfileCruiseVelocity: SIUnit>
+ /**
+ * Acceleration that the on board motion profile generator will
+ * Unit is [T]/s/s
+ */
+ var motionProfileAcceleration: SIUnit>
+ /**
+ * Enables the use of on board motion profiling for position mode
+ */
+ var useMotionProfileForPosition: Boolean
+
+ fun follow(motor: BerkeliumMotor<*>): Boolean
+
+ /**
+ * Sets the output [voltage] in volts and [arbitraryFeedForward] in volts
+ */
+ fun setVoltage(voltage: SIUnit, arbitraryFeedForward: SIUnit = SIUnit(0.0))
+
+ /**
+ * Sets the output [percent] in percent and [arbitraryFeedForward] in volts
+ */
+ fun setPercent(percent: Double, arbitraryFeedForward: SIUnit = SIUnit(0.0))
+
+ /**
+ * Sets the output [velocity] in [T]/s and [arbitraryFeedForward] in volts
+ */
+ fun setVelocity(velocity: SIUnit>, arbitraryFeedForward: SIUnit = SIUnit(0.0))
+
+ /**
+ * Sets the output [position] in [T] and [arbitraryFeedForward] in volts
+ */
+ fun setPosition(position: SIUnit, arbitraryFeedForward: SIUnit = SIUnit(0.0))
+
+ /**
+ * Sets the output of the motor to neutral
+ */
+ fun setNeutral()
+}
diff --git a/src/main/kotlin/org/team5419/fault/hardware/Limelight.kt b/src/main/kotlin/org/team5419/fault/hardware/Limelight.kt
new file mode 100644
index 0000000..5ba3793
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/hardware/Limelight.kt
@@ -0,0 +1,149 @@
+package org.team5419.fault.hardware
+
+import edu.wpi.first.networktables.NetworkTableInstance
+import org.team5419.fault.math.geometry.Rotation2d
+import org.team5419.fault.math.units.derived.tan
+import org.team5419.fault.math.units.derived.degrees
+import org.team5419.fault.math.units.Meter
+import org.team5419.fault.math.units.inches
+import org.team5419.fault.math.units.SIUnit
+
+import kotlin.math.PI
+import kotlin.math.asin
+import kotlin.math.tan
+
+open class Limelight(
+ networkTableName: String = "limelight",
+ val inverted: Boolean = false,
+ private val mTargetHeight: SIUnit = 0.inches,
+ private val mCameraHeight: SIUnit = 0.inches,
+ private val mCameraAngle: Rotation2d = Rotation2d() // angle below (or above) horizontal
+) {
+
+ // FEEDBACK VARIABLES
+ val targetFound: Boolean
+ get() = mLimelight.getEntry("tv").getBoolean(false)
+
+ val horizontalOffset: Double
+ get() {
+ if (inverted) {
+ return -mLimelight.getEntry("tx").getDouble(0.0)
+ } else {
+ return mLimelight.getEntry("tx").getDouble(0.0)
+ }
+ }
+
+ val verticalOffset: Double
+ get() {
+ if (inverted) {
+ return -mLimelight.getEntry("ty").getDouble(0.0)
+ } else {
+ return mLimelight.getEntry("ty").getDouble(0.0)
+ }
+ }
+
+ val targetArea: Double
+ get() = mLimelight.getEntry("ta").getDouble(0.0)
+
+ val targetSkew: Double
+ get() {
+ val skew = mLimelight.getEntry("ts").getDouble(0.0)
+ if (skew < -45.0) { /*>*/
+ return skew + 90.0
+ } else {
+ return skew
+ }
+ }
+
+ val horizontalLength: Double
+ get() = mLimelight.getEntry("tlong").getDouble(0.0)
+
+ val verticalLength: Double
+ get() = mLimelight.getEntry("tvert").getDouble(0.0)
+
+ val latency: Double
+ get() = mLimelight.getEntry("tl").getDouble(0.0)
+
+ // CALCULATED VARIABLES
+
+ val horizontalDistance: SIUnit
+ get() = (mTargetHeight - mCameraHeight) / (mCameraAngle.radian + horizontalOffset.degrees).tan
+
+ val calculateTargetSkew: Double
+ get() {
+ var temp = (horizontalLength / verticalLength) * kAspectRatio
+ if (temp < 0.0) temp = 0.0 /*>*/
+ if (temp > 1.0) temp = 1.0
+ return (180.0 / PI) * asin(temp)
+ }
+
+ // LIGHT MODE
+ var lightMode: LightMode = LightMode.Off
+ set(value) {
+ if (value == field) return
+ when (value) {
+ LightMode.On -> mLimelight.getEntry("ledMode").setNumber(3)
+ LightMode.Off -> mLimelight.getEntry("ledMode").setNumber(1)
+ LightMode.Blink -> mLimelight.getEntry("ledMode").setNumber(2)
+ }
+ field = value
+ }
+
+ // CAMERA MODE
+ var cameraMode: CameraMode = CameraMode.Vision
+ set(value) {
+ if (field == value) return
+ when (value) {
+ CameraMode.Driver -> mLimelight.getEntry("camMode").setNumber(1)
+ CameraMode.Vision -> mLimelight.getEntry("camMode").setNumber(0)
+ }
+ field = value
+ }
+
+ // STREAM Mode
+ var streamMode: StreamMode = StreamMode.Standard
+ set(value) {
+ if (field == value) return
+ when (value) {
+ StreamMode.Standard -> mLimelight.getEntry("stream").setNumber(0)
+ StreamMode.PipMain -> mLimelight.getEntry("stream").setNumber(1)
+ StreamMode.PipSecondary -> mLimelight.getEntry("stream").setNumber(2)
+ }
+ field = value
+ }
+
+ // SNAPSHOT MODE
+ var snapshotMode: SnapshotMode = SnapshotMode.Stop
+ set(value) {
+ if (field == value) return
+ when (value) {
+ SnapshotMode.Stop -> mLimelight.getEntry("snapshot").setNumber(0)
+ SnapshotMode.Start -> mLimelight.getEntry("snapshot").setNumber(1)
+ }
+ field = value
+ }
+
+ // PIPELINE
+ fun setPipeline(pipeline: Pipeline) = setPipeline(pipeline.id)
+ fun setPipeline(id: Int) {
+ if (id < 0 || id > 9) {
+ println("Pipeline id needs to be from 0 to 9, ignoring value: $id")
+ return
+ } else {
+ mLimelight.getEntry("pipeline").setNumber(id)
+ }
+ }
+
+ private val mLimelight = NetworkTableInstance.getDefault().getTable(networkTableName)
+
+ enum class LightMode { On, Off, Blink }
+ enum class CameraMode { Vision, Driver }
+ enum class StreamMode { Standard, PipMain, PipSecondary }
+ enum class SnapshotMode { Stop, Start }
+
+ data class Pipeline(val id: Int)
+
+ companion object {
+ const val kAspectRatio = 6.0 / 15.0
+ }
+}
diff --git a/src/main/kotlin/org/team5419/fault/hardware/ctre/BerkeliumIMU.kt b/src/main/kotlin/org/team5419/fault/hardware/ctre/BerkeliumIMU.kt
new file mode 100644
index 0000000..61b003b
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/hardware/ctre/BerkeliumIMU.kt
@@ -0,0 +1,8 @@
+package org.team5419.fault.hardware.ctre
+
+import com.ctre.phoenix.sensors.PigeonIMU
+import org.team5419.fault.math.geometry.Rotation2d
+import org.team5419.fault.math.units.derived.degrees
+import org.team5419.fault.util.Source
+
+fun PigeonIMU.asSource(): Source = { Rotation2d(fusedHeading.degrees) }
diff --git a/src/main/kotlin/org/team5419/fault/hardware/ctre/BerkeliumSPX.kt b/src/main/kotlin/org/team5419/fault/hardware/ctre/BerkeliumSPX.kt
new file mode 100644
index 0000000..e3a1349
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/hardware/ctre/BerkeliumSPX.kt
@@ -0,0 +1,22 @@
+package org.team5419.fault.hardware.ctre
+
+import com.ctre.phoenix.motorcontrol.can.VictorSPX
+import org.team5419.fault.math.units.Meter
+import org.team5419.fault.math.units.SIKey
+import org.team5419.fault.math.units.derived.Radian
+import org.team5419.fault.math.units.native.NativeUnitModel
+
+typealias LinearBerkeleiumSPX = BerkeliumSPX
+typealias AngularBerkeleiumSPX = BerkeliumSPX
+
+class BerkeliumSPX(
+ val victorSPX: VictorSPX,
+ model: NativeUnitModel
+) : CTREBerkeliumMotor(victorSPX, model) {
+
+ init {
+ victorSPX.configFactoryDefault()
+ }
+
+ constructor(id: Int, model: NativeUnitModel): this(VictorSPX(id), model)
+}
diff --git a/src/main/kotlin/org/team5419/fault/hardware/ctre/BerkeliumSRX.kt b/src/main/kotlin/org/team5419/fault/hardware/ctre/BerkeliumSRX.kt
new file mode 100644
index 0000000..b464a6c
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/hardware/ctre/BerkeliumSRX.kt
@@ -0,0 +1,42 @@
+package org.team5419.fault.hardware.ctre
+
+import com.ctre.phoenix.motorcontrol.can.TalonSRX
+import org.team5419.fault.math.units.SIUnit
+import org.team5419.fault.math.units.SIKey
+import org.team5419.fault.math.units.Meter
+import org.team5419.fault.math.units.Second
+import org.team5419.fault.math.units.inMilliseconds
+import org.team5419.fault.math.units.Ampere
+import org.team5419.fault.math.units.inAmps
+import org.team5419.fault.math.units.derived.Radian
+import org.team5419.fault.math.units.native.NativeUnitModel
+
+typealias LinearBerkeleiumSRX = BerkeliumSRX
+typealias AngularBerkeleiumSRX = BerkeliumSRX
+
+class BerkeliumSRX(
+ val talonSRX: TalonSRX,
+ model: NativeUnitModel
+) : CTREBerkeliumMotor(talonSRX, model) {
+
+ constructor(id: Int, model: NativeUnitModel): this(TalonSRX(id), model)
+
+ init {
+ talonSRX.configFactoryDefault(0)
+ }
+
+ fun configCurrentLimit(enabled: Boolean, config: CurrentLimitConfig? = null) {
+ talonSRX.enableCurrentLimit(enabled)
+ if (enabled && config != null) {
+ talonSRX.configContinuousCurrentLimit(config.continuousCurrentLimit.inAmps().toInt())
+ talonSRX.configPeakCurrentLimit(config.peakCurrentLimit.inAmps().toInt())
+ talonSRX.configPeakCurrentDuration(config.peakCurrentLimitDuration.inMilliseconds().toInt())
+ }
+ }
+
+ data class CurrentLimitConfig(
+ val peakCurrentLimit: SIUnit,
+ val peakCurrentLimitDuration: SIUnit,
+ val continuousCurrentLimit: SIUnit
+ )
+}
diff --git a/src/main/kotlin/org/team5419/fault/hardware/ctre/CTREBerkeliumEncoder.kt b/src/main/kotlin/org/team5419/fault/hardware/ctre/CTREBerkeliumEncoder.kt
new file mode 100644
index 0000000..6566b64
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/hardware/ctre/CTREBerkeliumEncoder.kt
@@ -0,0 +1,38 @@
+package org.team5419.fault.hardware.ctre
+
+import com.ctre.phoenix.motorcontrol.IMotorController
+import org.team5419.fault.hardware.AbstractBerkeliumEncoder
+import org.team5419.fault.math.units.SIKey
+import org.team5419.fault.math.units.SIUnit
+import org.team5419.fault.math.units.native.NativeUnit
+import org.team5419.fault.math.units.native.NativeUnitModel
+import org.team5419.fault.math.units.native.nativeUnits
+import org.team5419.fault.math.units.native.nativeUnitsPer100ms
+import kotlin.properties.Delegates
+
+class CTREBerkeliumEncoder(
+ val motorController: IMotorController,
+ val pidIdx: Int = 0,
+ model: NativeUnitModel
+) : AbstractBerkeliumEncoder(model) {
+ override val rawPosition get() =
+ motorController.getSelectedSensorPosition(pidIdx).toDouble().nativeUnits
+ override val rawVelocity get() =
+ motorController.getSelectedSensorVelocity(pidIdx).toDouble().nativeUnitsPer100ms // check this
+
+ var encoderPhase by Delegates.observable(false) {
+ _, _, newValue -> motorController.setSensorPhase(newValue)
+ }
+
+ /**
+ * sets position of encoder [newPosition] where [newPosition] is defined by the units of the[model]
+ */
+ override fun resetPosition(newPosition: SIUnit) {
+ // TODO(make sure this works)
+ motorController.setSelectedSensorPosition(model.toNativeUnitPosition(newPosition).value.toInt(), pidIdx, 0)
+ }
+
+ override fun resetPositionRaw(newPosition: SIUnit) {
+ motorController.setSelectedSensorPosition(newPosition.value.toInt(), pidIdx, 0)
+ }
+}
diff --git a/src/main/kotlin/org/team5419/fault/hardware/ctre/CTREBerkeliumMotor.kt b/src/main/kotlin/org/team5419/fault/hardware/ctre/CTREBerkeliumMotor.kt
new file mode 100644
index 0000000..1392843
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/hardware/ctre/CTREBerkeliumMotor.kt
@@ -0,0 +1,134 @@
+package org.team5419.fault.hardware.ctre
+
+import com.ctre.phoenix.motorcontrol.IMotorController
+import com.ctre.phoenix.motorcontrol.ControlMode
+import com.ctre.phoenix.motorcontrol.DemandType
+import com.ctre.phoenix.motorcontrol.NeutralMode
+
+import org.team5419.fault.hardware.AbstractBerkeliumMotor
+import org.team5419.fault.hardware.BerkeliumMotor
+import org.team5419.fault.math.units.SIKey
+import org.team5419.fault.math.units.SIUnit
+import org.team5419.fault.math.units.derived.Acceleration
+import org.team5419.fault.math.units.derived.Velocity
+import org.team5419.fault.math.units.derived.Volt
+import org.team5419.fault.math.units.derived.volts
+import org.team5419.fault.math.units.native.NativeUnitModel
+import org.team5419.fault.math.units.native.inNativeUnitsPer100msPerSecond
+import org.team5419.fault.math.units.native.nativeUnitsPer100ms
+import org.team5419.fault.math.units.native.toNativeUnitVelocity
+import org.team5419.fault.math.units.operations.times
+import org.team5419.fault.math.units.operations.div
+import org.team5419.fault.math.units.unitlessValue
+import kotlin.math.roundToInt
+
+import kotlin.properties.Delegates
+
+abstract class CTREBerkeliumMotor internal constructor(
+ val motorController: IMotorController,
+ val model: NativeUnitModel
+) : AbstractBerkeliumMotor() {
+
+ private var mLastDemand = Demand(ControlMode.Disabled, 0.0, DemandType.Neutral, 0.0)
+
+ override val encoder = CTREBerkeliumEncoder(motorController, 0, model)
+
+ override val voltageOutput: SIUnit
+ get() = motorController.motorOutputVoltage.volts
+
+ override var outputInverted: Boolean by Delegates.observable(false) { _, _, newValue ->
+ motorController.inverted = newValue
+ }
+
+ override var brakeMode: Boolean by Delegates.observable(false) { _, _, newValue ->
+ motorController.setNeutralMode(if (newValue) NeutralMode.Brake else NeutralMode.Coast)
+ }
+
+ override var voltageCompSaturation: SIUnit by Delegates.observable(12.0.volts) { _, _, newValue ->
+ motorController.configVoltageCompSaturation(newValue.value, 0)
+ motorController.enableVoltageCompensation(true)
+ }
+
+ override var motionProfileCruiseVelocity: SIUnit>
+ by Delegates.observable(SIUnit(0.0)) { _, _, newValue ->
+ motorController.configMotionCruiseVelocity(
+ model.toNativeUnitVelocity(newValue).nativeUnitsPer100ms.roundToInt(), 0
+ )
+ }
+
+ override var motionProfileAcceleration: SIUnit>
+ by Delegates.observable(SIUnit(0.0)) { _, _, newValue ->
+ motorController.configMotionAcceleration(
+ model.toNativeUnitAcceleration(newValue).inNativeUnitsPer100msPerSecond().roundToInt(), 0
+ )
+ }
+
+ init {
+ motorController.configVoltageCompSaturation(kCompVoltage.value, 0)
+ motorController.enableVoltageCompensation(true)
+ }
+
+ override fun setVoltage(voltage: SIUnit, arbitraryFeedForward: SIUnit) = sendDemand(
+ Demand(
+ ControlMode.PercentOutput, (voltage / kCompVoltage).unitlessValue,
+ DemandType.ArbitraryFeedForward, (arbitraryFeedForward / kCompVoltage).unitlessValue
+ )
+ )
+
+ override fun setPercent(percent: Double, arbitraryFeedForward: SIUnit) = sendDemand(
+ Demand(
+ ControlMode.PercentOutput, percent,
+ DemandType.ArbitraryFeedForward, (arbitraryFeedForward / kCompVoltage).unitlessValue
+ )
+ )
+
+ override fun setVelocity(velocity: SIUnit>, arbitraryFeedForward: SIUnit) = sendDemand(
+ Demand(
+ ControlMode.Velocity, model.toNativeUnitVelocity(velocity).nativeUnitsPer100ms,
+ DemandType.ArbitraryFeedForward, (arbitraryFeedForward / kCompVoltage).unitlessValue
+ )
+ )
+
+ override fun setPosition(position: SIUnit, arbitraryFeedForward: SIUnit) = sendDemand(
+ Demand(
+ if (useMotionProfileForPosition) ControlMode.MotionMagic else ControlMode.Position,
+ model.toNativeUnitPosition(position).value,
+ DemandType.ArbitraryFeedForward, (arbitraryFeedForward / kCompVoltage).unitlessValue
+ )
+ )
+
+ override fun setNeutral() = sendDemand(
+ Demand(
+ ControlMode.Disabled,
+ 0.0,
+ DemandType.Neutral,
+ 0.0
+ )
+ )
+
+ fun sendDemand(demand: Demand) {
+ if (demand != mLastDemand) {
+ motorController.set(demand.mode, demand.demand0, demand.demand1Type, demand.demand1)
+ mLastDemand = demand
+ }
+ }
+
+ override fun follow(motor: BerkeliumMotor<*>): Boolean =
+ if (motor is CTREBerkeliumMotor<*>) {
+ motorController.follow(motor.motorController)
+ true
+ } else {
+ super.follow(motor)
+ }
+
+ data class Demand(
+ val mode: ControlMode,
+ val demand0: Double,
+ val demand1Type: DemandType = DemandType.Neutral,
+ val demand1: Double = 0.0
+ )
+
+ companion object {
+ private val kCompVoltage = SIUnit(12.0)
+ }
+}
diff --git a/src/main/kotlin/org/team5419/fault/input/BerkeliumHID.kt b/src/main/kotlin/org/team5419/fault/input/BerkeliumHID.kt
new file mode 100644
index 0000000..8d41be1
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/input/BerkeliumHID.kt
@@ -0,0 +1,74 @@
+package org.team5419.fault.input
+
+import edu.wpi.first.wpilibj.GenericHID
+import org.team5419.fault.util.BooleanSource
+import org.team5419.fault.util.DoubleSource
+
+fun T.mapControls(
+ block: BerkeleiumHIDBuilder.() -> Unit
+) = BerkeleiumHIDBuilder(this).also(block).build()
+
+class BerkeleiumHIDBuilder(private val genericHID: T) {
+ private val controlBuilders = mutableListOf()
+ private val stateControlBuilders = mutableMapOf>()
+
+ fun button(
+ buttonId: Int,
+ block: HIDButtonBuilder.() -> Unit = {}
+ ) = button(HIDButtonSource(genericHID, buttonId), block = block)
+
+ fun axisButton(
+ axisId: Int,
+ threshold: Double = HIDButton.kDefaultThreshold,
+ block: HIDButtonBuilder.() -> Unit = {}
+ ) = button(HIDAxisSource(genericHID, axisId), threshold, block = block)
+
+ fun pov(angle: Int, block: HIDButtonBuilder.() -> Unit = {}) = pov(0, angle, block)
+ fun pov(
+ povId: Int,
+ angle: Int,
+ block: HIDButtonBuilder.() -> Unit = {}
+ ) = button(HIDPOVSource(genericHID, povId, angle), block = block)
+
+ fun button(
+ source: HIDSource,
+ threshold: Double = HIDButton.kDefaultThreshold,
+ block: HIDButtonBuilder.() -> Unit = {}
+ ): HIDButtonBuilder {
+ val builder = HIDButtonBuilder(source, threshold)
+ controlBuilders.add(builder)
+ block(builder)
+ return builder
+ }
+
+ fun state(state: BooleanSource, block: BerkeleiumHIDBuilder.() -> Unit) =
+ stateControlBuilders.compute(state) { _, _ -> BerkeleiumHIDBuilder(genericHID).also(block) }
+
+ fun build(): BerkeleiumHID {
+ return BerkeleiumHID(
+ genericHID,
+ controlBuilders.map { it.build() },
+ stateControlBuilders.mapValues { it.value.build() }
+ )
+ }
+}
+
+class BerkeleiumHID(
+ private val genericHID: T,
+ private val controls: List,
+ private val stateControls: Map>
+) {
+
+ fun getRawAxis(axisId: Int): DoubleSource = HIDAxisSource(genericHID, axisId)
+ fun getRawButton(buttonId: Int): BooleanSource = HIDButtonSource(
+ genericHID,
+ buttonId
+ ).booleanSource
+
+ fun update() {
+ controls.forEach { it.update() }
+ for ((state, controller) in stateControls) {
+ if (state()) controller.update()
+ }
+ }
+}
diff --git a/src/main/kotlin/org/team5419/fault/input/BerkeliumPS4.kt b/src/main/kotlin/org/team5419/fault/input/BerkeliumPS4.kt
new file mode 100644
index 0000000..5d99cc6
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/input/BerkeliumPS4.kt
@@ -0,0 +1,49 @@
+@file:Suppress("MatchingDeclarationName")
+package org.team5419.fault.input
+
+import edu.wpi.first.wpilibj.GenericHID
+import edu.wpi.first.wpilibj.Joystick
+
+typealias BerkeleiumPS4Controller = BerkeleiumHID
+typealias BerkeleiumPS4Builder = BerkeleiumHIDBuilder
+
+fun ps4Controller(
+ port: Int,
+ block: BerkeleiumPS4Builder.() -> Unit
+) = Joystick(port).mapControls(block)
+
+fun BerkeleiumPS4Builder.button(
+ button: PS4Button,
+ block: HIDButtonBuilder.() -> Unit = {}
+) = button(button.id, block)
+
+fun BerkeleiumPS4Builder.triggerAxisButton(
+ hand: GenericHID.Hand,
+ threshold: Double = HIDButton.kDefaultThreshold,
+ block: HIDButtonBuilder.() -> Unit = {}
+) = axisButton(yTriggerAxisToRawAxis(hand), threshold, block)
+
+fun BerkeleiumPS4Controller.getY(hand: GenericHID.Hand) = getRawAxis(yAxisToRawAxis(hand))
+fun BerkeleiumPS4Controller.getX(hand: GenericHID.Hand) = getRawAxis(xAxisToRawAxis(hand))
+fun BerkeleiumPS4Controller.getRawButton(button: PS4Button) = getRawButton(button.id)
+
+private fun yAxisToRawAxis(hand: GenericHID.Hand) = if (hand == GenericHID.Hand.kLeft) 1 else 5
+private fun xAxisToRawAxis(hand: GenericHID.Hand) = if (hand == GenericHID.Hand.kLeft) 0 else 2
+private fun yTriggerAxisToRawAxis(hand: GenericHID.Hand) = if (hand == GenericHID.Hand.kLeft) 2 else 3 // check this
+
+enum class PS4Button(val id: Int) {
+ Square(1),
+ X(2),
+ Circle(3),
+ Triangle(4),
+ LeftBumper(5),
+ RightBumper(6),
+ LeftTrigger(7),
+ RightTrigger(8),
+ Share(9),
+ Options(10),
+ LeftStick(11),
+ RightStick(12),
+ Playstation(13),
+ Touchpad(14)
+}
diff --git a/src/main/kotlin/org/team5419/fault/input/BerkeliumXbox.kt b/src/main/kotlin/org/team5419/fault/input/BerkeliumXbox.kt
new file mode 100644
index 0000000..1090014
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/input/BerkeliumXbox.kt
@@ -0,0 +1,45 @@
+@file:Suppress("MatchingDeclarationName")
+package org.team5419.fault.input
+
+import edu.wpi.first.wpilibj.GenericHID
+import edu.wpi.first.wpilibj.XboxController
+
+typealias BerkeleiumXboxController = BerkeleiumHID
+typealias BerkeleiumXboxBuilder = BerkeleiumHIDBuilder
+
+fun xboxController(port: Int, block: BerkeleiumXboxBuilder.() -> Unit): BerkeleiumXboxController =
+ XboxController(port).mapControls(block)
+
+fun BerkeleiumXboxBuilder.button(
+ button: XboxButton,
+ block: HIDButtonBuilder.() -> Unit = {}
+) = button(button.id, block)
+
+fun BerkeleiumXboxBuilder.triggerAxisButton(
+ hand: GenericHID.Hand,
+ threshold: Double = HIDButton.kDefaultThreshold,
+ block: HIDButtonBuilder.() -> Unit = {}
+) = axisButton(yTriggerAxisToRawAxis(hand), threshold, block)
+
+fun BerkeleiumXboxController.getY(hand: GenericHID.Hand) = getRawAxis(yAxisToRawAxis(hand))
+
+fun BerkeleiumXboxController.getX(hand: GenericHID.Hand) = getRawAxis(xAxisToRawAxis(hand))
+
+fun BerkeleiumXboxController.getRawButton(button: XboxButton) = getRawButton(button.id)
+
+private fun yAxisToRawAxis(hand: GenericHID.Hand) = if (hand == GenericHID.Hand.kLeft) 1 else 5
+private fun xAxisToRawAxis(hand: GenericHID.Hand) = if (hand == GenericHID.Hand.kLeft) 0 else 4
+private fun yTriggerAxisToRawAxis(hand: GenericHID.Hand) = if (hand == GenericHID.Hand.kLeft) 2 else 3
+
+enum class XboxButton(val id: Int) {
+ A(1),
+ B(2),
+ X(3),
+ Y(4),
+ LeftBumper(5),
+ RightBumper(6),
+ Back(7),
+ Start(8),
+ LeftStick(9),
+ RightStick(10)
+}
diff --git a/src/main/kotlin/org/team5419/fault/input/DriveHelper.kt b/src/main/kotlin/org/team5419/fault/input/DriveHelper.kt
new file mode 100644
index 0000000..26693d3
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/input/DriveHelper.kt
@@ -0,0 +1,79 @@
+package org.team5419.fault.input
+
+import org.team5419.fault.util.BooleanSource
+import org.team5419.fault.util.DoubleSource
+import kotlin.math.absoluteValue
+
+abstract class DriveHelper : DriveSignalSource {
+
+ abstract fun output(): DriveSignal
+
+ override fun invoke() = output()
+
+ protected fun handleDeadband(value: Double, deadband: Double) = when {
+ value.absoluteValue < deadband.absoluteValue -> 0.0
+ else -> value
+ }
+}
+
+class TankHelper(
+ private val left: DoubleSource,
+ private val right: DoubleSource,
+ private val slow: BooleanSource = { false },
+ private val deadband: Double = kDefaultDeaband,
+ private val slowMultiplier: Double = kDefaultSlowMultiplier
+) : DriveHelper() {
+
+ override fun output(): DriveSignal {
+ val currentSlow = if (slow()) slowMultiplier else 1.0
+ var currentLeft = left()
+ var currentRight = right()
+ currentLeft = handleDeadband(currentLeft, deadband)
+ currentRight = handleDeadband(currentRight, deadband)
+ return DriveSignal(currentLeft * currentSlow, currentRight)
+ }
+
+ companion object {
+ const val kDefaultDeaband = 0.02
+ const val kDefaultSlowMultiplier = 0.4
+ }
+}
+
+class SpaceDriveHelper(
+ private val throttle: DoubleSource,
+ private val wheel: DoubleSource,
+ private val quickTurn: BooleanSource,
+ private val slow: BooleanSource = { false },
+ private val deadband: Double = kDefaultDeadband,
+ private val quickTurnMultiplier: Double = kDefaultQuickTurnMultiplier,
+ private val slowMultiplier: Double = kDefaultSlowMultiplier
+) : DriveHelper() {
+
+ override fun output(): DriveSignal {
+ var currentThrottle = throttle()
+ var currentTurn = wheel() * if (quickTurn()) 1.0 else quickTurnMultiplier
+ currentThrottle = handleDeadband(currentThrottle, deadband)
+ currentTurn = handleDeadband(currentTurn, deadband)
+ val currentSlow = if (slow()) slowMultiplier else 1.0
+ val left = currentSlow * (currentThrottle - currentTurn)
+ val right = currentSlow * (currentThrottle + currentTurn)
+ return DriveSignal(left, right)
+ }
+
+ companion object {
+ const val kDefaultDeadband = 0.02
+ const val kDefaultQuickTurnMultiplier = 0.4
+ const val kDefaultSlowMultiplier = 0.4
+ }
+}
+
+class CheesyDriveHelper(
+ private val throttle: DoubleSource,
+ private val wheel: DoubleSource,
+ private val quickTurn: BooleanSource
+) : DriveHelper() {
+
+ override fun output(): DriveSignal {
+ return DriveSignal()
+ }
+}
diff --git a/src/main/kotlin/org/team5419/fault/input/DriveSignal.kt b/src/main/kotlin/org/team5419/fault/input/DriveSignal.kt
new file mode 100644
index 0000000..ae660d3
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/input/DriveSignal.kt
@@ -0,0 +1,12 @@
+package org.team5419.fault.input
+
+import org.team5419.fault.util.Source
+
+typealias DriveSignalSource = Source
+
+data class DriveSignal(val left: Double = 0.0, val right: Double = 0.0, val brake: Boolean = false) {
+ companion object {
+ val kBrake = DriveSignal(0.0, 0.0, true)
+ val kNeutral = DriveSignal()
+ }
+}
diff --git a/src/main/kotlin/org/team5419/fault/input/HIDControl.kt b/src/main/kotlin/org/team5419/fault/input/HIDControl.kt
new file mode 100644
index 0000000..00e5698
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/input/HIDControl.kt
@@ -0,0 +1,63 @@
+package org.team5419.fault.input
+
+import org.team5419.fault.auto.Action
+import kotlin.math.absoluteValue
+
+typealias HIDControlListener = () -> Unit
+
+interface HIDControl {
+ fun update()
+}
+class HIDButton(
+ private val source: HIDSource,
+ private val threshold: Double,
+ private val whileOn: List,
+ private val whileOff: List,
+ private val changeToOn: List,
+ private val changeToOff: List
+) : HIDControl {
+
+ private var lastValue = source().absoluteValue >= threshold
+
+ override fun update() {
+ val currentValue = source().absoluteValue >= threshold
+ when {
+ // Value has changed
+ lastValue != currentValue -> when {
+ currentValue -> changeToOn
+ else -> changeToOff
+ }
+ // Value stayed the same
+ else -> when {
+ currentValue -> whileOn
+ else -> whileOff
+ }
+ }.forEach { it() }
+ lastValue = currentValue
+ }
+
+ companion object {
+ const val kDefaultThreshold = 0.5
+ }
+}
+
+abstract class HIDControlBuilder(val source: HIDSource) {
+ abstract fun build(): HIDControl
+}
+class HIDButtonBuilder(source: HIDSource, private val threshold: Double) : HIDControlBuilder(source) {
+
+ private val whileOff = mutableListOf()
+ private val whileOn = mutableListOf()
+ private val changeToOn = mutableListOf()
+ private val changeToOff = mutableListOf()
+
+ fun changeToOn(action: Action) = changeToOn { action.start() }
+ fun changeToOff(action: Action) = changeToOff { action.start() }
+
+ fun whileOff(block: HIDControlListener) = also { whileOff.add(block) }
+ fun whileOn(block: HIDControlListener) = also { whileOn.add(block) }
+ fun changeToOn(block: HIDControlListener) = also { changeToOn.add(block) }
+ fun changeToOff(block: HIDControlListener) = also { changeToOff.add(block) }
+
+ override fun build() = HIDButton(source, threshold, whileOn, whileOff, changeToOn, changeToOff)
+}
diff --git a/src/main/kotlin/org/team5419/fault/input/HIDSource.kt b/src/main/kotlin/org/team5419/fault/input/HIDSource.kt
new file mode 100644
index 0000000..670b314
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/input/HIDSource.kt
@@ -0,0 +1,31 @@
+package org.team5419.fault.input
+
+import edu.wpi.first.wpilibj.GenericHID
+import org.team5419.fault.util.BooleanSource
+import org.team5419.fault.util.Source
+
+interface HIDSource : Source
+
+class HIDButtonSource(
+ private val genericHID: GenericHID,
+ private val buttonId: Int
+) : HIDSource {
+ val booleanSource: BooleanSource = { genericHID.getRawButton(buttonId) }
+
+ override fun invoke() = if (booleanSource()) 1.0 else 0.0
+}
+
+class HIDAxisSource(
+ private val genericHID: GenericHID,
+ private val axisId: Int
+) : HIDSource {
+ override fun invoke() = genericHID.getRawAxis(axisId)
+}
+
+class HIDPOVSource(
+ private val genericHID: GenericHID,
+ private val povId: Int,
+ private val angle: Int
+) : HIDSource {
+ override fun invoke() = if (genericHID.getPOV(povId) == angle) 1.0 else 0.0
+}
diff --git a/src/main/kotlin/org/team5419/fault/math/Epsilon.kt b/src/main/kotlin/org/team5419/fault/math/Epsilon.kt
new file mode 100644
index 0000000..0c9ae74
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/math/Epsilon.kt
@@ -0,0 +1,8 @@
+package org.team5419.fault.math
+
+import kotlin.math.absoluteValue
+
+@Suppress("TopLevelPropertyNaming")
+const val kEpsilon = 1E-9
+
+infix fun Double.epsilonEquals(other: Double) = minus(other).absoluteValue < kEpsilon
diff --git a/src/main/kotlin/org/team5419/fault/math/MathMisc.kt b/src/main/kotlin/org/team5419/fault/math/MathMisc.kt
new file mode 100644
index 0000000..865d5c3
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/math/MathMisc.kt
@@ -0,0 +1,42 @@
+package org.team5419.fault.math
+
+import kotlin.math.PI
+
+fun Double.lerp(endValue: Double, t: Double) = this + (endValue - this) * t.coerceIn(0.0, 1.0)
+
+infix fun Double.cos(other: Double) = times(Math.cos(other))
+infix fun Double.sin(other: Double) = times(Math.sin(other))
+
+fun limit(value: Double, limit: Double): Double {
+ return limit(value, -limit, limit)
+}
+
+/**
+ * method that limits input to a certain range.
+ * @param value value to be limited
+ * @param min lower bound of the value
+ * @param max upper bound of the value
+ * @return bounded value
+ */
+fun limit(value: Double, min: Double, max: Double): Double {
+ return Math.min(max, Math.max(min, value))
+}
+
+/**
+ * method to interpolate between 2 doubles
+ * @param a first value
+ * @param b second value
+ * @param x value between the 2 values
+ * @return interpolated value
+ */
+fun interpolate(a: Double, b: Double, x: Double): Double {
+ val newX = limit(x, 0.0, 1.0)
+ return a + (b - a) * newX
+}
+
+fun Double.boundRadians(): Double {
+ var x = this
+ while (x >= PI) x -= (2 * PI)
+ while (x < -PI) x += (2 * PI)
+ return x
+}
diff --git a/src/main/kotlin/org/team5419/fault/math/geometry/Pose2d.kt b/src/main/kotlin/org/team5419/fault/math/geometry/Pose2d.kt
new file mode 100644
index 0000000..12fea98
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/math/geometry/Pose2d.kt
@@ -0,0 +1,90 @@
+package org.team5419.fault.math.geometry
+
+import org.team5419.fault.math.epsilonEquals
+import org.team5419.fault.math.kEpsilon
+import org.team5419.fault.math.units.Meter
+import org.team5419.fault.math.units.SIUnit
+import org.team5419.fault.math.units.derived.Radian
+import org.team5419.fault.math.units.derived.toRotation2d
+import kotlin.math.absoluteValue
+
+data class Pose2d(
+ val translation: Vector2 = Vector2(),
+ val rotation: Rotation2d = Rotation2d()
+) : State {
+
+ constructor(
+ x: SIUnit,
+ y: SIUnit,
+ rotation: Rotation2d = Rotation2d()
+ ) : this(Vector2(x, y), rotation)
+
+ constructor(
+ translation: Vector2 = Vector2(),
+ rotation: SIUnit
+ ) : this(translation, rotation.toRotation2d())
+
+ constructor(
+ x: SIUnit,
+ y: SIUnit,
+ rotation: SIUnit
+ ) : this(Vector2(x, y), rotation)
+
+ val twist: Twist2d
+ get() {
+ val dtheta = rotation.radian.value
+ val halfDTheta = dtheta / 2.0
+ val cosMinusOne = rotation.cos - 1.0
+
+ val halfThetaByTanOfHalfDTheta = if (cosMinusOne.absoluteValue < kEpsilon) {
+ 1.0 - 1.0 / 12.0 * dtheta * dtheta
+ } else {
+ -(halfDTheta * rotation.sin) / cosMinusOne
+ }
+ val translationPart = translation *
+ Rotation2d(halfThetaByTanOfHalfDTheta, -halfDTheta, false)
+ return Twist2d(translationPart.x, translationPart.y, rotation.radian)
+ }
+
+ val mirror get() = Pose2d(Vector2(translation.x, -translation.y), -rotation)
+
+ infix fun inFrameOfReferenceOf(fieldRelativeOrigin: Pose2d) = (-fieldRelativeOrigin) + this
+
+ operator fun plus(other: Pose2d) = transformBy(other)
+
+ operator fun minus(other: Pose2d) = this + -other
+
+ fun transformBy(other: Pose2d) =
+ Pose2d(
+ translation + (other.translation * rotation),
+ rotation + other.rotation
+ )
+
+ operator fun unaryMinus(): Pose2d {
+ val invertedRotation = -rotation
+ return Pose2d((-translation) * invertedRotation, invertedRotation)
+ }
+
+ fun isCollinear(other: Pose2d): Boolean {
+ if (!rotation.isParallel(other.rotation)) return false
+ val twist = (-this + other).twist
+ return twist.dy.value epsilonEquals 0.0 && twist.dTheta.value epsilonEquals 0.0
+ }
+
+ @Suppress("ReturnCount")
+ override fun interpolate(endValue: Pose2d, t: Double): Pose2d {
+ if (t <= 0) {
+ return Pose2d(this.translation, this.rotation)
+ } else if (t >= 1) {
+ return Pose2d(endValue.translation, endValue.rotation)
+ }
+ val twist = (-this + endValue).twist
+ return this + (twist * t).asPose
+ }
+
+ override fun toString() = toCSV()
+
+ override fun toCSV() = "${translation.toCSV()}, ${rotation.degree}"
+
+ override fun distance(other: Pose2d) = (-this + other).twist.norm.value
+}
diff --git a/src/main/kotlin/org/team5419/fault/math/geometry/Pose2dWithCurvature.kt b/src/main/kotlin/org/team5419/fault/math/geometry/Pose2dWithCurvature.kt
new file mode 100644
index 0000000..375f5b6
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/math/geometry/Pose2dWithCurvature.kt
@@ -0,0 +1,41 @@
+package org.team5419.fault.math.geometry
+
+import org.team5419.fault.math.epsilonEquals
+import org.team5419.fault.math.interpolate
+import org.team5419.fault.math.lerp
+
+data class Pose2dWithCurvature(
+ val pose: Pose2d,
+ val curvature: Double,
+ val dkds: Double
+) : State {
+
+ val mirror get() = Pose2dWithCurvature(pose.mirror, -curvature, -dkds)
+
+ override fun interpolate(endValue: Pose2dWithCurvature, t: Double) =
+ Pose2dWithCurvature(
+ pose.interpolate(endValue.pose, t),
+ curvature.lerp(endValue.curvature, t),
+ dkds.lerp(endValue.dkds, t)
+ )
+
+ override fun distance(other: Pose2dWithCurvature) = pose.distance(other.pose)
+
+ operator fun plus(other: Pose2d) = Pose2dWithCurvature(this.pose + other, curvature, dkds)
+
+ override fun toCSV() = "${pose.toCSV()}, $curvature, $dkds"
+
+ override fun toString() = toCSV()
+
+ override fun hashCode(): Int {
+ return super.hashCode()
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (other is Pose2dWithCurvature) {
+ return this.pose.equals(other.pose) && this.curvature epsilonEquals other.curvature &&
+ this.dkds epsilonEquals other.dkds
+ }
+ return false
+ }
+}
diff --git a/src/main/kotlin/org/team5419/fault/math/geometry/Rectangle2d.kt b/src/main/kotlin/org/team5419/fault/math/geometry/Rectangle2d.kt
new file mode 100644
index 0000000..7112034
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/math/geometry/Rectangle2d.kt
@@ -0,0 +1,122 @@
+package org.team5419.fault.math.geometry
+
+import org.team5419.fault.math.epsilonEquals
+import org.team5419.fault.math.units.Meter
+import org.team5419.fault.math.units.SIUnit
+import org.team5419.fault.math.units.meters
+import org.team5419.fault.math.units.operations.div
+import org.team5419.fault.math.units.operations.times
+import org.team5419.fault.math.units.operations.div
+
+import kotlin.math.max
+import kotlin.math.min
+import org.team5419.fault.math.units.operations.div
+
+@Suppress("FunctionName")
+fun Rectangle2d(
+ one: Vector2d,
+ two: Vector2d
+): Rectangle2d {
+ val minX = min(one.x.value, two.x.value)
+ val minY = min(one.y.value, two.y.value)
+ val maxX = max(one.x.value, two.x.value)
+ val maxY = max(one.y.value, two.y.value)
+ return Rectangle2d(
+ minX.meters, minY.meters,
+ (maxX - minX).meters, (maxY - minY).meters
+ )
+}
+
+@Suppress("FunctionName", "UnsafeCallOnNullableType")
+fun Rectangle2d(
+ vararg pointsToInclude: Vector2d
+): Rectangle2d {
+ val minX = pointsToInclude.minBy { it.x }!!.x
+ val minY = pointsToInclude.minBy { it.y }!!.y
+ val maxX = pointsToInclude.maxBy { it.x }!!.x
+ val maxY = pointsToInclude.maxBy { it.y }!!.y
+ return Rectangle2d(
+ minX, minY,
+ maxX - minX, maxY - minY
+ )
+}
+
+data class Rectangle2d constructor(
+ val x: SIUnit,
+ val y: SIUnit,
+ val w: SIUnit,
+ val h: SIUnit
+) {
+
+ val topLeft = Vector2(x, y + h)
+ val topRight = Vector2(x + w, y + h)
+ val bottomLeft = Vector2(x, y)
+ val bottomRight = Vector2(x + w, y)
+
+ val center = Vector2(x + w / 2.0, y + h / 2.0)
+
+ val maxCorner = topRight
+ val minCorner = bottomLeft
+
+ fun isIn(r: Rectangle2d) =
+ x < r.x + r.w && x + w > r.x && y < r.y + r.h && y + h > r.y
+
+ fun isWithin(r: Rectangle2d) = r.x in x..(x + w - r.w) && r.y in y..(y + h - r.h)
+
+ operator fun contains(p: Vector2d) = p.x in x..(x + w) && p.y in y..(y + h)
+
+ @Suppress("ComplexMethod", "ReturnCount")
+ fun doesCollide(rectangle: Rectangle2d, translation: Vector2d): Boolean {
+ if (translation.x.value epsilonEquals 0.0 && translation.y.value epsilonEquals 0.0) return false
+ // Check if its even in range
+ val boxRect = Rectangle2d(
+ rectangle.topLeft, rectangle.bottomRight,
+ rectangle.topLeft + translation, rectangle.bottomRight + translation
+ )
+ // println(boxRect)
+ if (!boxRect.isIn(this)) return false
+ // AABB collision
+ // Calculate distances
+ val xInvEntry: SIUnit
+ val xInvExit: SIUnit
+ val yInvEntry: SIUnit
+ val yInvExit: SIUnit
+ if (translation.x.value > 0.0) {
+ xInvEntry = (x - (rectangle.x + rectangle.w))
+ xInvExit = ((x + w) - rectangle.x)
+ } else {
+ xInvEntry = ((x + w) - rectangle.x)
+ xInvExit = (x - (rectangle.x + rectangle.w))
+ }
+ if (translation.y.value > 0.0) {
+ yInvEntry = (y - (rectangle.y + rectangle.h))
+ yInvExit = ((y + h) - rectangle.y)
+ } else {
+ yInvEntry = ((y + h) - rectangle.y)
+ yInvExit = (y - (rectangle.y + rectangle.h))
+ }
+ // Find time of collisions
+ val xEntry: Double
+ val xExit: Double
+ val yEntry: Double
+ val yExit: Double
+ if (translation.x.value epsilonEquals 0.0) {
+ xEntry = Double.NEGATIVE_INFINITY
+ xExit = Double.POSITIVE_INFINITY
+ } else {
+ xEntry = (xInvEntry / translation.x).value
+ xExit = (xInvExit / translation.x).value
+ }
+ if (translation.y.value epsilonEquals 0.0) {
+ yEntry = Double.NEGATIVE_INFINITY
+ yExit = Double.POSITIVE_INFINITY
+ } else {
+ yEntry = (yInvEntry / translation.y).value
+ yExit = (yInvExit / translation.y).value
+ }
+ val entryTime = max(xEntry, yEntry)
+ val exitTime = min(xExit, yExit)
+
+ return entryTime <= exitTime && (xEntry >= 0.0 || yEntry >= 0.0) && (xEntry < 1.0 || yEntry < 1.0)
+ }
+}
diff --git a/src/main/kotlin/org/team5419/fault/math/geometry/Rotation2d.kt b/src/main/kotlin/org/team5419/fault/math/geometry/Rotation2d.kt
new file mode 100644
index 0000000..b67bece
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/math/geometry/Rotation2d.kt
@@ -0,0 +1,62 @@
+package org.team5419.fault.math.geometry
+
+import org.team5419.fault.math.epsilonEquals
+import org.team5419.fault.math.kEpsilon
+import org.team5419.fault.math.units.SIUnit
+import org.team5419.fault.math.units.derived.Radian
+import org.team5419.fault.math.units.derived.cos
+import org.team5419.fault.math.units.derived.sin
+import org.team5419.fault.math.units.derived.radians
+import org.team5419.fault.math.units.derived.inDegrees
+// import org.team5419.fault.math.units.derived.radians
+
+@Suppress("EqualsWithHashCodeExist")
+class Rotation2d {
+
+ val value: SIUnit
+ val cos: Double
+ val sin: Double
+
+ constructor() : this(0.0.radians)
+
+ constructor(heading: SIUnit) : this(heading.cos, heading.sin, true)
+
+ constructor(x: Double, y: Double, normalize: Boolean) {
+ if (normalize) {
+ val magnitude = Math.hypot(x, y)
+ if (magnitude > kEpsilon) {
+ sin = y / magnitude
+ cos = x / magnitude
+ } else {
+ sin = 0.0
+ cos = 1.0
+ }
+ } else {
+ cos = x
+ sin = y
+ }
+ value = Math.atan2(sin, cos).radians
+ }
+
+ val radian get() = value
+ val degree get() = value.inDegrees()
+
+ fun isParallel(rotation: Rotation2d) = (this - rotation).radian epsilonEquals 0.0.radians
+
+ operator fun minus(other: Rotation2d) = plus(-other)
+ operator fun unaryMinus() = Rotation2d(-value)
+
+ operator fun times(other: Double) = Rotation2d(value * other)
+
+ operator fun plus(other: Rotation2d): Rotation2d {
+ return Rotation2d(
+ cos * other.cos - sin * other.sin,
+ cos * other.sin + sin * other.cos,
+ true
+ )
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return other is Rotation2d && other.sin epsilonEquals this.sin && other.cos epsilonEquals this.cos
+ }
+}
diff --git a/src/main/kotlin/org/team5499/monkeyLib/math/geometry/Geometric.kt b/src/main/kotlin/org/team5419/fault/math/geometry/State.kt
similarity index 53%
rename from src/main/kotlin/org/team5499/monkeyLib/math/geometry/Geometric.kt
rename to src/main/kotlin/org/team5419/fault/math/geometry/State.kt
index 91aaa0c..191567c 100644
--- a/src/main/kotlin/org/team5499/monkeyLib/math/geometry/Geometric.kt
+++ b/src/main/kotlin/org/team5419/fault/math/geometry/State.kt
@@ -1,16 +1,18 @@
-package org.team5499.monkeyLib.math.geometry
+package org.team5419.fault.math.geometry
-import org.team5499.monkeyLib.util.Interpolable
-import org.team5499.monkeyLib.util.CSVWritable
+import org.team5419.fault.util.Interpolable
+import org.team5419.fault.util.CSVWritable
-interface Geometric : Interpolable, CSVWritable {
+interface State : Interpolable, CSVWritable {
- public override fun toString(): String
+ fun distance(other: T): Double
- public override fun equals(other: Any?): Boolean
+ public override fun toString(): String
public override fun toCSV(): String
+ public override fun equals(other: Any?): Boolean
+
public override fun interpolate(other: T, x: Double): T
public override fun hashCode(): Int
diff --git a/src/main/kotlin/org/team5419/fault/math/geometry/Twist2d.kt b/src/main/kotlin/org/team5419/fault/math/geometry/Twist2d.kt
new file mode 100644
index 0000000..53d4823
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/math/geometry/Twist2d.kt
@@ -0,0 +1,40 @@
+package org.team5419.fault.math.geometry
+
+import org.team5419.fault.math.kEpsilon
+import org.team5419.fault.math.units.Meter
+import org.team5419.fault.math.units.SIUnit
+import org.team5419.fault.math.units.derived.Radian
+import org.team5419.fault.math.units.meters
+import kotlin.math.hypot
+import kotlin.math.sin
+import kotlin.math.cos
+import kotlin.math.abs
+
+class Twist2d constructor(
+ val dx: SIUnit,
+ val dy: SIUnit,
+ val dTheta: SIUnit
+) {
+
+ val norm get() = if (dy.value == 0.0) dx.absoluteValue else hypot(dx.value, dy.value).meters
+
+ val asPose: Pose2d
+ get() {
+ val dTheta = this.dTheta.value
+ val sinTheta = sin(dTheta)
+ val cosTheta = cos(dTheta)
+
+ val (s, c) = if (abs(dTheta) < kEpsilon) {
+ 1.0 - 1.0 / 6.0 * dTheta * dTheta to .5 * dTheta
+ } else {
+ sinTheta / dTheta to (1.0 - cosTheta) / dTheta
+ }
+ return Pose2d(
+ Vector2(dx * s - dy * c, dx * c + dy * s),
+ Rotation2d(cosTheta, sinTheta, false)
+ )
+ }
+
+ operator fun times(scale: Double) =
+ Twist2d(dx * scale, dy * scale, dTheta * scale)
+}
diff --git a/src/main/kotlin/org/team5419/fault/math/geometry/Vector2.kt b/src/main/kotlin/org/team5419/fault/math/geometry/Vector2.kt
new file mode 100644
index 0000000..e50e2d5
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/math/geometry/Vector2.kt
@@ -0,0 +1,77 @@
+
+package org.team5419.fault.math.geometry
+
+import org.team5419.fault.math.epsilonEquals
+import org.team5419.fault.math.units.meters
+import org.team5419.fault.math.units.SIUnit
+import org.team5419.fault.math.units.SIKey
+import org.team5419.fault.math.units.Meter
+import kotlin.math.hypot
+
+fun Rotation2d.toTranslation() = Vector2(cos.meters, sin.meters)
+typealias Vector2d = Vector2
+
+@Suppress("TooManyFunctions")
+data class Vector2 constructor(
+ val x: SIUnit,
+ val y: SIUnit
+) : State> {
+
+ constructor() : this(SIUnit(0.0), SIUnit(0.0))
+
+ // Vector to Translation3d
+ constructor(
+ rotation: Rotation2d,
+ distance: SIUnit = SIUnit(0.0)
+ ) : this (distance * rotation.cos, distance * rotation.sin)
+
+ val norm get() = hypot(x.value, y.value).meters
+
+ override fun interpolate(endValue: Vector2, t: Double) = when {
+ t <= 0 -> this
+ t >= 1 -> endValue
+ else -> Vector2(
+ x.lerp(endValue.x, t),
+ y.lerp(endValue.y, t)
+ )
+ }
+
+ override fun distance(other: Vector2): Double {
+ val x = this.x.value - other.x.value
+ val y = this.y.value - other.y.value
+ return hypot(x, y)
+ }
+
+ operator fun plus(other: Vector2) = Vector2(x + other.x, y + other.y)
+ operator fun minus(other: Vector2) = Vector2(x - other.x, y - other.y)
+
+ operator fun times(other: Rotation2d) = Vector2(
+ x * other.cos - y * other.sin,
+ x * other.sin + y * other.cos
+ )
+
+ operator fun times(other: Number): Vector2 {
+ val factor = other.toDouble()
+ return Vector2(x * factor, y * factor)
+ }
+
+ operator fun div(other: Number): Vector2 {
+ val factor = other.toDouble()
+ return Vector2(x / factor, y / factor)
+ }
+
+ operator fun unaryMinus() = Vector2(-x, -y)
+
+ override fun toCSV() = "${x.value}, ${y.value}"
+
+ override fun toString() = toCSV()
+
+ override fun hashCode() = super.hashCode()
+
+ override fun equals(other: Any?): Boolean {
+ if (other is Vector2<*>) {
+ return other.x.value epsilonEquals this.x.value && other.y.value epsilonEquals this.y.value
+ }
+ return false
+ }
+}
diff --git a/src/main/kotlin/org/team5419/fault/math/localization/PositionTracker.kt b/src/main/kotlin/org/team5419/fault/math/localization/PositionTracker.kt
new file mode 100644
index 0000000..5ac0ea3
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/math/localization/PositionTracker.kt
@@ -0,0 +1,58 @@
+package org.team5419.fault.math.localization
+
+import edu.wpi.first.wpilibj.Timer
+import org.team5419.fault.math.geometry.Pose2d
+import org.team5419.fault.math.geometry.Rotation2d
+import org.team5419.fault.math.units.SIUnit
+import org.team5419.fault.math.units.Second
+import org.team5419.fault.math.units.seconds
+import org.team5419.fault.util.CSVWritable
+import org.team5419.fault.util.Source
+import kotlin.reflect.KProperty
+
+abstract class PositionTracker(
+ val heading: Source,
+ private val buffer: TimeInterpolatableBuffer = TimeInterpolatableBuffer()
+) : Source, CSVWritable {
+
+ var robotPosition = Pose2d()
+ private set
+
+ private var lastHeading = Rotation2d()
+ private var headingOffset = Rotation2d()
+
+ override fun invoke() = robotPosition
+
+ @Synchronized fun reset(pose: Pose2d = Pose2d()) = resetInternal(pose)
+
+ protected open fun resetInternal(pose: Pose2d) {
+ robotPosition = pose
+ val newHeading = heading()
+ lastHeading = newHeading
+ headingOffset = -newHeading + pose.rotation
+ buffer.clear()
+ }
+
+ @Synchronized
+ fun update() {
+ val newHeading = heading()
+ val headingDelta = newHeading - lastHeading
+ val newRobotPosition = robotPosition + update(headingDelta)
+ robotPosition = Pose2d(
+ newRobotPosition.translation,
+ newHeading + headingOffset // check this
+ )
+ lastHeading = newHeading
+
+ buffer[Timer.getFPGATimestamp().seconds] = robotPosition
+ }
+
+ protected abstract fun update(deltaHeading: Rotation2d): Pose2d
+
+ operator fun get(timestamp: SIUnit) = buffer[timestamp] ?: Pose2d()
+
+ operator fun getValue(thisRef: Any?, property: KProperty<*>): Pose2d = robotPosition
+ operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Pose2d) = reset(value)
+
+ override fun toCSV() = robotPosition.toCSV()
+}
diff --git a/src/main/kotlin/org/team5419/fault/math/localization/TankPositionTracker.kt b/src/main/kotlin/org/team5419/fault/math/localization/TankPositionTracker.kt
new file mode 100644
index 0000000..f7f9c1b
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/math/localization/TankPositionTracker.kt
@@ -0,0 +1,33 @@
+package org.team5419.fault.math.localization
+
+import org.team5419.fault.math.geometry.Pose2d
+import org.team5419.fault.math.geometry.Rotation2d
+import org.team5419.fault.math.geometry.Twist2d
+import org.team5419.fault.math.units.meters
+import org.team5419.fault.util.Source
+
+class TankPositionTracker(
+ headingSource: Source,
+ val leftDistanceSource: Source,
+ val rightDistanceSource: Source,
+ buffer: TimeInterpolatableBuffer = TimeInterpolatableBuffer()
+) : PositionTracker(headingSource, buffer) {
+
+ private var lastLeftDistance = 0.0
+ private var lastRightDistance = 0.0
+
+ override fun resetInternal(pose: Pose2d) {
+ lastLeftDistance = leftDistanceSource()
+ lastRightDistance = rightDistanceSource()
+ super.resetInternal(pose)
+ }
+
+ override fun update(deltaHeading: Rotation2d): Pose2d {
+ val currentLeftDistance = leftDistanceSource()
+ val currentRightDistance = rightDistanceSource()
+ val leftDelta = currentLeftDistance - lastLeftDistance
+ val rightDelta = currentRightDistance - lastRightDistance
+ val dx = (leftDelta + rightDelta) / 2.0
+ return Twist2d(dx.meters, 0.0.meters, deltaHeading.radian).asPose
+ }
+}
diff --git a/src/main/kotlin/org/team5419/fault/math/localization/TimeInterpolatableBuffer.kt b/src/main/kotlin/org/team5419/fault/math/localization/TimeInterpolatableBuffer.kt
new file mode 100644
index 0000000..63f0f89
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/math/localization/TimeInterpolatableBuffer.kt
@@ -0,0 +1,62 @@
+package org.team5419.fault.math.localization
+
+import edu.wpi.first.wpilibj.Timer
+import org.team5419.fault.math.units.SIUnit
+import org.team5419.fault.math.units.Second
+import org.team5419.fault.math.units.seconds
+import org.team5419.fault.util.Interpolable
+import org.team5419.fault.util.Source
+import org.team5419.fault.math.units.unitlessValue
+import org.team5419.fault.math.units.operations.times
+import org.team5419.fault.math.units.operations.div
+
+import java.util.TreeMap
+
+class TimeInterpolatableBuffer>(
+ private val historySpan: SIUnit = SIUnit(1.0),
+ private val timeSource: Source> = { Timer.getFPGATimestamp().seconds }
+) {
+
+ private val bufferMap = TreeMap, T>()
+
+ operator fun set(time: SIUnit, value: T): T? {
+ cleanUp()
+ return bufferMap.put(time, value)
+ }
+
+ private fun cleanUp() {
+ val currentTime = timeSource()
+
+ while (bufferMap.isNotEmpty()) {
+ val entry = bufferMap.lastEntry()
+ if (currentTime - entry.key >= historySpan) {
+ bufferMap.remove(entry.key)
+ } else {
+ return
+ }
+ }
+ }
+
+ fun clear() {
+ bufferMap.clear()
+ }
+
+ @Suppress("ReturnCount")
+ operator fun get(time: SIUnit): T? {
+ if (bufferMap.isEmpty()) return null
+
+ bufferMap[time]?.let { return it }
+
+ val topBound = bufferMap.ceilingEntry(time)
+ val bottomBound = bufferMap.floorEntry(time)
+
+ return when {
+ topBound == null -> bottomBound.value
+ bottomBound == null -> topBound.value
+ else -> bottomBound.value.interpolate(
+ topBound.value,
+ ((time - bottomBound.key) / (topBound.key - bottomBound.key)).unitlessValue
+ )
+ }
+ }
+}
diff --git a/src/main/kotlin/org/team5419/fault/math/physics/DCMotorTransmission.kt b/src/main/kotlin/org/team5419/fault/math/physics/DCMotorTransmission.kt
new file mode 100644
index 0000000..0b05466
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/math/physics/DCMotorTransmission.kt
@@ -0,0 +1,78 @@
+package org.team5419.fault.math.physics
+
+import org.team5419.fault.math.kEpsilon
+
+/*
+ * Implementation from:
+ * NASA Ames Robotics "The Cheesy Poofs"
+ * Team 254
+ */
+
+/**
+ * Model of a DC motor rotating a shaft. All parameters refer to the output (e.g. should already consider gearing
+ * and efficiency losses). The motor is assumed to be symmetric forward/reverse.
+ */
+class DCMotorTransmission(
+ val speedPerVolt: Double, // rad/s per V (no load),
+ private val torquePerVolt: Double, // N m per V (stall),
+ val frictionVoltage: Double // V
+) {
+
+ /**
+ * Returns the free speed of the motor at the specified voltage
+ * @param voltage specified voltage
+ * @return free speed
+ */
+ fun getFreeSpeedAtVoltage(voltage: Double): Double {
+ return when {
+ voltage > kEpsilon -> Math.max(0.0, voltage - frictionVoltage) * speedPerVolt
+ voltage < kEpsilon -> Math.min(0.0, voltage + frictionVoltage) * speedPerVolt
+ else -> 0.0
+ }
+ }
+
+ /**
+ * Returns the torque produced by the motor
+ * @param outputSpeed The speed that is being outputted by the motor
+ * @param voltage The voltage through the motor
+ * @return torque
+ */
+ fun getTorqueForVoltage(outputSpeed: Double, voltage: Double): Double {
+ var effectiveVoltage = voltage
+ when {
+ outputSpeed > kEpsilon -> // Forward motion, rolling friction.
+ effectiveVoltage -= frictionVoltage
+ outputSpeed < kEpsilon -> // Reverse motion, rolling friction.
+ effectiveVoltage += frictionVoltage
+ voltage > kEpsilon -> // System is static, forward torque.
+ effectiveVoltage = Math.max(0.0, voltage - frictionVoltage)
+ voltage < kEpsilon -> // System is static, reverse torque.
+ effectiveVoltage = Math.min(0.0, voltage + frictionVoltage)
+ else -> // System is idle.
+ return 0.0
+ }
+ return torquePerVolt * (-outputSpeed / speedPerVolt + effectiveVoltage)
+ }
+
+ /**
+ * Returns the voltage going through the motor
+ * @param outputSpeed The speed that is being outputted by the motor
+ * @param torque Torque produced by the motor
+ * @return voltage
+ */
+ fun getVoltageForTorque(outputSpeed: Double, torque: Double): Double {
+ val fv: Double = when {
+ outputSpeed > kEpsilon -> // Forward motion, rolling friction.
+ frictionVoltage
+ outputSpeed < kEpsilon -> // Reverse motion, rolling friction.
+ -frictionVoltage
+ torque > kEpsilon -> // System is static, forward torque.
+ frictionVoltage
+ torque < kEpsilon -> // System is static, reverse torque.
+ -frictionVoltage
+ else -> // System is idle.
+ return 0.0
+ }
+ return torque / torquePerVolt + outputSpeed / speedPerVolt + fv
+ }
+}
diff --git a/src/main/kotlin/org/team5419/fault/math/physics/DifferentialDrive.kt b/src/main/kotlin/org/team5419/fault/math/physics/DifferentialDrive.kt
new file mode 100644
index 0000000..2f306e6
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/math/physics/DifferentialDrive.kt
@@ -0,0 +1,438 @@
+@file:Suppress("MemberVisibilityCanBePrivate", "unused")
+
+package org.team5419.fault.math.physics
+
+import org.team5419.fault.math.epsilonEquals
+import org.team5419.fault.math.kEpsilon
+import org.team5419.fault.util.CSVWritable
+import java.text.DecimalFormat
+import java.util.Arrays
+
+/*
+ * Implementation from:
+ * NASA Ames Robotics "The Cheesy Poofs"
+ * Team 254
+ */
+
+/**
+ * Dynamic model a differential drive robot. Note: to simplify things, this math assumes the center of mass is
+ * coincident with the kinematic center of rotation (e.g. midpoint of the center axle).
+ */
+@Suppress("LongParameterList", "TooManyFunctions")
+class DifferentialDrive(
+ /**
+ * Equivalent mass when accelerating purely linearly, in kg.
+ * This is equivalent in that it also absorbs the effects of drivetrain inertia.
+ * Measure by doing drivetrain acceleration characterizaton in a straight line.
+ */
+ private val mass: Double,
+ /**
+ * Equivalent moment of inertia when accelerating purely angularly in kg m^2.
+ * This is equivalent in that it also absorbs the effects of drivetrain inertia.
+ * Measure by doing drivetrain acceleration characterization while turing in place.
+ */
+ private val moi: Double,
+ /**
+ * Drag torque (proportional to angular velocity) that resists turning in Nm/rad/s.
+ * Empirical testing of 254's drivebase showed that there was an unexplained loss in torque ~proportional to angular
+ * velocity, likely due to scrub of wheels.
+ * NOTE: this may not be a purely linear tern, and 254 has done limited testing, but this factor may help a model
+ * to better match reality.
+ */
+ private val angularDrag: Double,
+ /**
+ * The radius of the wheel
+ */
+ val wheelRadius: Double, // m,
+ /**
+ * Effective kinematic wheelbase radius. Might be larger than theoretical to compensate for skid steer. Measure by
+ * turning the robot in place several times and figuring out what the equivalent wheel base radius is.
+ */
+ private val effectiveWheelBaseRadius: Double, // m
+ /**
+ * DC Motor transmission for both sides of the drivetrain.
+ */
+ private val leftTransmission: DCMotorTransmission,
+ private val rightTransmission: DCMotorTransmission
+) {
+
+ /**
+ * Solve forward kinematics to get chassis motion from wheel motion.
+ * Could be either acceleration or velocity.
+ */
+ fun solveForwardKinematics(wheelMotion: WheelState): ChassisState = ChassisState(
+ wheelRadius * (wheelMotion.right + wheelMotion.left) / 2.0,
+ wheelRadius * (wheelMotion.right - wheelMotion.left) / (2.0 * effectiveWheelBaseRadius)
+ )
+
+ /**
+ * Solve inverse kinematics to get wheel motion from chassis motion.
+ * Could be either acceleration or velocity.
+ */
+ fun solveInverseKinematics(chassisMotion: ChassisState): WheelState = WheelState(
+ (chassisMotion.linear - effectiveWheelBaseRadius * chassisMotion.angular) / wheelRadius,
+ (chassisMotion.linear + effectiveWheelBaseRadius * chassisMotion.angular) / wheelRadius
+ )
+
+ /**
+ * Solve forward dynamics for torques and accelerations.
+ */
+ fun solveForwardDynamics(chassisVelocity: ChassisState, voltage: WheelState): DriveDynamics = solveForwardDynamics(
+ solveInverseKinematics(chassisVelocity),
+ chassisVelocity,
+ (chassisVelocity.angular / chassisVelocity.linear).let { if (it.isNaN()) 0.0 else it },
+ voltage
+ )
+
+ /**
+ * Get the voltage simply from the Kv and the friction voltage of the transmissions
+ */
+ fun getVoltagesFromkV(velocities: DifferentialDrive.WheelState): WheelState {
+ return DifferentialDrive.WheelState(
+ velocities.left / leftTransmission.speedPerVolt +
+ leftTransmission.frictionVoltage * Math.signum(velocities.left),
+ velocities.right / rightTransmission.speedPerVolt +
+ rightTransmission.frictionVoltage * Math.signum(velocities.right)
+ )
+ }
+
+ /**
+ * Solve forward dynamics for torques and accelerations.
+ */
+ fun solveForwardDynamics(wheelVelocity: WheelState, voltage: WheelState): DriveDynamics {
+ val chassisVelocity = solveForwardKinematics(wheelVelocity)
+ return solveForwardDynamics(
+ wheelVelocity,
+ chassisVelocity,
+ (chassisVelocity.angular / chassisVelocity.linear).let { if (it.isNaN()) 0.0 else it },
+ voltage
+ )
+ }
+
+ /**
+ * Solve forward dynamics for torques and accelerations.
+ */
+ fun solveForwardDynamics(
+ wheelVelocity: WheelState,
+ chassisVelocity: ChassisState,
+ curvature: Double,
+ voltage: WheelState
+ ): DriveDynamics {
+
+ val wheelTorque: WheelState
+ val chassisAcceleration: ChassisState
+ val wheelAcceleration: WheelState
+ val dcurvature: Double
+
+ val leftStationary = wheelVelocity.left epsilonEquals 0.0 && Math.abs(
+ voltage.left
+ ) < leftTransmission.frictionVoltage
+ val rightStationary = wheelVelocity.right epsilonEquals 0.0 && Math.abs(
+ voltage.right
+ ) < rightTransmission.frictionVoltage
+
+ // Neither side breaks static friction, so we remain stationary.
+ if (leftStationary && rightStationary) {
+ wheelTorque = WheelState()
+ chassisAcceleration = ChassisState()
+ wheelAcceleration = WheelState()
+ dcurvature = 0.0
+ } else {
+ // Solve for motor torques generated on each side.
+ wheelTorque = WheelState(
+ leftTransmission.getTorqueForVoltage(wheelVelocity.left, voltage.left),
+ rightTransmission.getTorqueForVoltage(wheelVelocity.right, voltage.right)
+ )
+
+ // Add forces and torques about the center of mass.
+ chassisAcceleration = ChassisState(
+ (wheelTorque.right + wheelTorque.left) / (wheelRadius * mass),
+ // (Tr - Tl) / r_w * r_wb - drag * w = I * angular_accel
+ effectiveWheelBaseRadius * (wheelTorque.right - wheelTorque.left) /
+ (wheelRadius * moi) - chassisVelocity.angular * angularDrag / moi
+ )
+
+ // Solve for change in curvature from angular acceleration.
+ // total angular accel = linear_accel * curvature + v^2 * dcurvature
+ dcurvature = ((chassisAcceleration.angular - chassisAcceleration.linear * curvature) /
+ (chassisVelocity.linear * chassisVelocity.linear)).let { if (it.isNaN()) 0.0 else it }
+
+ // Resolve chassis accelerations to each wheel.
+ wheelAcceleration = WheelState(
+ chassisAcceleration.linear - chassisAcceleration.angular * effectiveWheelBaseRadius,
+ chassisAcceleration.linear + chassisAcceleration.angular * effectiveWheelBaseRadius
+ )
+ }
+ return DriveDynamics(
+ curvature,
+ dcurvature,
+ chassisVelocity,
+ chassisAcceleration,
+ wheelVelocity,
+ wheelAcceleration,
+ voltage,
+ wheelTorque
+ )
+ }
+
+ /**
+ * Solve inverse dynamics for torques and voltages
+ */
+ fun solveInverseDynamics(chassisVelocity: ChassisState, chassisAcceleration: ChassisState): DriveDynamics {
+
+ val curvature = (chassisVelocity.angular / chassisVelocity.linear)
+ .let { if (it.isNaN()) 0.0 else it }
+
+ val dcurvature = ((chassisAcceleration.angular - chassisAcceleration.linear * curvature) /
+ (chassisVelocity.linear * chassisVelocity.linear))
+ .let { if (it.isNaN()) 0.0 else it }
+
+ return solveInverseDynamics(
+ solveInverseKinematics(chassisVelocity),
+ chassisVelocity,
+ solveInverseKinematics(chassisAcceleration),
+ chassisAcceleration,
+ curvature,
+ dcurvature
+ )
+ }
+
+ /**
+ * Solve inverse dynamics for torques and voltages
+ */
+ fun solveInverseDynamics(wheelVelocity: WheelState, wheelAcceleration: WheelState): DriveDynamics {
+
+ val chassisVelocity = solveForwardKinematics(wheelVelocity)
+ val chassisAcceleration = solveForwardKinematics(wheelAcceleration)
+
+ val curvature = (chassisVelocity.angular / chassisVelocity.linear)
+ .let { if (it.isNaN()) 0.0 else it }
+
+ val dcurvature = ((chassisAcceleration.angular - chassisAcceleration.linear * curvature) /
+ (chassisVelocity.linear * chassisVelocity.linear))
+ .let { if (it.isNaN()) 0.0 else it }
+
+ return solveInverseDynamics(
+ wheelVelocity,
+ chassisVelocity,
+ wheelAcceleration,
+ chassisAcceleration,
+ curvature,
+ dcurvature
+ )
+ }
+
+ /**
+ * Solve inverse dynamics for torques and voltages
+ */
+ fun solveInverseDynamics(
+ wheelVelocity: WheelState,
+ chassisVelocity: ChassisState,
+ wheelAcceleration: WheelState,
+ chassisAcceleration: ChassisState,
+ curvature: Double,
+ dcurvature: Double
+ ): DriveDynamics {
+
+ // Determine the necessary torques on the left and right wheels to produce the desired wheel accelerations.
+ val wheelTorque = WheelState(
+ wheelRadius / 2.0 * (chassisAcceleration.linear * mass -
+ chassisAcceleration.angular * moi / effectiveWheelBaseRadius -
+ chassisVelocity.angular * angularDrag / effectiveWheelBaseRadius),
+
+ wheelRadius / 2.0 * (chassisAcceleration.linear * mass +
+ chassisAcceleration.angular * moi / effectiveWheelBaseRadius +
+ chassisVelocity.angular * angularDrag / effectiveWheelBaseRadius)
+ )
+
+ // Solve for input voltages
+ val voltage = WheelState(
+ leftTransmission.getVoltageForTorque(wheelVelocity.left, wheelTorque.left),
+ rightTransmission.getVoltageForTorque(wheelVelocity.right, wheelTorque.right)
+ )
+
+ return DriveDynamics(
+ curvature,
+ dcurvature,
+ chassisVelocity,
+ chassisAcceleration,
+ wheelVelocity,
+ wheelAcceleration,
+ voltage,
+ wheelTorque
+ )
+ }
+
+ /**
+ * Solve for the max absolute velocity that the drivetrain is capable of given a max voltage and curvature
+ */
+ @Suppress("ReturnCount")
+ fun getMaxAbsVelocity(curvature: Double, /*double dcurvature, */ maxAbsVoltage: Double): Double {
+ // Alternative implementation:
+ // (Tr - Tl) * r_wb / r_w = I * v^2 * dk
+ // (Tr + Tl) / r_w = 0
+ // T = Tr = -Tl
+ // 2T * r_wb / r_w = I*v^2*dk
+ // T = 2*I*v^2*dk*r_w/r_wb
+ // T = kt*(-vR/kv + V) = -kt*(-vL/vmax + V)
+ // Vr = v * (1 + k*r_wb)
+ // 0 = 2*I*dk*r_w/r_wb * v^2 + kt * ((1 + k*r_wb) * v / kv) - kt * V
+ // solve using quadratic formula?
+ // -b +/- sqrt(b^2 - 4*a*c) / (2a)
+
+ // k = w / v
+ // v = r_w*(wr + wl) / 2
+ // w = r_w*(wr - wl) / (2 * r_wb)
+ // Plug in maxAbsVoltage for each wheel.
+
+ val leftSpeedAtMaxVoltage = leftTransmission.getFreeSpeedAtVoltage(maxAbsVoltage)
+ val rightSpeedAtMaxVoltage = rightTransmission.getFreeSpeedAtVoltage(maxAbsVoltage)
+
+ if (curvature epsilonEquals 0.0) {
+ return wheelRadius * Math.min(leftSpeedAtMaxVoltage, rightSpeedAtMaxVoltage)
+ }
+ if (curvature.isInfinite()) {
+ // Turn in place. Return value meaning becomes angular velocity.
+ val wheelSpeed = Math.min(leftSpeedAtMaxVoltage, rightSpeedAtMaxVoltage)
+ return Math.signum(curvature) * wheelRadius * wheelSpeed / effectiveWheelBaseRadius
+ }
+
+ val rightSpeedIfLeftMax =
+ leftSpeedAtMaxVoltage * (effectiveWheelBaseRadius * curvature + 1.0) /
+ (1.0 - effectiveWheelBaseRadius * curvature)
+
+ if (Math.abs(rightSpeedIfLeftMax) <= rightSpeedAtMaxVoltage + kEpsilon) {
+ // Left max is active constraint.
+ return wheelRadius * (leftSpeedAtMaxVoltage + rightSpeedIfLeftMax) / 2.0
+ }
+ val leftSpeedIfRightMax =
+ rightSpeedAtMaxVoltage * (1.0 - effectiveWheelBaseRadius * curvature) /
+ (1.0 + effectiveWheelBaseRadius * curvature)
+
+ // Right at max is active constraint.
+ return wheelRadius * (rightSpeedAtMaxVoltage + leftSpeedIfRightMax) / 2.0
+ }
+
+ data class MinMax(val min: Double, val max: Double)
+
+ // Curvature is redundant here in the case that chassisVelocity is not purely angular. It is the responsibility of
+ // the caller to ensure that curvature = angular vel / linear vel in these cases.
+ @Suppress("ComplexMethod", "NestedBlockDepth")
+ fun getMinMaxAcceleration(
+ chassisVelocity: ChassisState,
+ curvature: Double, /*double dcurvature,*/
+ maxAbsVoltage: Double
+ ): MinMax {
+ val wheelVelocities = solveInverseKinematics(chassisVelocity)
+
+ var min = java.lang.Double.POSITIVE_INFINITY
+ var max = java.lang.Double.NEGATIVE_INFINITY
+
+ // Math:
+ // (Tl + Tr) / r_w = m*a
+ // (Tr - Tl) / r_w * r_wb - drag*w = i*(a * k + v^2 * dk)
+
+ // 2 equations, 2 unknowns.
+ // Solve for a and (Tl|Tr)
+
+ val linearTerm = if (java.lang.Double.isInfinite(curvature)) 0.0 else mass * effectiveWheelBaseRadius
+ val angularTerm = if (java.lang.Double.isInfinite(curvature)) moi else moi * curvature
+
+ val dragTorque = chassisVelocity.angular * angularDrag
+
+ // Check all four cases and record the min and max valid accelerations.
+ for (left in Arrays.asList(false, true)) {
+ for (sign in Arrays.asList(1.0, -1.0)) {
+
+ val fixedTransmission = if (left) leftTransmission else rightTransmission
+ val variableTransmission = if (left) rightTransmission else leftTransmission
+ val fixedTorque = fixedTransmission.getTorqueForVoltage(wheelVelocities[left], sign * maxAbsVoltage)
+ var variableTorque: Double
+ // NOTE: variable_torque is wrong. Units don't work out correctly. We made a math error somewhere...
+ // Leaving this "as is" for code release so as not to be disingenuous, but this whole function needs
+ // revisiting in the future...
+ variableTorque = if (left) {
+ (/*-moi * chassisVelocity.linear * chassisVelocity.linear * dcurvature*/
+ -dragTorque * mass * wheelRadius + fixedTorque * (linearTerm + angularTerm)) /
+ (linearTerm - angularTerm)
+ } else {
+ (/*moi * chassisVelocity.linear * chassisVelocity.linear * dcurvature*/
+ +dragTorque * mass * wheelRadius + fixedTorque * (linearTerm - angularTerm)) /
+ (linearTerm + angularTerm)
+ }
+ val variableVoltage = variableTransmission.getVoltageForTorque(wheelVelocities[!left], variableTorque)
+ if (Math.abs(variableVoltage) <= maxAbsVoltage + kEpsilon) {
+ val accel = if (java.lang.Double.isInfinite(curvature)) {
+ (if (left) -1.0 else 1.0) * (fixedTorque - variableTorque) *
+ effectiveWheelBaseRadius / (moi * wheelRadius) - dragTorque / moi
+ /*- chassisVelocity.linear * chassisVelocity.linear * dcurvature*/
+ } else {
+ (fixedTorque + variableTorque) / (mass * wheelRadius)
+ }
+ min = Math.min(min, accel)
+ max = Math.max(max, accel)
+ }
+ }
+ }
+ return MinMax(min, max)
+ }
+
+ /**
+ * Can refer to velocity or acceleration depending on context.
+ */
+ class ChassisState {
+ var linear: Double = 0.toDouble()
+ var angular: Double = 0.toDouble()
+
+ constructor(linear: Double, angular: Double) {
+ this.linear = linear
+ this.angular = angular
+ }
+
+ constructor()
+
+ override fun toString(): String {
+ val fmt = DecimalFormat("#0.000")
+ return fmt.format(linear) + ", " + fmt.format(angular)
+ }
+
+ operator fun minus(other: ChassisState) =
+ ChassisState(this.linear - other.linear, this.angular - other.angular)
+
+ operator fun times(scalar: Double) = ChassisState(linear * scalar, angular * scalar)
+ operator fun div(scalar: Double) = this * (1 / scalar)
+ }
+
+ /**
+ * Can refer to velocity, acceleration, torque, voltage, etc., depending on context.
+ */
+ class WheelState(val left: Double, val right: Double) {
+ constructor() : this(0.0, 0.0)
+
+ operator fun get(isLeft: Boolean) = if (isLeft) left else right
+
+ override fun toString(): String {
+ val fmt = DecimalFormat("#0.000")
+ return fmt.format(left) + ", " + fmt.format(right)
+ }
+ }
+
+ /**
+ * Full state dynamics of the drivetrain
+ */
+ class DriveDynamics(
+ val curvature: Double, // m^-1
+ val dcurvature: Double, // m^-2
+ val chassisVelocity: ChassisState, // m/s
+ val chassisAcceleration: ChassisState, // m/s^2
+ val wheelVelocity: WheelState, // rad/s
+ val wheelAcceleration: WheelState, // rad/s^2
+ val voltage: WheelState, // V
+ val wheelTorque: WheelState // N m
+ ) : CSVWritable {
+ override fun toCSV(): String {
+ return "$curvature,$dcurvature,$chassisVelocity, $chassisAcceleration, " +
+ "$wheelVelocity, $wheelAcceleration, $voltage, $wheelTorque"
+ }
+ }
+}
diff --git a/src/main/kotlin/org/team5419/fault/math/pid/PIDF.kt b/src/main/kotlin/org/team5419/fault/math/pid/PIDF.kt
new file mode 100644
index 0000000..ddf0bdf
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/math/pid/PIDF.kt
@@ -0,0 +1,137 @@
+package org.team5419.fault.math.pid
+
+import org.team5419.fault.math.kEpsilon
+import org.team5419.fault.util.DoubleSource
+import kotlin.math.absoluteValue
+
+// adapted from:
+// https://github.com/Team1323/2019DeepSpace/blob/13d5d4ac56c2da11d112ab9ab84241c8b6878018/src/main/java/com/team1323/lib/util/SynchronousPIDF.java
+class PIDF(
+ var kP: Double = 0.0,
+ var kI: Double = 0.0,
+ var kD: Double = 0.0,
+ var kF: Double = 0.0,
+ private val feedbackVariable: DoubleSource
+) {
+
+ constructor(
+ kP: Double = 0.0,
+ kI: Double = 0.0,
+ kD: Double = 0.0,
+ feedbackVariable: DoubleSource
+ ): this(kP, kI, kD, 0.0, feedbackVariable)
+
+ constructor(
+ kP: Double = 0.0,
+ kD: Double = 0.0,
+ feedbackVariable: DoubleSource
+ ): this(kP, 0.0, kD, 0.0, feedbackVariable)
+
+ var setpoint = 0.0
+ set(value) {
+ if (value == field) return
+ field = if (maximumInput > minimumInput) {
+ if (value > maximumInput) maximumInput
+ else if (value < minimumInput) minimumInput
+ else value
+ } else value
+ }
+
+ var maximumOutput = 1.0
+ private set
+ var minimumOutput = -1.0
+ private set
+
+ var maximumInput = 0.0
+ private set
+ var minimumInput = 0.0
+ private set
+
+ var deadband = 0.0
+
+ var continuous = false
+ var inverted = false
+
+ var error = 0.0
+ private set
+ var output = 0.0
+ private set
+
+ private var lastError = 0.0
+ private var lastInput = Double.NaN
+ private var accumulator = 0.0
+
+ @Suppress("ComplexMethod")
+ fun calculate(dt: Double): Double {
+ var newDt = dt
+ if (newDt < kEpsilon) newDt = kEpsilon
+ val input = feedbackVariable()
+ lastInput = input
+ error = setpoint - input
+ if (continuous) {
+ if (error.absoluteValue > (maximumInput - minimumInput) / 2.0) {
+ if (error > 0) {
+ error = error - maximumInput + minimumInput
+ } else {
+ error = error + maximumInput - minimumInput
+ }
+ }
+ }
+ if ((error * kP < maximumOutput) && (error * kP > minimumOutput)) {
+ accumulator += error * newDt
+ } else {
+ accumulator = 0.0
+ }
+
+ val propError = if (error.absoluteValue < deadband) 0.0 else error
+
+ output = kP * propError + kI * accumulator + kD * (error - lastError) / dt + kF * setpoint
+ lastError = error
+
+ if (output > maximumOutput) {
+ output = maximumOutput
+ } else if (output < minimumOutput) {
+ output = minimumOutput
+ }
+
+ return output * if (inverted) -1.0 else 1.0
+ }
+
+ fun setPID(kP: Double, kI: Double, kD: Double) = setPID(kP, kI, kD, 0.0)
+ fun setPID(kP: Double, kI: Double, kD: Double, kF: Double) {
+ this.kP = kP
+ this.kI = kI
+ this.kD = kD
+ this.kF = kF
+ }
+
+ fun setInputRange(min: Double, max: Double) {
+ if (min > max) {
+ println("ERROR: tried to set min input to greater than max input in PID")
+ return
+ }
+ minimumInput = min
+ maximumInput = max
+ }
+
+ fun setOutputRange(min: Double, max: Double) {
+ if (min > max) {
+ println("ERROR: tried to set min output to greater than max output in PID")
+ return
+ }
+ minimumOutput = min
+ maximumOutput = max
+ }
+
+ fun reset() {
+ lastInput = Double.NaN
+ lastError = 0.0
+ output = 0.0
+ setpoint = 0.0
+ resetAccumulator()
+ }
+
+ fun resetAccumulator() {
+ accumulator = 0.0
+ }
+}
diff --git a/src/main/kotlin/org/team5499/monkeyLib/math/splines/CubicHermiteSpline.kt b/src/main/kotlin/org/team5419/fault/math/splines/CubicHermiteSpline.kt
similarity index 67%
rename from src/main/kotlin/org/team5499/monkeyLib/math/splines/CubicHermiteSpline.kt
rename to src/main/kotlin/org/team5419/fault/math/splines/CubicHermiteSpline.kt
index 63b03d8..342a66f 100644
--- a/src/main/kotlin/org/team5499/monkeyLib/math/splines/CubicHermiteSpline.kt
+++ b/src/main/kotlin/org/team5419/fault/math/splines/CubicHermiteSpline.kt
@@ -1,11 +1,12 @@
-package org.team5499.monkeyLib.math.splines
+package org.team5419.fault.math.splines
-import org.team5499.monkeyLib.math.geometry.Vector2
-import org.team5499.monkeyLib.math.geometry.Rotation2d
-import org.team5499.monkeyLib.math.geometry.Pose2d
+import org.team5419.fault.math.geometry.Vector2d
+import org.team5419.fault.math.geometry.Rotation2d
+import org.team5419.fault.math.geometry.Pose2d
+import org.team5419.fault.math.units.meters
@SuppressWarnings("MagicNumber")
-class CubicHermiteSpline(p0: Vector2, h0: Rotation2d, p1: Vector2, h1: Rotation2d) : Spline() {
+class CubicHermiteSpline(p0: Vector2d, h0: Rotation2d, p1: Vector2d, h1: Rotation2d) : Spline() {
private val ax: Double
private val bx: Double
@@ -18,17 +19,17 @@ class CubicHermiteSpline(p0: Vector2, h0: Rotation2d, p1: Vector2, h1: Rotation2
private val dy: Double
init {
- val scale = 2.0 * p0.distanceTo(p1)
+ val scale = 2.0 * p0.distance(p1)
- val x0 = p0.x
- val x1 = p1.x
- val dx0 = h0.cosAngle * scale
- val dx1 = h1.cosAngle * scale
+ val x0 = p0.x.value
+ val x1 = p1.x.value
+ val dx0 = h0.cos * scale
+ val dx1 = h1.cos * scale
- val y0 = p0.y
- val y1 = p1.y
- val dy0 = h0.sinAngle * scale
- val dy1 = h1.sinAngle * scale
+ val y0 = p0.y.value
+ val y1 = p1.y.value
+ val dy0 = h0.sin * scale
+ val dy1 = h1.sin * scale
ax = dx0 + dx1 + 2.0 * x0 - 2.0 * x1
bx = -2.0 * dx0 - dx1 - 3.0 * x0 + 3.0 * x1
@@ -44,10 +45,10 @@ class CubicHermiteSpline(p0: Vector2, h0: Rotation2d, p1: Vector2, h1: Rotation2
constructor(p0: Pose2d, p1: Pose2d): this(p0.translation, p0.rotation, p1.translation, p1.rotation)
constructor(): this(Pose2d(), Pose2d())
- override fun getPoint(t: Double): Vector2 {
+ override fun getPoint(t: Double): Vector2d {
val x = t * t * t * ax + t * t * bx + t * cx + dx
val y = t * t * t * ay + t * t * by + t * cy + dy
- return Vector2(x, y)
+ return Vector2d(x.meters, y.meters)
}
override fun getHeading(t: Double): Rotation2d {
diff --git a/src/main/kotlin/org/team5419/fault/math/splines/FunctionalQuadraticSpline.kt b/src/main/kotlin/org/team5419/fault/math/splines/FunctionalQuadraticSpline.kt
new file mode 100644
index 0000000..e17dc68
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/math/splines/FunctionalQuadraticSpline.kt
@@ -0,0 +1,22 @@
+package org.team5419.fault.math.splines
+
+import org.team5419.fault.math.geometry.Vector2d
+import org.team5419.fault.math.units.Meter
+import org.team5419.fault.math.units.Mult
+import org.team5419.fault.math.units.SIUnit
+import org.team5419.fault.math.units.operations.times
+import org.team5419.fault.math.units.operations.div
+
+data class FunctionalQuadraticSpline(
+ private val p1: Vector2d,
+ private val p2: Vector2d,
+ private val p3: Vector2d
+) {
+
+ private val a: SIUnit>
+ get() = p3.x * (p2.y - p1.y) + p2.x * (p1.y - p3.y) + p1.x * (p3.y - p2.y)
+ private val b: SIUnit, Meter>>
+ get() = p3.x * p3.x * (p1.y - p2.y) + p2.x * p2.x * (p3.y - p1.y) + p1.x * p1.x * (p2.y - p3.y)
+
+ val vertexXCoordinate: SIUnit get() = -b / (2.0 * a)
+}
diff --git a/src/main/kotlin/org/team5499/monkeyLib/math/splines/QuinticHermiteSpline.kt b/src/main/kotlin/org/team5419/fault/math/splines/QuinticHermiteSpline.kt
similarity index 79%
rename from src/main/kotlin/org/team5499/monkeyLib/math/splines/QuinticHermiteSpline.kt
rename to src/main/kotlin/org/team5419/fault/math/splines/QuinticHermiteSpline.kt
index 83b96c7..b4a0728 100644
--- a/src/main/kotlin/org/team5499/monkeyLib/math/splines/QuinticHermiteSpline.kt
+++ b/src/main/kotlin/org/team5419/fault/math/splines/QuinticHermiteSpline.kt
@@ -1,26 +1,29 @@
-package org.team5499.monkeyLib.math.splines
+package org.team5419.fault.math.splines
-import org.team5499.monkeyLib.math.geometry.Vector2
-import org.team5499.monkeyLib.math.geometry.Rotation2d
-import org.team5499.monkeyLib.math.geometry.Pose2d
+import org.team5419.fault.math.geometry.Vector2d
+import org.team5419.fault.math.geometry.Rotation2d
+import org.team5419.fault.math.geometry.Pose2d
+import org.team5419.fault.math.units.meters
+import org.team5419.fault.math.units.operations.times
+import org.team5419.fault.math.units.operations.div
// yes external contructors are disgusting. but they work
@Suppress("FunctionNaming", "MagicNumber")
-fun QuinticHermiteSpline(p0: Vector2, p1: Vector2, h0: Rotation2d, h1: Rotation2d): QuinticHermiteSpline {
- val scale = 1.2 * p0.distanceTo(p1)
+fun QuinticHermiteSpline(p0: Vector2d, p1: Vector2d, h0: Rotation2d, h1: Rotation2d): QuinticHermiteSpline {
+ val scale = 1.2 * p0.distance(p1)
- val x0 = p0.x
- val x1 = p1.x
- val dx0 = h0.cosAngle * scale
- val dx1 = h1.cosAngle * scale
+ val x0 = p0.x.value
+ val x1 = p1.x.value
+ val dx0 = h0.cos * scale
+ val dx1 = h1.cos * scale
val ddx0 = 0.0
val ddx1 = 0.0
// y vars
- val y0 = p0.y
- val y1 = p1.y
- val dy0 = h0.sinAngle * scale
- val dy1 = h1.sinAngle * scale
+ val y0 = p0.y.value
+ val y1 = p1.y.value
+ val dy0 = h0.sin * scale
+ val dy1 = h1.sin * scale
val ddy0 = 0.0
val ddy1 = 0.0
@@ -111,14 +114,14 @@ class QuinticHermiteSpline(
}
val startPose: Pose2d
- get() = Pose2d(Vector2(x0, y0), Rotation2d(dx0, dy0, true))
+ get() = Pose2d(Vector2d(x0.meters, y0.meters), Rotation2d(dx0, dy0, true))
val endPose: Pose2d
- get() = Pose2d(Vector2(x1, y1), Rotation2d(dx1, dy1, true))
+ get() = Pose2d(Vector2d(x1.meters, y1.meters), Rotation2d(dx1, dy1, true))
- override fun getPoint(t: Double): Vector2 {
+ override fun getPoint(t: Double): Vector2d {
val x = ax * t * t * t * t * t + bx * t * t * t * t + cx * t * t * t + dx * t * t + ex * t + fx
val y = ay * t * t * t * t * t + by * t * t * t * t + cy * t * t * t + dy * t * t + ey * t + fy
- return Vector2(x, y)
+ return Vector2d(x.meters, y.meters)
}
private fun dx(t: Double) = 5 * ax * t * t * t * t + 4 * bx * t * t * t + 3 * cx * t * t + 2 * dx * t + ex
@@ -154,7 +157,7 @@ class QuinticHermiteSpline(
override fun getHeading(t: Double) = Rotation2d(dx(t), dy(t), true)
- public fun sumDCurvature2(): Double {
+ fun sumDCurvature2(): Double {
val dt = 1.0 / kSamples
var sum = 0.0
var t = 0.0
@@ -241,11 +244,11 @@ class QuinticHermiteSpline(
}
magnitude = Math.sqrt(magnitude)
- val p2 = Vector2(0.0, sumDCurvature2(splines))
+ val p2 = Vector2d(0.0.meters, sumDCurvature2(splines).meters)
for (i in 0..splines.size - 2) {
- if (splines.get(i).startPose.isColinear(splines.get(i + 1).startPose) &&
- splines.get(i).endPose.isColinear(splines.get(i + 1).endPose)) {
+ if (splines.get(i).startPose.isCollinear(splines.get(i + 1).startPose) &&
+ splines.get(i).endPose.isCollinear(splines.get(i + 1).endPose)) {
continue
}
controlPoints[i]!!.ddx *= (kStepSize / magnitude)
@@ -260,11 +263,11 @@ class QuinticHermiteSpline(
splines.get(i + 1).calcCoeffs()
}
- val p1 = Vector2(-kStepSize, sumDCurvature2(splines))
+ val p1 = Vector2d(-kStepSize.meters, sumDCurvature2(splines).meters)
for (i in 0..splines.size - 2) {
- if (splines.get(i).startPose.isColinear(splines.get(i + 1).startPose) &&
- splines.get(i).endPose.isColinear(splines.get(i + 1).endPose)) {
+ if (splines.get(i).startPose.isCollinear(splines.get(i + 1).startPose) &&
+ splines.get(i).endPose.isCollinear(splines.get(i + 1).endPose)) {
continue
}
splines.get(i).ddx1 += (2.0 * controlPoints[i]!!.ddx)
@@ -277,16 +280,19 @@ class QuinticHermiteSpline(
splines.get(i + 1).calcCoeffs()
}
- val p3 = Vector2(kStepSize, sumDCurvature2(splines))
+ val p3 = Vector2d(kStepSize.meters, sumDCurvature2(splines).meters)
+
+ val stepSize = FunctionalQuadraticSpline(
+ p1, p2, p3
+ ).vertexXCoordinate
- val stepSize = fitParabola(p1, p2, p3)
for (i in 0..splines.size - 2) {
- if (splines.get(i).startPose.isColinear(splines.get(i + 1).startPose) &&
- splines.get(i).endPose.isColinear(splines.get(i + 1).endPose)) {
+ if (splines.get(i).startPose.isCollinear(splines.get(i + 1).startPose) &&
+ splines.get(i).endPose.isCollinear(splines.get(i + 1).endPose)) {
continue
}
- controlPoints[i]!!.ddx *= (1.0 + stepSize / kStepSize)
- controlPoints[i]!!.ddy *= (1.0 + stepSize / kStepSize)
+ controlPoints[i]!!.ddx *= (1.0 + stepSize.value / kStepSize)
+ controlPoints[i]!!.ddy *= (1.0 + stepSize.value / kStepSize)
splines.get(i).ddx1 += controlPoints[i]!!.ddx
splines.get(i).ddy1 += controlPoints[i]!!.ddy
@@ -298,21 +304,5 @@ class QuinticHermiteSpline(
splines.get(i + 1).calcCoeffs()
}
}
-
- fun fitParabola(p1: Vector2, p2: Vector2, p3: Vector2): Double {
- // println("$p1 - $p2 - $p3")
- val t1 = p3.x * (p2.y - p1.y)
- val t2 = p2.x * (p1.y - p3.y)
- val t3 = p1.x * (p3.y - p2.y)
- val A = (t1 + t2 + t3)
-
- val t4 = (p3.x * p3.x) * (p1.y - p2.y)
- val t5 = (p2.x * p2.x) * (p3.y - p1.y)
- val t6 = (p1.x * p1.x) * (p2.y - p3.y)
- val B = (t4 + t5 + t6)
- // println("$t1, $t2, $t3, $t4, $t5, $t6")
- // println("a:$A, b:$B, result: ${-B / (2 * A)}")
- return -B / (2 * A)
- }
}
}
diff --git a/src/main/kotlin/org/team5499/monkeyLib/math/splines/Spline.kt b/src/main/kotlin/org/team5419/fault/math/splines/Spline.kt
similarity index 60%
rename from src/main/kotlin/org/team5499/monkeyLib/math/splines/Spline.kt
rename to src/main/kotlin/org/team5419/fault/math/splines/Spline.kt
index acb4c7f..49b4417 100644
--- a/src/main/kotlin/org/team5499/monkeyLib/math/splines/Spline.kt
+++ b/src/main/kotlin/org/team5419/fault/math/splines/Spline.kt
@@ -1,13 +1,13 @@
-package org.team5499.monkeyLib.math.splines
+package org.team5419.fault.math.splines
-import org.team5499.monkeyLib.math.geometry.Vector2
-import org.team5499.monkeyLib.math.geometry.Rotation2d
-import org.team5499.monkeyLib.math.geometry.Pose2d
-import org.team5499.monkeyLib.math.geometry.Pose2dWithCurvature
+import org.team5419.fault.math.geometry.Vector2d
+import org.team5419.fault.math.geometry.Rotation2d
+import org.team5419.fault.math.geometry.Pose2d
+import org.team5419.fault.math.geometry.Pose2dWithCurvature
abstract class Spline {
- abstract fun getPoint(t: Double): Vector2
+ abstract fun getPoint(t: Double): Vector2d
abstract fun getHeading(t: Double): Rotation2d
diff --git a/src/main/kotlin/org/team5499/monkeyLib/math/splines/SplineGenerator.kt b/src/main/kotlin/org/team5419/fault/math/splines/SplineGenerator.kt
similarity index 57%
rename from src/main/kotlin/org/team5499/monkeyLib/math/splines/SplineGenerator.kt
rename to src/main/kotlin/org/team5419/fault/math/splines/SplineGenerator.kt
index da6c4ac..6b76bff 100644
--- a/src/main/kotlin/org/team5499/monkeyLib/math/splines/SplineGenerator.kt
+++ b/src/main/kotlin/org/team5419/fault/math/splines/SplineGenerator.kt
@@ -1,24 +1,23 @@
-package org.team5499.monkeyLib.math.splines
+package org.team5419.fault.math.splines
-import org.team5499.monkeyLib.math.geometry.Vector2
-import org.team5499.monkeyLib.math.geometry.Pose2d
-import org.team5499.monkeyLib.math.geometry.Pose2dWithCurvature
+import org.team5419.fault.math.geometry.Pose2d
+import org.team5419.fault.math.geometry.Pose2dWithCurvature
+import org.team5419.fault.math.units.Meter
+import org.team5419.fault.math.units.SIUnit
+import org.team5419.fault.math.units.derived.Radian
@SuppressWarnings("LongParameterList")
object SplineGenerator {
- private const val kMaxDX = 2.0
- private const val kMaxDY = 0.05
- private const val kMaxDTheta = 0.1
private const val kMinSampleSize = 1
fun parameterizeSpline(
s: Spline,
- maxDx: Double,
- maxDy: Double,
- maxDTheta: Double,
- t0: Double,
- t1: Double
+ maxDx: SIUnit,
+ maxDy: SIUnit,
+ maxDTheta: SIUnit,
+ t0: Double = 0.0,
+ t1: Double = 1.0
): MutableList {
val rv: MutableList = mutableListOf()
rv.add(s.getPoseWithCurvature(0.0))
@@ -31,24 +30,20 @@ object SplineGenerator {
return rv
}
- fun parameterizeSpline(s: Spline): MutableList {
- return parameterizeSpline(s, kMaxDX, kMaxDY, kMaxDTheta, 0.0, 1.0)
- }
-
fun parameterizeSpline(
s: Spline,
- maxDx: Double,
- maxDy: Double,
- maxDTheta: Double
+ maxDx: SIUnit,
+ maxDy: SIUnit,
+ maxDTheta: SIUnit
): MutableList {
return parameterizeSpline(s, maxDx, maxDy, maxDTheta, 0.0, 1.0)
}
fun parameterizeSplines(
splines: List,
- maxDx: Double,
- maxDy: Double,
- maxDTheta: Double
+ maxDx: SIUnit,
+ maxDy: SIUnit,
+ maxDTheta: SIUnit
): MutableList {
val rv: MutableList = mutableListOf()
if (splines.isEmpty()) return rv
@@ -61,34 +56,26 @@ object SplineGenerator {
return rv
}
- fun parameterizeSplines(splines: List): MutableList {
- return parameterizeSplines(splines, kMaxDX, kMaxDY, kMaxDTheta)
- }
-
- fun parameterizeSplines(vararg s: Spline): MutableList {
- return parameterizeSplines(s.toMutableList())
- }
-
private fun getSegmentArc(
s: Spline,
rv: MutableList,
t0: Double,
t1: Double,
- maxDx: Double,
- maxDy: Double,
- maxDTheta: Double
+ maxDx: SIUnit,
+ maxDy: SIUnit,
+ maxDTheta: SIUnit
) {
val p0 = s.getPoint(t0)
val p1 = s.getPoint(t1)
val r0 = s.getHeading(t0)
val r1 = s.getHeading(t1)
- val transformation = Pose2d(Vector2(p0, p1).rotateBy(r0.inverse()), r1.rotateBy(r0.inverse()))
- val twist = Pose2d.log(transformation)
+ val transformation = Pose2d((p1 - p0) * -r0, r1 + -r0)
+// val transformation = Pose2d(Vector2(p0, p1).rotateBy(r0.inverse()), r1.rotateBy(r0.inverse()))
+ val twist = transformation.twist
if (twist.dy > maxDy || twist.dx > maxDx || twist.dTheta > maxDTheta) {
getSegmentArc(s, rv, t0, (t0 + t1) / 2, maxDx, maxDy, maxDTheta)
getSegmentArc(s, rv, (t0 + t1) / 2, t1, maxDx, maxDy, maxDTheta)
} else {
- // println(twist)
rv.add(s.getPoseWithCurvature(t1))
}
}
diff --git a/src/main/kotlin/org/team5419/fault/math/units/Ampere.kt b/src/main/kotlin/org/team5419/fault/math/units/Ampere.kt
new file mode 100644
index 0000000..a02fbe8
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/math/units/Ampere.kt
@@ -0,0 +1,11 @@
+package org.team5419.fault.math.units
+
+object Ampere : SIKey
+
+val Double.amps get() = SIUnit(this)
+
+val Number.amps get() = toDouble().amps
+
+fun SIUnit.inMilliamps() = value.div(kMilli)
+fun SIUnit.inMicroamps() = value.div(kMicro)
+fun SIUnit.inAmps() = value
diff --git a/src/main/kotlin/org/team5419/fault/math/units/Kilogram.kt b/src/main/kotlin/org/team5419/fault/math/units/Kilogram.kt
new file mode 100644
index 0000000..31a1207
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/math/units/Kilogram.kt
@@ -0,0 +1,23 @@
+@file:SuppressWarnings("TopLevelPropertyNaming")
+package org.team5419.fault.math.units
+
+object Kilogram : SIKey
+
+const val kCentiOffsetKilo = 1e-5
+const val kBaseOffsetKilo = 1e-3
+const val kMilliOffsetKilo = 1e-6
+const val kLbOffsetKilo = 0.453592
+
+val Double.lbs get() = pounds
+val Double.pounds get() = SIUnit(times(kLbOffsetKilo))
+
+val Number.lbs get() = toDouble().lbs
+val Number.pounds get() = toDouble().pounds
+
+fun SIUnit.inKilograms() = value
+fun SIUnit.inGrams() = value.div(kBaseOffsetKilo)
+fun SIUnit.inCentigrams() = value.div(kCentiOffsetKilo)
+fun SIUnit.inMilligrams() = value.div(kMilliOffsetKilo)
+
+fun SIUnit.inLbs() = inPounds()
+fun SIUnit.inPounds() = value.div(kLbOffsetKilo)
diff --git a/src/main/kotlin/org/team5419/fault/math/units/Meter.kt b/src/main/kotlin/org/team5419/fault/math/units/Meter.kt
new file mode 100644
index 0000000..51e17d6
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/math/units/Meter.kt
@@ -0,0 +1,34 @@
+@file:SuppressWarnings("TopLevelPropertyNaming", "TooManyFunctions")
+package org.team5419.fault.math.units
+
+object Meter : SIKey
+
+const val kInchToMeter = 0.0254
+const val kFeetToMeter = kInchToMeter * 12.0
+const val kThouToMeter = kInchToMeter * 0.001
+const val kYardToMeter = kFeetToMeter * 3.0
+
+val Double.meters get() = SIUnit(this)
+val Double.thou get() = SIUnit(times(kThouToMeter))
+val Double.inches get() = SIUnit(times(kInchToMeter))
+val Double.feet get() = SIUnit(times(kFeetToMeter))
+val Double.yards get() = SIUnit(times(kYardToMeter))
+
+val Number.meters get() = toDouble().meters
+val Number.thou get() = toDouble().thou
+val Number.inches get() = toDouble().inches
+val Number.feet get() = toDouble().feet
+
+fun SIUnit.inThou() = value.div(kThouToMeter)
+fun SIUnit.inInches() = value.div(kInchToMeter)
+fun SIUnit.inFeet() = value.div(kFeetToMeter)
+fun SIUnit.inYards() = value.div(kYardToMeter)
+
+fun SIUnit.inKilometers() = value.div(kKilo)
+fun SIUnit.inHectometers() = value.div(kHecto)
+fun SIUnit.inDecameters() = value.div(kDeca)
+fun SIUnit.inMeters() = value
+fun SIUnit.inDecimeters() = value.div(kDeci)
+fun SIUnit.inCentimeters() = value.div(kCenti)
+fun SIUnit.inMillimeters() = value.div(kMilli)
+fun SIUnit.inMicrometers() = value.div(kMicro)
diff --git a/src/main/kotlin/org/team5419/fault/math/units/SIUnit.kt b/src/main/kotlin/org/team5419/fault/math/units/SIUnit.kt
new file mode 100644
index 0000000..e5b7c09
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/math/units/SIUnit.kt
@@ -0,0 +1,75 @@
+@file:SuppressWarnings("TopLevelPropertyNaming", "TooManyFunctions")
+package org.team5419.fault.math.units
+
+import org.team5419.fault.math.epsilonEquals
+import org.team5419.fault.math.lerp
+import org.team5419.fault.math.units.derived.Ohm
+import org.team5419.fault.math.units.derived.Volt
+import org.team5419.fault.math.units.derived.Watt
+import kotlin.math.absoluteValue
+
+// SI Constants
+const val kKilo = 1e3
+const val kHecto = 1e2
+const val kDeca = 1e1
+const val kDeci = 1e-1
+const val kCenti = 1e-2
+const val kMilli = 1e-3
+const val kMicro = 1e-6
+const val kNano = 1e-9
+
+// SI Unit
+inline class SIUnit(val value: Double) : Comparable> {
+
+ val absoluteValue get() = SIUnit(value.absoluteValue)
+
+ operator fun unaryMinus() = SIUnit(-value)
+ operator fun plus(other: SIUnit) = SIUnit(value.plus(other.value))
+ operator fun minus(other: SIUnit) = SIUnit(value.minus(other.value))
+ operator fun times(other: Double) = SIUnit(value.times(other))
+ operator fun div(other: Double) = SIUnit(value.div(other))
+ operator fun times(other: Number) = times(other.toDouble())
+ operator fun div(other: Number) = div(other.toDouble())
+
+ override operator fun compareTo(other: SIUnit) = value.compareTo(other.value)
+ operator fun compareTo(other: Double) = value.compareTo(other)
+ operator fun compareTo(other: Number) = compareTo(other.toDouble())
+
+ fun lerp(endValue: SIUnit, t: Double) = SIUnit(value.lerp(endValue.value, t))
+ infix fun epsilonEquals(other: SIUnit) = value.epsilonEquals(other.value)
+}
+
+val Double.kilo get() = SIUnitBuilder(times(kKilo))
+val Double.deca get() = SIUnitBuilder(times(kDeca))
+val Double.base get() = SIUnitBuilder(this)
+val Double.deci get() = SIUnitBuilder(times(kDeci))
+val Double.centi get() = SIUnitBuilder(times(kCenti))
+val Double.milli get() = SIUnitBuilder(times(kMilli))
+val Double.micro get() = SIUnitBuilder(times(kMicro))
+
+val Number.kilo get() = toDouble().kilo
+val Number.deca get() = toDouble().deca
+val Number.base get() = toDouble().base
+val Number.deci get() = toDouble().deci
+val Number.centi get() = toDouble().centi
+val Number.milli get() = toDouble().milli
+val Number.micro get() = toDouble().micro
+
+inline class SIUnitBuilder(private val value: Double) {
+ val seconds get() = SIUnit(value)
+ val meters get() = SIUnit(value)
+ val grams get() = SIUnit(value.times(kBaseOffsetKilo))
+ val amps get() = SIUnit(value)
+ val ohms get() = SIUnit(value)
+ val volts get() = SIUnit(value)
+ val watts get() = SIUnit(value)
+}
+
+interface SIKey
+
+class Mult : SIKey
+class Frac : SIKey
+
+object Unitless : SIKey
+
+val SIUnit.unitlessValue get() = value
diff --git a/src/main/kotlin/org/team5419/fault/math/units/Second.kt b/src/main/kotlin/org/team5419/fault/math/units/Second.kt
new file mode 100644
index 0000000..1c60eb5
--- /dev/null
+++ b/src/main/kotlin/org/team5419/fault/math/units/Second.kt
@@ -0,0 +1,32 @@
+@file:SuppressWarnings("TooManyFunctions")
+package org.team5419.fault.math.units
+
+object Second : SIKey
+
+@Suppress("TopLevelPropertyNaming")
+const val kMinuteToSecond = 60.0
+@Suppress("TopLevelPropertyNaming")
+const val kHourToSecond = kMinuteToSecond * 60.0
+
+val Double.seconds get() = SIUnit(this)
+val Double.minutes get() = SIUnit(times(kMinuteToSecond))
+val Double.hours get() = SIUnit(times(kHourToSecond))
+val Double.milliseconds get() = SIUnit(div(1000.0))
+
+val Number.seconds get() = toDouble().seconds
+val Number.minutes get() = toDouble().minutes
+val Number.hours get() = toDouble().hours
+val Number.milliseconds get() = toDouble().milliseconds
+
+fun SIUnit.inMinutes() = value.div(kMinuteToSecond)
+fun SIUnit.inHours() = value.div(kHourToSecond)
+
+fun SIUnit.inKiloseconds() = value.div(kKilo)
+fun SIUnit.inHectoseconds() = value.div(kHecto)
+fun SIUnit.inDecaseconds() = value.div(kDeca)
+fun SIUnit.inSeconds() = value
+fun SIUnit