@@ -3,11 +3,13 @@ package org.strykeforce.healthcheck.internal
33import com.ctre.phoenix.motorcontrol.can.BaseTalon
44import com.ctre.phoenix6.controls.Follower
55import com.ctre.phoenix6.hardware.TalonFX
6+ import com.ctre.phoenix6.hardware.TalonFXS
67import edu.wpi.first.wpilibj2.command.Subsystem
78import edu.wpi.first.wpilibj2.command.SubsystemBase
89import mu.KotlinLogging
910import okhttp3.internal.toImmutableList
1011import org.strykeforce.healthcheck.*
12+ import org.strykeforce.swerve.FXSwerveModule
1113import org.strykeforce.swerve.SwerveDrive
1214import org.strykeforce.swerve.V6TalonSwerveModule
1315import java.lang.reflect.Field
@@ -178,7 +180,7 @@ class IOHealthCheckBuilder(private val io: Checkable) {
178180 // queue the swerveDrive field for removal in fieldsToRemove
179181 healthCheckFields.forEach {
180182 if (it.type == SwerveDrive ::class .java) {
181- healthChecks.addAll(SwerveDriveHealthCheckIOBuilder (io, it).build())
183+ healthChecks.addAll(FXSwerveDriveHealthCheckIOBuilder (io, it).build())
182184 fieldsToRemove.add(it)
183185 }
184186 }
@@ -188,7 +190,10 @@ class IOHealthCheckBuilder(private val io: Checkable) {
188190 healthCheckFields.forEach {
189191 if (it.type == TalonFX ::class .java) {
190192 healthChecks.add(P6TalonHealthCheckIOBuilder (io, it).build())
191- } else {
193+ } else if (it.type == TalonFXS ::class .java) {
194+ healthChecks.add(FXSTalonHealthCheckIOBuilder (io, it).build())
195+ }
196+ else {
192197 healthChecks.add(TalonHealthCheckIOBuilder (io, it).build())
193198 }
194199 }
@@ -199,6 +204,7 @@ class IOHealthCheckBuilder(private val io: Checkable) {
199204 // remove the follower check since run during leader
200205 val followerHealthChecks = healthChecks.mapNotNull { it as ? TalonFollowerHealthCheck }
201206 val p6FollowerHealthChecks = healthChecks.mapNotNull { it as ? P6TalonFollowerHealthCheck }
207+ val fxsFollowerHealthChecks = healthChecks.mapNotNull { it as ? FXSTalonFollowerHealthCheck }
202208 for (fhc in followerHealthChecks) {
203209 val lhc = healthChecks.mapNotNull { it as ? TalonHealthCheck }.find { it.talon.deviceID == fhc.leaderId }
204210 if (lhc == null ) {
@@ -222,6 +228,17 @@ class IOHealthCheckBuilder(private val io: Checkable) {
222228 }
223229 healthChecks.removeAll(p6FollowerHealthChecks)
224230
231+ for (fhc in fxsFollowerHealthChecks) {
232+ val lhc = healthChecks.mapNotNull { it as ? FXSTalonHealthCheck }.find { it.talonFxs.deviceID == fhc.leaderId }
233+ if (lhc == null ) {
234+ logger.error { " $io : no leader (id = ${fhc.leaderId} ) found for follower (id = ${fhc.talonFxs.deviceID} )" }
235+ continue
236+ }
237+ // leader is timed or position check, add follower to each child case
238+ lhc.healthChecks.map { it as FXSTalonHealthCheckCase }.forEach{it.addFollowerTalon(fhc.talonFxs)}
239+ }
240+ healthChecks.removeAll(fxsFollowerHealthChecks)
241+
225242 // gget all methods from subsystem annotated with @AfterHealthCHeck
226243 val afterHealthCheckMethods = io.afterHealthCheckAnnotatedMethods()
227244
@@ -399,6 +416,57 @@ class SwerveDriveHealthCheckIOBuilder(private val io: Checkable, private val fi
399416 }
400417}
401418
419+ class FXSwerveDriveHealthCheckIOBuilder (private val io : Checkable , private val field : Field ) {
420+ private val swerveDrive = field.get(io) as ? SwerveDrive ? : throw IllegalArgumentException (" $io : field '${field.name} ' is not a SwerveDrive" )
421+
422+ fun build (): List <HealthCheck > {
423+ val talonHealthChecks = mutableListOf<HealthCheck >()
424+
425+ var azimuthLeader: TalonFXS ? = null
426+ var driveLeader: TalonFX ? = null
427+ var driveLeaderId: Int ? = null
428+ var azimuthLeaderId: Int ? = null
429+ val azimuthFollowers = mutableListOf<TalonFXS >()
430+ val driveFollowers = mutableListOf<TalonFX >()
431+
432+ // extract talons from swerve drive modules and
433+ // create TalonTimedHealthChecks for leader azimuth and drive talons
434+ // create TalonFollowerHealthCheck for remainder
435+ // follow associated leaders
436+ swerveDrive.swerveModules.forEach {
437+ val module =
438+ it as ? FXSwerveModule ? : throw IllegalArgumentException (" $io : $it is not FXTalonSwerveModule" )
439+ val azimuth = module.azimuthTalon
440+ val drive = module.driveTalon
441+
442+ if (azimuth.deviceID == AZIMUTH_LEADER_ID ) {
443+ talonHealthChecks.add(FXSTalonTimedHealthCheckBuilder (azimuth).build())
444+ azimuthLeader = azimuth
445+ } else {
446+ talonHealthChecks.add(FXSTalonFollowerHealthCheck (azimuth, AZIMUTH_LEADER_ID ))
447+ azimuthFollowers.add(azimuth)
448+ }
449+
450+ if (drive.deviceID == DRIVE_LEADER_ID ) {
451+ talonHealthChecks.add(P6TalonTimedHealthCheckBuilder (drive).build())
452+ driveLeader = drive
453+ driveLeaderId = drive.deviceID
454+ } else {
455+ talonHealthChecks.add(P6TalonFollowerHealthCheck (drive, DRIVE_LEADER_ID ))
456+ driveFollowers.add(drive)
457+ }
458+ }
459+
460+ requireNotNull(azimuthLeader) { " $io : swerve azimuth talon with id $AZIMUTH_LEADER_ID not found" }
461+ requireNotNull(driveLeader) { " $io : swerve drive talon with id $DRIVE_LEADER_ID not found" }
462+
463+ azimuthFollowers.forEach { it.setControl(Follower (azimuthLeaderId ? : AZIMUTH_LEADER_ID , false ))}
464+ driveFollowers.forEach { it.setControl(Follower (driveLeaderId ? : DRIVE_LEADER_ID ,false ) )}
465+
466+ return talonHealthChecks
467+ }
468+ }
469+
402470class TalonHealthCheckBuilder (private val subsystem : Subsystem , private val field : Field ) {
403471
404472 val talon = field.get(subsystem) as ? BaseTalon
@@ -529,6 +597,38 @@ class P6TalonHealthCheckIOBuilder(private val io: Checkable, private val field:
529597 }
530598}
531599
600+ class FXSTalonHealthCheckIOBuilder (private val io : Checkable , private val field : Field ) {
601+ val talonFxs = field.get(io) as ? TalonFXS ? : throw java.lang.IllegalArgumentException (" $io : field '${field.name} ' is not subclass of TalonFXS" )
602+
603+ fun build (): FXSTalonHealthCheck {
604+ val timedAnnotation = field.getAnnotation(Timed ::class .java)
605+ val positionAnnotation = field.getAnnotation(Position ::class .java)
606+ val followAnnotation = field.getAnnotation(Follow ::class .java)
607+
608+ if (arrayListOf (timedAnnotation, positionAnnotation, followAnnotation).filterNotNull().count() > 1 )
609+ throw IllegalArgumentException (" $io : only one of @Timed, @Position, or @Follow may be specified." )
610+
611+ if (timedAnnotation != null ) {
612+ val builder = FXSTalonTimedHealthCheckBuilder (talonFxs)
613+ builder.percentOutput = timedAnnotation.percentOutput
614+ builder.duration = timedAnnotation.duration
615+ return builder.build()
616+ }
617+
618+ if (positionAnnotation != null ) {
619+ val builder = FXSTalonPositionHealthCheckBuilder (talonFxs)
620+ builder.percentOutput = positionAnnotation.percentOutput
621+ builder.encoderChange = positionAnnotation.encoderChange
622+ return builder.build()
623+ }
624+
625+ if (followAnnotation != null ) return FXSTalonFollowerHealthCheck (talonFxs, followAnnotation.leader)
626+
627+ // Default to timed if not specified
628+ return FXSTalonTimedHealthCheckBuilder (talonFxs).build()
629+ }
630+ }
631+
532632class TalonTimedHealthCheckBuilder (val talon : BaseTalon ) {
533633
534634 var percentOutput = doubleArrayOf(0.2 , 1.0 , - 0.2 , - 1.0 )
@@ -568,6 +668,25 @@ class P6TalonTimedHealthCheckBuilder(val talonFx: TalonFX) {
568668 }
569669}
570670
671+ class FXSTalonTimedHealthCheckBuilder (val talonFxs : TalonFXS ) {
672+ var percentOutput = doubleArrayOf(0.2 , 1.0 , - 0.2 , - 1.0 )
673+ var duration = 2.0
674+ var limits: DoubleArray? = null
675+
676+ fun build (): FXSTalonHealthCheck {
677+ val cases = percentOutput.let {
678+ var previousCase: FXSTalonTimedHealthCheckCase ? = null
679+ it.map { pctOut ->
680+ val case = FXSTalonTimedHealthCheckCase (previousCase, talonFxs, pctOut, (duration * 1e6).toLong())
681+ previousCase = case
682+ case
683+ }
684+ }
685+
686+ return FXSTalonTimedHealthCheck (talonFxs, cases, percentOutput)
687+ }
688+ }
689+
571690
572691class TalonPositionHealthCheckBuilder (val talon : BaseTalon ) {
573692 var percentOutput = doubleArrayOf(0.25 , - 0.25 )
@@ -610,3 +729,24 @@ class P6TalonPositionHealthCheckBuilder(var talonFx: TalonFX) {
610729 return P6TalonPositionHealthCheck (talonFx, cases, percentOutput)
611730 }
612731}
732+
733+ class FXSTalonPositionHealthCheckBuilder (var talonFxs : TalonFXS ) {
734+ var percentOutput = doubleArrayOf(0.25 , - 0.25 )
735+ var encoderChange = 0.0
736+ var limits: DoubleArray? = null
737+
738+ fun build (): FXSTalonHealthCheck {
739+ if (encoderChange == 0.0 ) logger.warn { " Talon ${talonFxs.deviceID} : position health check encoderChange is zero" }
740+
741+ val cases = percentOutput.let {
742+ var previousCase: FXSTalonPositionHealthCheckCase ? = null
743+ it.map { pctOut ->
744+ val case = FXSTalonPositionHealthCheckCase (previousCase, talonFxs, pctOut, encoderChange)
745+ previousCase = case
746+ case
747+ }
748+ }
749+
750+ return FXSTalonPositionHealthCheck (talonFxs, cases, percentOutput)
751+ }
752+ }
0 commit comments