diff --git a/.wpilib/wpilib_preferences.json b/.wpilib/wpilib_preferences.json
index acab627..6b13a10 100644
--- a/.wpilib/wpilib_preferences.json
+++ b/.wpilib/wpilib_preferences.json
@@ -1,6 +1,6 @@
{
- "enableCppIntellisense": false,
- "currentLanguage": "java",
- "projectYear": "2025beta",
- "teamNumber": 4829
-}
\ No newline at end of file
+ "enableCppIntellisense": false,
+ "currentLanguage": "java",
+ "projectYear": "2025beta",
+ "teamNumber": 4829
+}
diff --git a/build.gradle b/build.gradle
index 6762242..1abcd7b 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,43 +1,11 @@
plugins {
id "java"
- id "edu.wpi.first.GradleRIO" version "2025.1.1-beta-1"
+ id "edu.wpi.first.GradleRIO" version "2025.1.1-beta-3"
id "com.peterabeles.gversion" version "1.10"
id "com.diffplug.spotless" version "6.25.0"
+ id "pmd"
id "io.freefair.lombok" version "8.4"
id "com.google.protobuf" version "0.9.4"
- id "pmd"
-}
-
-def grpcVersion = '1.61.0' // CURRENT_GRPC_VERSION
-def protobufVersion = '3.25.1'
-def protocVersion = protobufVersion
-
-protobuf {
- protoc {
- artifact = "com.google.protobuf:protoc:${protobufVersion}"
- }
- plugins {
- grpc {
- artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}"
- }
- }
- generateProtoTasks {
- all()*.plugins {
- grpc {}
- }
- }
-}
-
-sourceSets {
- main {
- java {
- srcDirs 'build/generated/source/proto/main/grpc'
- srcDirs 'build/generated/source/proto/main/java'
- }
- proto {
- srcDirs "trajectory_native/proto"
- }
- }
}
java {
@@ -64,14 +32,25 @@ deploy {
// getTargetTypeClass is a shortcut to get the class type using a string
frcJava(getArtifactTypeClass('FRCJavaArtifact')) {
- gcType = edu.wpi.first.gradlerio.deploy.roborio.GarbageCollectorType.G1_Base
+ // https://www.chiefdelphi.com/t/2024-wpilib-feedback/464322/141
+ final MAX_JAVA_HEAP_SIZE_MB = 100;
+ jvmArgs.add("-XX:+UnlockExperimentalVMOptions")
+
+ // Set the minimum heap size to the maximum heap size to avoid resizing
+ jvmArgs.add("-Xmx" + MAX_JAVA_HEAP_SIZE_MB + "M")
+ jvmArgs.add("-Xms" + MAX_JAVA_HEAP_SIZE_MB + "M")
+ jvmArgs.add("-XX:GCTimeRatio=5")
+ jvmArgs.add("-XX:+UseSerialGC")
+ jvmArgs.add("-XX:MaxGCPauseMillis=50")
}
// Static files artifact
frcStaticFileDeploy(getArtifactTypeClass('FileTreeArtifact')) {
- predeploy << { execute 'rm -rf /home/lvuser/deploy' }
files = project.fileTree('src/main/deploy')
directory = '/home/lvuser/deploy'
+ // Change to true to delete files on roboRIO that no
+ // longer exist in deploy directory on roboRIO
+ deleteOldFiles = false
}
}
}
@@ -98,62 +77,15 @@ repositories {
mavenLocal()
}
-configurations.all {
- exclude group: "edu.wpi.first.wpilibj"
-}
-
-task(checkAkitInstall, dependsOn: "classes", type: JavaExec) {
- mainClass = "org.littletonrobotics.junction.CheckInstall"
+task(replayWatch, type: JavaExec) {
+ mainClass = "org.littletonrobotics.junction.ReplayWatch"
classpath = sourceSets.main.runtimeClasspath
}
-compileJava.finalizedBy checkAkitInstall
-
-
-//Check robot type when deploying
-task(checkRobot, dependsOn: "classes", type: JavaExec) {
- main = "frc.robot.Constants"
- classpath = sourceSets.main.runtimeClasspath
-}
-
-deployroborio.dependsOn(checkRobot)
-
-//Create commit with working changes on event branches
-task(eventDeploy) {
- doLast {
- if (project.gradle.startParameter.taskNames.any({ it.toLowerCase().contains("deploy") })) {
-
- def branchPrefix = "event"
- def branch = 'git branch --show-current'.execute().text.trim()
- def commitMessage = "Update at '${new Date().toString()}'"
-
- if (branch.startsWith(branchPrefix)) {
- exec {
- workingDir(projectDir)
- executable 'git'
- args 'add', '-A'
- }
- exec {
- workingDir(projectDir)
- executable 'git'
- args 'commit', '-m', commitMessage
- ignoreExitValue = true
- }
-
- println "Committed to branch: '$branch'"
- println "Commit message: '$commitMessage'"
- } else {
- println "Not on an event branch, skipping commit"
- }
- } else {
- println "Not running deploy task, skipping commit"
- }
- }
-}
-createVersionFile.dependsOn(eventDeploy)
// Defining my dependencies. In this case, WPILib (+ friends), and vendor libraries.
// Also defines JUnit 4.
dependencies {
+ annotationProcessor wpi.java.deps.wpilibAnnotations()
implementation wpi.java.deps.wpilib()
implementation wpi.java.vendor.java()
@@ -171,25 +103,13 @@ dependencies {
nativeRelease wpi.java.vendor.jniRelease(wpi.platforms.desktop)
simulationRelease wpi.sim.enableRelease()
- testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1'
- testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.2'
+ implementation "gov.nist.math:jama:1.0.3"
-
- // Mockito for mocking objects in unit tests
- testImplementation 'org.mockito:mockito-core:4.8.1'
- testImplementation 'org.mockito:mockito-junit-jupiter:4.8.1'
-
- implementation 'gov.nist.math:jama:1.0.3'
-
- implementation "io.grpc:grpc-protobuf:${grpcVersion}"
- implementation "io.grpc:grpc-services:${grpcVersion}"
- implementation "io.grpc:grpc-stub:${grpcVersion}"
- implementation "javax.annotation:javax.annotation-api:1.3.2"
- runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}"
- implementation "com.google.guava:guava:31.1-jre"
+ testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1'
+ testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
def akitJson = new groovy.json.JsonSlurper().parseText(new File(projectDir.getAbsolutePath() + "/vendordeps/AdvantageKit.json").text)
- annotationProcessor "org.littletonrobotics.akit.junction:junction-autolog:$akitJson.version"
+ annotationProcessor "org.littletonrobotics.akit:akit-autolog:$akitJson.version"
}
test {
@@ -198,7 +118,12 @@ test {
}
// Simulation configuration (e.g. environment variables).
-wpi.sim.addGui().defaultEnabled = true
+//
+// The sim GUI is *disabled* by default to support running
+// AdvantageKit log replay from the command line. Set the
+// value to "true" to enable the sim GUI by default (this
+// is the standard WPILib behavior).
+wpi.sim.addGui().defaultEnabled = false
wpi.sim.addDriverstation()
// Setting up my Jar File. In this case, adding all libraries into the main jar ('fat jar')
@@ -226,22 +151,58 @@ tasks.withType(JavaCompile) {
}
// Create version file
-project.compileJava.dependsOn(createVersionFile)
-gversion {
- srcDir = "src/main/java/"
- classPackage = "frc.robot"
- className = "BuildConstants"
- dateFormat = "yyyy-MM-dd HH:mm:ss z"
- timeZone = "America/New_York"
- indent = " "
-}
+// project.compileJava.dependsOn(createVersionFile)
+// gversion {
+// srcDir = "src/main/java/"
+// classPackage = "frc.robot"
+// className = "BuildConstants"
+// dateFormat = "yyyy-MM-dd HH:mm:ss z"
+// timeZone = "America/New_York"
+// indent = " "
+// }
+
+// TODO: set this shit up
+// Create commit with working changes on event branches
+// task(eventDeploy) {
+// doLast {
+// if (project.gradle.startParameter.taskNames.any({ it.toLowerCase().contains("deploy") })) {
+// def branchPrefix = "event"
+// def branch = 'git branch --show-current'.execute().text.trim()
+// def commitMessage = "Update at '${new Date().toString()}'"
+
+// if (branch.startsWith(branchPrefix)) {
+// exec {
+// workingDir(projectDir)
+// executable 'git'
+// args 'add', '-A'
+// }
+// exec {
+// workingDir(projectDir)
+// executable 'git'
+// args 'commit', '-m', commitMessage
+// ignoreExitValue = true
+// }
+
+// println "Committed to branch: '$branch'"
+// println "Commit message: '$commitMessage'"
+// } else {
+// println "Not on an event branch, skipping commit"
+// }
+// } else {
+// println "Not running deploy task, skipping commit"
+// }
+// }
+// }
+// createVersionFile.dependsOn(eventDeploy)
+
// Spotless formatting
+project.compileJava.dependsOn(spotlessApply)
spotless {
enforceCheck = false
java {
- target fileTree('.') {
- include '**/*.java'
- exclude '**/build/**', '**/build-*/**'
+ target fileTree(".") {
+ include "**/*.java"
+ exclude "**/build/**", "**/build-*/**"
}
toggleOffOn()
googleJavaFormat()
@@ -250,36 +211,32 @@ spotless {
endWithNewline()
}
groovyGradle {
- target fileTree('.') {
- include '**/*.gradle'
- exclude '**/build/**', '**/build-*/**'
+ target fileTree(".") {
+ include "**/*.gradle"
+ exclude "**/build/**", "**/build-*/**"
}
greclipse()
indentWithSpaces(4)
trimTrailingWhitespace()
endWithNewline()
}
- format 'xml', {
- target fileTree('.') {
- include '**/*.xml'
- exclude '**/build/**', '**/build-*/**'
+ json {
+ target fileTree(".") {
+ include "**/*.json"
+ exclude "**/build/**", "**/build-*/**"
}
- eclipseWtp('xml')
- trimTrailingWhitespace()
- indentWithSpaces(2)
- endWithNewline()
+ gson().indentWithSpaces(2)
}
- format 'misc', {
- target fileTree('.') {
- include '**/*.md', '**/.gitignore'
- exclude '**/build/**', '**/build-*/**'
+ format "misc", {
+ target fileTree(".") {
+ include "**/*.md", "**/.gitignore"
+ exclude "**/build/**", "**/build-*/**"
}
trimTrailingWhitespace()
indentWithSpaces(2)
endWithNewline()
}
}
-
// PMD configuration
pmd {
ignoreFailures = true
diff --git a/simgui-ds.json b/simgui-ds.json
index 73cc713..69b1a3c 100644
--- a/simgui-ds.json
+++ b/simgui-ds.json
@@ -88,5 +88,10 @@
"buttonCount": 0,
"povCount": 0
}
+ ],
+ "robotJoysticks": [
+ {
+ "guid": "Keyboard0"
+ }
]
}
diff --git a/simgui.json b/simgui.json
index 5f9d275..896ae55 100644
--- a/simgui.json
+++ b/simgui.json
@@ -1,7 +1,10 @@
{
"NTProvider": {
"types": {
- "/FMSInfo": "FMSInfo"
+ "/AdvantageKit/RealOutputs/Alerts": "Alerts",
+ "/FMSInfo": "FMSInfo",
+ "/SmartDashboard/Alerts": "Alerts",
+ "/SmartDashboard/VisionSystemSim-main/Sim Field": "Field2d"
}
},
"NetworkTables Info": {
diff --git a/src/main/java/frc/robot/Constants.java b/src/main/java/frc/robot/Constants.java
index eefde9d..cf81f13 100644
--- a/src/main/java/frc/robot/Constants.java
+++ b/src/main/java/frc/robot/Constants.java
@@ -1,22 +1,28 @@
package frc.robot;
+import edu.wpi.first.math.system.plant.DCMotor;
import edu.wpi.first.math.util.Units;
+import frc.robot.subsystems.swerve.SwerveConstants.*;
+import frc.robot.subsystems.swerve.SwerveConstants.DriveConstants;
+import frc.robot.subsystems.swerve.SwerveConstants.ModuleConstants;
public final class Constants {
- public class LogPaths {
+ public static final class LogPaths {
public static final String SYSTEM_PERFORMANCE_PATH = "SystemPerformance/";
public static final String PHYSICS_SIMULATION_PATH = "MaplePhysicsSimulation/";
public static final String APRIL_TAGS_VISION_PATH = "Vision/AprilTags/";
}
- public static final Mode currentMode = Mode.REAL;
+ public static final Mode CURRENT_MODE = Mode.SIM;
public static enum Mode {
/** Running on a real robot. */
REAL,
+
/** Running a physics simulator. */
SIM,
+
/** Replaying from a log file. */
REPLAY
}
@@ -41,6 +47,85 @@ public static final class HardwareConstants {
public static final double MIN_FALCON_DEADBAND = 0.001;
}
+ /**
+ * stores the constants and PID configs for chassis because we want an all-real simulation for the
+ * chassis, the numbers are required to be considerably precise
+ */
+ public class SimulationConstants {
+ /**
+ * numbers that needs to be changed to fit each robot TODO: change these numbers to match your
+ * robot
+ */
+ public static final double WHEEL_COEFFICIENT_OF_FRICTION = 0.95,
+ ROBOT_MASS_KG = 60; // with bumpers
+
+ /** TODO: change motor type to match your robot */
+ public static final DCMotor DRIVE_MOTOR = DCMotor.getKrakenX60(1),
+ STEER_MOTOR = DCMotor.getFalcon500(1);
+
+ public static final double WHEEL_RADIUS_METERS = ModuleConstants.WHEEL_DIAMETER_METERS / 2.0,
+ DRIVE_GEAR_RATIO = ModuleConstants.DRIVE_GEAR_RATIO,
+ STEER_GEAR_RATIO = 11.0,
+ TIME_ROBOT_STOP_ROTATING_SECONDS = 0.06,
+ STEER_FRICTION_VOLTAGE = 0.12,
+ DRIVE_FRICTION_VOLTAGE = ModuleConstants.DRIVE_S,
+ DRIVE_INERTIA = 0.01,
+ STEER_INERTIA = 0.01;
+
+ /* adjust current limit */
+ public static final double DRIVE_CURRENT_LIMIT = ModuleConstants.DRIVE_STATOR_LIMIT;
+ // public static final double STEER_CURRENT_LIMIT = ModuleConstants.ST;
+
+ /* equations that calculates some constants for the simulator (don't modify) */
+ private static final double GRAVITY_CONSTANT = 9.81;
+ public static final double DRIVE_BASE_RADIUS = DriveConstants.MODULE_TRANSLATIONS[0].getNorm(),
+ /* friction_force = normal_force * coefficient_of_friction */
+ MAX_FRICTION_ACCELERATION = GRAVITY_CONSTANT * WHEEL_COEFFICIENT_OF_FRICTION,
+ MAX_FRICTION_FORCE_PER_MODULE =
+ MAX_FRICTION_ACCELERATION * ROBOT_MASS_KG / DriveConstants.MODULE_TRANSLATIONS.length,
+ /* force = torque / distance */
+ MAX_PROPELLING_FORCE_NEWTONS =
+ DRIVE_MOTOR.getTorque(DRIVE_CURRENT_LIMIT) * DRIVE_GEAR_RATIO / WHEEL_RADIUS_METERS,
+ /* floor_speed = wheel_angular_velocity * wheel_radius */
+ CHASSIS_MAX_VELOCITY =
+ DRIVE_MOTOR.freeSpeedRadPerSec
+ / DRIVE_GEAR_RATIO
+ * WHEEL_RADIUS_METERS, // calculate using choreo
+ CHASSIS_MAX_ACCELERATION_MPS_SQ =
+ Math.min(
+ MAX_FRICTION_ACCELERATION, // cannot exceed max friction
+ MAX_PROPELLING_FORCE_NEWTONS / ROBOT_MASS_KG),
+ CHASSIS_MAX_ANGULAR_VELOCITY_RAD_PER_SEC = CHASSIS_MAX_VELOCITY / DRIVE_BASE_RADIUS,
+ CHASSIS_MAX_ANGULAR_ACCELERATION_RAD_PER_SEC_SQ =
+ CHASSIS_MAX_ACCELERATION_MPS_SQ / DRIVE_BASE_RADIUS,
+ CHASSIS_FRICTIONAL_ANGULAR_ACCELERATION =
+ CHASSIS_MAX_ANGULAR_VELOCITY_RAD_PER_SEC / TIME_ROBOT_STOP_ROTATING_SECONDS;
+
+ /* for collision detection in simulation */
+ public static final double BUMPER_WIDTH_METERS = Units.inchesToMeters(34.5),
+ BUMPER_LENGTH_METERS = Units.inchesToMeters(36),
+ /* https://en.wikipedia.org/wiki/Friction#Coefficient_of_friction */
+ BUMPER_COEFFICIENT_OF_FRICTION = 0.75,
+ /* https://simple.wikipedia.org/wiki/Coefficient_of_restitution */
+ BUMPER_COEFFICIENT_OF_RESTITUTION = 0.08;
+
+ /* Gyro Sim */
+ public static final double GYRO_ANGULAR_ACCELERATION_THRESHOLD_SKIDDING_RAD_PER_SEC_SQ = 100;
+ public static final double SKIDDING_AMOUNT_AT_THRESHOLD_RAD = Math.toRadians(1.2);
+ /*
+ * https://store.ctr-electronics.com/pigeon-2/
+ * for a well-installed one with vibration reduction, only 0.4 degree
+ * but most teams just install it directly on the rigid chassis frame (including my team :D)
+ * so at least 1.2 degrees of drifting in 1 minutes for an average angular velocity of 60 degrees/second
+ * which is the average velocity during normal swerve-circular-offense
+ * */
+ public static final double NORMAL_GYRO_DRIFT_IN_1_MIN_Std_Dev_RAD = Math.toRadians(1.2);
+ public static final double AVERAGE_VELOCITY_RAD_PER_SEC_DURING_TEST = Math.toRadians(60);
+
+ public static final int SIMULATION_TICKS_IN_1_PERIOD = 5;
+ public static final double SIMULATED_PERIOD_SECONDS = 0.02;
+ }
+
/**
* This is where constants used to describe the game's field go. This will have the dimensions of
* the field, but also the coordinates of obstacles, game pieces, or other places of interest.
diff --git a/src/main/java/frc/robot/Robot.java b/src/main/java/frc/robot/Robot.java
index 41a77a0..5dd899d 100644
--- a/src/main/java/frc/robot/Robot.java
+++ b/src/main/java/frc/robot/Robot.java
@@ -1,18 +1,14 @@
-/*----------------------------------------------------------------------------*/
-/* Copyright (c) 2017-2019 FIRST. All Rights Reserved. */
-/* Open Source Software - may be modified and shared by FRC teams. The code */
-/* must be accompanied by the FIRST BSD license file in the root directory of */
-/* the project. */
-/*----------------------------------------------------------------------------*/
-
package frc.robot;
import edu.wpi.first.wpilibj2.command.Command;
import edu.wpi.first.wpilibj2.command.CommandScheduler;
-import frc.robot.Constants.Mode;
+import frc.robot.extras.simulation.field.SimulatedField;
+import org.littletonrobotics.junction.LogFileUtil;
import org.littletonrobotics.junction.LoggedRobot;
import org.littletonrobotics.junction.Logger;
import org.littletonrobotics.junction.networktables.NT4Publisher;
+import org.littletonrobotics.junction.wpilog.WPILOGReader;
+import org.littletonrobotics.junction.wpilog.WPILOGWriter;
/**
* The VM is configured to automatically run this class, and to call the functions corresponding to
@@ -23,62 +19,82 @@
public class Robot extends LoggedRobot {
private Command m_autonomousCommand;
private RobotContainer m_robotContainer;
- private static final Mode JAVA_SIM_MODE = Mode.SIM;
- public static final Mode CURRENT_ROBOT_MODE = isReal() ? Mode.REAL : JAVA_SIM_MODE;
- /**
- * This function is run when the robot is first started up and should be used for any
- * initialization code.
- */
- @Override
- public void robotInit() {
- Logger.recordMetadata("ProjectName", "MyProject"); // Set a metadata value
-
- if (isReal()) {
- // Logger.addDataReceiver(new WPILOGWriter()); // Log to a USB stick ("/U/logs")
- Logger.addDataReceiver(new NT4Publisher()); // Publish data to NetworkTables
- // new PowerDistribution(1, ModuleType.kRev); // Enables power distribution logging
- } else {
- setUseTiming(false); // Run as fast as possible
- // String logPath = LogFileUtil.findReplayLog(); // Pull the replay log from AdvantageScope
- // (or prompt the user)
- // Logger.setReplaySource(new WPILOGReader(logPath)); // Read replay log
- // Logger.addDataReceiver(new WPILOGWriter(LogFileUtil.addPathSuffix(logPath, "_sim"))); //
- // Save outputs to a new log
+ public Robot() {
+ // Record metadata
+ Logger.recordMetadata("ProjectName", BuildConstants.MAVEN_NAME);
+ Logger.recordMetadata("BuildDate", BuildConstants.BUILD_DATE);
+ Logger.recordMetadata("GitSHA", BuildConstants.GIT_SHA);
+ Logger.recordMetadata("GitDate", BuildConstants.GIT_DATE);
+ Logger.recordMetadata("GitBranch", BuildConstants.GIT_BRANCH);
+
+ // This tells you if you have uncommitted changes in your project
+ switch (BuildConstants.DIRTY) {
+ case 0:
+ Logger.recordMetadata("GitDirty", "All changes committed");
+ break;
+ case 1:
+ Logger.recordMetadata("GitDirty", "Uncomitted changes");
+ break;
+ default:
+ Logger.recordMetadata("GitDirty", "Unknown");
+ break;
}
- // Logger.disableDeterministicTimestamps() // See "Deterministic Timestamps" in the
- // "Understanding Data Flow" page
- Logger.start(); // Start logging! No more data receivers, replay sources, or metadata values may
- // be added.
- // Instantiate our RobotContainer. This will perform all our button bindings, and put our
- // autonomous chooser on the dashboard.
- m_robotContainer = new RobotContainer();
- // uncomment the line below if there is a USB camera plugged into the RoboRIO
- // CameraServer.startAutomaticCapture();
+ // Set up data receivers & replay source
+ switch (Constants.CURRENT_MODE) {
+ case REAL:
+ // Running on a real robot, log to a USB stick ("/U/logs")
+ Logger.addDataReceiver(new WPILOGWriter());
+ // Gets data from network tables
+ Logger.addDataReceiver(new NT4Publisher());
+ break;
+
+ case SIM:
+ // Running a physics simulator, log to NT
+ Logger.addDataReceiver(new NT4Publisher());
+ break;
+
+ case REPLAY:
+ // Replaying a log, set up replay source
+ setUseTiming(false); // Run as fast as possible
+ String logPath = LogFileUtil.findReplayLog();
+ Logger.setReplaySource(new WPILOGReader(logPath));
+ Logger.addDataReceiver(new WPILOGWriter(LogFileUtil.addPathSuffix(logPath, "_sim")));
+ break;
+ }
+
+ // Start AdvantageKit logger
+ Logger.start();
+ // Instantiate our RobotContainer. This will perform all our button bindings,
+ // and put our autonomous chooser on the dashboard.
+ m_robotContainer = new RobotContainer();
}
- /**
- * This function is called every robot packet, no matter the mode. Use this for items like
- * diagnostics that you want ran during disabled, autonomous, teleoperated and test.
- *
- *
This runs after the mode specific periodic functions, but before LiveWindow and
- * SmartDashboard integrated updating.
- */
+ /** This function is called periodically during all modes. */
@Override
public void robotPeriodic() {
- // Runs the Scheduler. This is responsible for polling buttons, adding newly-scheduled
- // commands, running already-scheduled commands, removing finished or interrupted commands,
- // and running subsystem periodic() methods. This must be called from the robot's periodic
- // block in order for anything in the Command-based framework to work.
+ // TODO: test this!
+ // Switch thread to high priority to improve loop timing
+ // Threads.setCurrentThreadPriority(true, 99);
+
+ // Runs the Scheduler. This is responsible for polling buttons, adding
+ // newly-scheduled commands, running already-scheduled commands, removing
+ // finished or interrupted commands, and running subsystem periodic() methods.
+ // This must be called from the robot's periodic block in order for anything in
+ // the Command-based framework to work.
CommandScheduler.getInstance().run();
+
+ // Return to normal thread priority
+ // Threads.setCurrentThreadPriority(false, 10);
}
- /** This function is called once each time the robot enters Disabled mode. */
+ /** This function is called once when the robot is disabled. */
@Override
public void disabledInit() {}
+ /** This function is called periodically when disabled. */
@Override
public void disabledPeriodic() {}
@@ -97,6 +113,7 @@ public void autonomousInit() {
@Override
public void autonomousPeriodic() {}
+ /** This function is called once when teleop is enabled. */
@Override
public void teleopInit() {
// This makes sure that the autonomous stops running when
@@ -106,13 +123,15 @@ public void teleopInit() {
if (m_autonomousCommand != null) {
m_autonomousCommand.cancel();
}
- // m_robotContainer.teleopInit();
+
+ m_robotContainer.teleopInit();
}
/** This function is called periodically during operator control. */
@Override
public void teleopPeriodic() {}
+ /** This function is called once when test mode is enabled. */
@Override
public void testInit() {
// Cancels all running commands at the start of test mode.
@@ -123,6 +142,14 @@ public void testInit() {
@Override
public void testPeriodic() {}
+ /** This function is called once when the robot is first started up. */
@Override
- public void simulationPeriodic() {}
+ public void simulationInit() {}
+
+ /** This function is called periodically whilst in simulation. */
+ @Override
+ public void simulationPeriodic() {
+ SimulatedField.getInstance().simulationPeriodic();
+ m_robotContainer.updateFieldSimAndDisplay();
+ }
}
diff --git a/src/main/java/frc/robot/RobotContainer.java b/src/main/java/frc/robot/RobotContainer.java
index de2c9d0..0f249f0 100644
--- a/src/main/java/frc/robot/RobotContainer.java
+++ b/src/main/java/frc/robot/RobotContainer.java
@@ -1,20 +1,328 @@
-// Copyright (c) FIRST and other WPILib contributors.
-// Open Source Software; you can modify and/or share it under the terms of
-// the WPILib BSD license file in the root directory of this project.
-
package frc.robot;
+import edu.wpi.first.math.geometry.Pose2d;
+import edu.wpi.first.math.geometry.Pose3d;
+import edu.wpi.first.math.geometry.Rotation2d;
+import edu.wpi.first.math.system.plant.DCMotor;
import edu.wpi.first.wpilibj2.command.Command;
-import edu.wpi.first.wpilibj2.command.Commands;
+import edu.wpi.first.wpilibj2.command.InstantCommand;
+import edu.wpi.first.wpilibj2.command.button.CommandXboxController;
+import edu.wpi.first.wpilibj2.command.button.Trigger;
+import frc.robot.Constants.SimulationConstants;
+import frc.robot.commands.drive.DriveCommand;
+import frc.robot.extras.simulation.field.SimulatedField;
+import frc.robot.extras.simulation.mechanismSim.swerve.GyroSimulation;
+import frc.robot.extras.simulation.mechanismSim.swerve.SwerveDriveSimulation;
+import frc.robot.extras.simulation.mechanismSim.swerve.SwerveModuleSimulation;
+import frc.robot.extras.simulation.mechanismSim.swerve.SwerveModuleSimulation.WHEEL_GRIP;
+import frc.robot.extras.util.JoystickUtil;
+import frc.robot.subsystems.swerve.SwerveConstants;
+import frc.robot.subsystems.swerve.SwerveConstants.DriveConstants;
+import frc.robot.subsystems.swerve.SwerveConstants.ModuleConstants;
+import frc.robot.subsystems.swerve.SwerveDrive;
+import frc.robot.subsystems.swerve.gyro.GyroInterface;
+import frc.robot.subsystems.swerve.gyro.PhysicalGyro;
+import frc.robot.subsystems.swerve.gyro.SimulatedGyro;
+import frc.robot.subsystems.swerve.module.ModuleInterface;
+import frc.robot.subsystems.swerve.module.PhysicalModule;
+import frc.robot.subsystems.swerve.module.SimulatedModule;
+import frc.robot.subsystems.vision.PhysicalVision;
+import frc.robot.subsystems.vision.SimulatedVision;
+import frc.robot.subsystems.vision.VisionInterface;
+import frc.robot.subsystems.vision.VisionSubsystem;
+import java.util.function.DoubleSupplier;
+import org.littletonrobotics.junction.Logger;
public class RobotContainer {
+
+ private final VisionSubsystem visionSubsystem;
+ private final SwerveDrive swerveDrive;
+ private final CommandXboxController operatorController = new CommandXboxController(1);
+ // private final Indexer indexer = new Indexer(new IndexerIOTalonFX());
+ // private final Intake intake = new Intake(new IntakeIOTalonFX());
+ // private final Pivot pivot = new Pivot(new PivotIOTalonFX());
+ // private final Flywheel flywheel = new Flywheel(new FlywheelIOTalonFX());
+ private final CommandXboxController driverController = new CommandXboxController(0);
+
+ // Simulation, we store them here in the robot container
+ // private final SimulatedField simulatedArena;
+ private final SwerveDriveSimulation swerveDriveSimulation;
+ private final GyroSimulation gyroSimulation;
+
+ // Subsystems
+ // private final XboxController driverController = new XboxController(0);
+
public RobotContainer() {
- configureBindings();
+ switch (Constants.CURRENT_MODE) {
+ case REAL -> {
+ /* Real robot, instantiate hardware IO implementations */
+
+ /* Disable Simulations */
+ // this.simulatedArena = null;
+ this.gyroSimulation = null;
+ this.swerveDriveSimulation = null;
+
+ swerveDrive =
+ new SwerveDrive(
+ new PhysicalGyro(),
+ new PhysicalModule(SwerveConstants.moduleConfigs[0]),
+ new PhysicalModule(SwerveConstants.moduleConfigs[1]),
+ new PhysicalModule(SwerveConstants.moduleConfigs[2]),
+ new PhysicalModule(SwerveConstants.moduleConfigs[3]));
+ visionSubsystem = new VisionSubsystem(new PhysicalVision());
+ }
+
+ case SIM -> {
+ /* Sim robot, instantiate physics sim IO implementations */
+
+ /* create simulations */
+ /* create simulation for pigeon2 IMU (different IMUs have different measurement erros) */
+ this.gyroSimulation = GyroSimulation.createNavX2();
+ /* create a swerve drive simulation */
+ this.swerveDriveSimulation =
+ new SwerveDriveSimulation(
+ SimulationConstants.ROBOT_MASS_KG,
+ DriveConstants.TRACK_WIDTH,
+ DriveConstants.WHEEL_BASE,
+ DriveConstants.TRACK_WIDTH + .2,
+ DriveConstants.WHEEL_BASE + .2,
+ SwerveModuleSimulation.getModule(
+ DCMotor.getFalcon500(1),
+ DCMotor.getFalcon500(1),
+ 60,
+ WHEEL_GRIP.TIRE_WHEEL,
+ ModuleConstants.DRIVE_GEAR_RATIO),
+ gyroSimulation,
+ new Pose2d(3, 3, new Rotation2d()));
+ SimulatedField.getInstance().addDriveTrainSimulation(swerveDriveSimulation);
+ swerveDrive =
+ new SwerveDrive(
+ new SimulatedGyro(
+ gyroSimulation), // SimulatedGyro is a wrapper around gyro simulation, that
+ // reads
+ // the simulation result
+ /* SimulatedModule are edited such that they also wraps around module simulations */
+ new SimulatedModule(swerveDriveSimulation.getModules()[0]),
+ new SimulatedModule(swerveDriveSimulation.getModules()[1]),
+ new SimulatedModule(swerveDriveSimulation.getModules()[2]),
+ new SimulatedModule(swerveDriveSimulation.getModules()[3]));
+
+ visionSubsystem =
+ new VisionSubsystem(
+ new SimulatedVision(() -> swerveDriveSimulation.getSimulatedDriveTrainPose()));
+
+ SimulatedField.getInstance().resetFieldForAuto();
+ resetFieldAndOdometryForAuto(
+ new Pose2d(1.3980597257614136, 5.493067741394043, Rotation2d.fromRadians(3.1415)));
+ }
+
+ default -> {
+ visionSubsystem = new VisionSubsystem(new VisionInterface() {});
+ /* Replayed robot, disable IO implementations */
+
+ /* physics simulations are also not needed */
+ this.gyroSimulation = null;
+ this.swerveDriveSimulation = null;
+ // this.simulatedArena = null;
+ swerveDrive =
+ new SwerveDrive(
+ new GyroInterface() {},
+ new ModuleInterface() {},
+ new ModuleInterface() {},
+ new ModuleInterface() {},
+ new ModuleInterface() {});
+ }
+ }
+ }
+
+ private void resetFieldAndOdometryForAuto(Pose2d robotStartingPoseAtBlueAlliance) {
+ final Pose2d startingPose = robotStartingPoseAtBlueAlliance;
+
+ if (swerveDriveSimulation != null) {
+ swerveDriveSimulation.setSimulationWorldPose(startingPose);
+ SimulatedField.getInstance().resetFieldForAuto();
+ updateFieldSimAndDisplay();
+ }
+
+ // swerveDrive.periodic();
+ swerveDrive.setPose(startingPose);
+ }
+
+ public void teleopInit() {
+ configureButtonBindings();
}
- private void configureBindings() {}
+ // public void intakeCallback(boolean hasNote) {
+ // if (hasNote) {
+ // driverController.setRumble(RumbleType.kBothRumble, 0.1);
+ // operatorController.setRumble(RumbleType.kBothRumble, 1);
+ // } else {
+ // driverController.setRumble(RumbleType.kBothRumble, 0);
+ // operatorController.setRumble(RumbleType.kBothRumble, 0);
+ // }
+ // }
+ private void configureButtonBindings() {
+ DoubleSupplier driverLeftStickX = driverController::getLeftX;
+ DoubleSupplier driverLeftStickY = driverController::getLeftY;
+ DoubleSupplier driverRightStickX = driverController::getRightX;
+ DoubleSupplier driverLeftStick[] =
+ new DoubleSupplier[] {
+ () -> JoystickUtil.modifyAxisPolar(driverLeftStickX, driverLeftStickY, 3)[0],
+ () -> JoystickUtil.modifyAxisPolar(driverLeftStickX, driverLeftStickY, 3)[1]
+ };
+
+ DoubleSupplier operatorLeftStickX = operatorController::getLeftX;
+ DoubleSupplier operatorRightStickY = operatorController::getRightY;
+
+ Trigger driverRightBumper = new Trigger(driverController.rightBumper());
+ Trigger driverRightDirectionPad = new Trigger(driverController.pov(90));
+ Trigger driverDownDirectionPad = new Trigger(driverController.pov(180));
+ Trigger driverLeftDirectionPad = new Trigger(driverController.pov(270));
+
+ // // autodrive
+ // Trigger driverAButton = new Trigger(driverController::getAButton);
+ // lol whatever
+ // // intake
+ // Trigger operatorLeftTrigger = new Trigger(()->operatorController.getLeftTriggerAxis() > 0.2);
+ // Trigger operatorLeftBumper = new Trigger(operatorController::getLeftBumper);
+ // // amp and speaker
+ // Trigger operatorBButton = new Trigger(operatorController::getBButton);
+ // Trigger operatorRightBumper = new Trigger(operatorController::getRightBumper);
+ // Trigger operatorRightTrigger = new Trigger(()->operatorController.getRightTriggerAxis() >
+ // 0.2);
+ // Trigger driverRightTrigger = new Trigger(()->driverController.getRightTriggerAxis() > 0.2);
+
+ // // manual pivot and intake rollers
+ // Trigger operatorAButton = new Trigger(operatorController::getAButton);
+ // Trigger operatorXButton = new Trigger(operatorController::getXButton);
+ // Trigger operatorYButton = new Trigger(operatorController::getYButton);
+ // DoubleSupplier operatorRightStickY = operatorController::getRightY;
+ // // unused
+ // Trigger operatorUpDirectionPad = new Trigger(()->operatorController.getPOV() == 0);
+ // Trigger operatorLeftDirectionPad = new Trigger(()->operatorController.getPOV() == 270);
+ // Trigger operatorDownDirectionPad = new Trigger(()->operatorController.getPOV() == 180);
+ // Trigger driverLeftTrigger = new Trigger(()->driverController.getLeftTriggerAxis() > 0.2);
+ Trigger driverLeftBumper = new Trigger(driverController.leftBumper());
+ // Trigger driverBButton = new Trigger(driverController::getBButton);
+ // Trigger driverYButton = new Trigger(driverController::getYButton);
+ // DoubleSupplier operatorLeftStickY = operatorController::getLeftY;
+
+ // //DRIVER BUTTONS
+
+ // // driving
+
+ Command driveCommand =
+ new DriveCommand(
+ swerveDrive,
+ visionSubsystem,
+ driverLeftStick[1],
+ driverLeftStick[0],
+ () -> JoystickUtil.modifyAxis(driverRightStickX, 3),
+ () -> !driverRightBumper.getAsBoolean(),
+ () -> driverLeftBumper.getAsBoolean());
+ swerveDrive.setDefaultCommand(driveCommand);
+
+ // // shooterSubsystem.setDefaultCommand(new FlywheelSpinUpAuto(shooterSubsystem,
+ // visionSubsystem));
+
+ // driverLeftTrigger.whileTrue(new TowerIntake(intakeSubsystem, pivotSubsystem,
+ // shooterSubsystem, false, ledSubsystem, this::intakeCallback));
+ // driverLeftTrigger.whileFalse(new TowerIntake(intakeSubsystem, pivotSubsystem,
+ // shooterSubsystem, false, ledSubsystem, this::intakeCallback).withTimeout(0.3));
+ // // Amp Lineup
+ // driverAButton.whileTrue(new AutoAlignWithAmp(swerveDrive, visionSubsystem));
+ // // Spinup for shoot
+ // driverRightTrigger.whileTrue(new SpinUpForSpeaker(swerveDrive, shooterSubsystem,
+ // pivotSubsystem, visionSubsystem, driverLeftStickX, driverLeftStickY, driverRightBumper,
+ // ledSubsystem));
+
+ // // driverLeftBumper.whileTrue(new ShootSpeaker(swerveDrive, shooterSubsystem,
+ // pivotSubsystem, visionSubsystem, driverLeftStickX, operatorLeftStickY, driverRightBumper,
+ // ledSubsystem));
+ // // driverRightTrigger.whileTrue(new ShootWhileMove(swerveDrive, shooterSubsystem,
+ // pivotSubsystem, visionSubsystem, driverLeftStick, driverYButton, ledSubsystem));
+
+ // // Resets the robot angle in the odometry, factors in which alliance the robot is on
+ driverRightDirectionPad.onTrue(
+ new InstantCommand(
+ () ->
+ swerveDrive.setPose(
+ new Pose2d(
+ swerveDrive.getPose().getX(),
+ swerveDrive.getPose().getY(),
+ Rotation2d.fromDegrees(swerveDrive.getAllianceAngleOffset())))));
+ driverController
+ .x()
+ .onTrue(
+ new InstantCommand(
+ () -> swerveDrive.setPose(swerveDriveSimulation.getSimulatedDriveTrainPose())));
+ // // // Reset robot odometry based on vision pose measurement from april tags
+ driverLeftDirectionPad.onTrue(
+ new InstantCommand(() -> swerveDrive.setPose(visionSubsystem.getLastSeenPose())));
+ // // driverLeftDpad.onTrue(new InstantCommand(() -> swerveDrive.resetOdometry(new
+ // Pose2d(15.251774787902832, 5.573054313659668, Rotation2d.fromRadians(3.14159265)))));
+ // // driverBButton.whileTrue(new ShootPass(swerveDrive, shooterSubsystem, pivotSubsystem,
+ // visionSubsystem, driverLeftStickX, driverLeftStickY, driverRightBumper, ledSubsystem));
+
+ // // driverXButton.
+ // driverBButton.whileTrue(new ShootPass(swerveDrive, shooterSubsystem, pivotSubsystem,
+ // visionSubsystem, driverLeftStickY, operatorLeftStickY, driverYButton, ledSubsystem));
+ // // driverDownDirectionPad.whileTrue(new IntakeFromShooter(shooterSubsystem,
+ // intakeSubsystem));
+ // // driverYButton.whileTrue(new ShootSpeaker(swerveDrive, shooterSubsystem, pivotSubsystem,
+ // visionSubsystem, driverLeftStickX, operatorLeftStickY, driverRightBumper, ledSubsystem));
+ // // OPERATOR BUTTONS
+
+ // // speaker
+ // operatorRightTrigger.whileTrue(new ShootSpeaker(swerveDrive, shooterSubsystem,
+ // pivotSubsystem, visionSubsystem, driverLeftStickX, driverLeftStickY, driverRightBumper,
+ // ledSubsystem));
+ // // amp
+ // operatorRightBumper.whileTrue(new ShootAmp(shooterSubsystem, pivotSubsystem, ledSubsystem,
+ // operatorBButton));
+ // // fender shot
+ // operatorUpDirectionPad.whileTrue(new SubwooferShot(swerveDrive, shooterSubsystem,
+ // pivotSubsystem, visionSubsystem, driverLeftStickX, driverLeftStickY, driverRightStickX,
+ // driverRightBumper, ledSubsystem));
+ // // intake (aka SUCC_BUTTON)
+ // operatorLeftTrigger.whileTrue(new TowerIntake(intakeSubsystem, pivotSubsystem,
+ // shooterSubsystem, false, ledSubsystem, this::intakeCallback));
+ // operatorLeftTrigger.whileFalse(new TowerIntake(intakeSubsystem, pivotSubsystem,
+ // shooterSubsystem, false, ledSubsystem, this::intakeCallback).withTimeout(0.2));
+ // // outtake (aka UNSUCC_BUTTON)
+ // operatorLeftBumper.whileTrue(new TowerIntake(intakeSubsystem, pivotSubsystem,
+ // shooterSubsystem, true, ledSubsystem, this::intakeCallback));
+ // // manual pivot (possible climb, unlikely)
+ // operatorAButton.whileTrue(new ManualPivot(pivotSubsystem,
+ // ()->modifyAxisCubed(operatorRightStickY)));
+ // operatorDownDirectionPad.whileTrue(new ManualPivot(pivotSubsystem, ()->-0.2));
+ // // manual rollers
+ // operatorYButton.whileTrue(new ManualIntake(intakeSubsystem, true));
+ // operatorXButton.whileTrue(new ManualIntake(intakeSubsystem, false));
+
+ // operatorBButton.onTrue(new StopShooterAndIntake(intakeSubsystem, pivotSubsystem,
+ // shooterSubsystem));
+ }
public Command getAutonomousCommand() {
- return Commands.print("No autonomous command configured");
+ // Resets the pose factoring in the robot side
+ // This is just a failsafe, pose should be reset at the beginning of auto
+ swerveDrive.setPose(
+ new Pose2d(
+ swerveDrive.getPose().getX(),
+ swerveDrive.getPose().getY(),
+ Rotation2d.fromDegrees(swerveDrive.getAllianceAngleOffset())));
+ // return autoChooser.getSelected();
+ // return new DriveForwardAndBack(swerveDrive);
+ return null;
+ }
+
+ public void updateFieldSimAndDisplay() {
+ if (swerveDriveSimulation == null) return;
+ Logger.recordOutput(
+ "FieldSimulation/RobotPosition", swerveDriveSimulation.getSimulatedDriveTrainPose());
+ Logger.recordOutput(
+ "FieldSimulation/Notes",
+ SimulatedField.getInstance().getGamePiecesByType("Note").toArray(Pose3d[]::new));
}
}
diff --git a/src/main/java/frc/robot/commands/drive/DriveCommand.java b/src/main/java/frc/robot/commands/drive/DriveCommand.java
index 4d1b522..83b1ac9 100644
--- a/src/main/java/frc/robot/commands/drive/DriveCommand.java
+++ b/src/main/java/frc/robot/commands/drive/DriveCommand.java
@@ -2,7 +2,7 @@
import frc.robot.subsystems.swerve.SwerveConstants.DriveConstants;
import frc.robot.subsystems.swerve.SwerveDrive;
-import frc.robot.subsystems.vision.Vision;
+import frc.robot.subsystems.vision.VisionSubsystem;
import java.util.function.BooleanSupplier;
import java.util.function.DoubleSupplier;
@@ -29,7 +29,7 @@ public class DriveCommand extends DriveCommandBase {
*/
public DriveCommand(
SwerveDrive driveSubsystem,
- Vision visionSubsystem,
+ VisionSubsystem visionSubsystem,
DoubleSupplier leftJoystickY,
DoubleSupplier leftJoystickX,
DoubleSupplier rightJoystickX,
diff --git a/src/main/java/frc/robot/commands/drive/DriveCommandBase.java b/src/main/java/frc/robot/commands/drive/DriveCommandBase.java
index ea557e1..b8136a3 100644
--- a/src/main/java/frc/robot/commands/drive/DriveCommandBase.java
+++ b/src/main/java/frc/robot/commands/drive/DriveCommandBase.java
@@ -6,8 +6,9 @@
import edu.wpi.first.wpilibj2.command.Command;
import frc.robot.extras.interpolators.MultiLinearInterpolator;
import frc.robot.subsystems.swerve.SwerveDrive;
-import frc.robot.subsystems.vision.Vision;
import frc.robot.subsystems.vision.VisionConstants;
+import frc.robot.subsystems.vision.VisionConstants.Limelight;
+import frc.robot.subsystems.vision.VisionSubsystem;
public abstract class DriveCommandBase extends Command {
@@ -16,7 +17,7 @@ public abstract class DriveCommandBase extends Command {
private final MultiLinearInterpolator twoAprilTagLookupTable =
new MultiLinearInterpolator(VisionConstants.TWO_APRIL_TAG_LOOKUP_TABLE);
- private final Vision vision;
+ private final VisionSubsystem vision;
private final SwerveDrive swerveDrive;
private double lastTimeStampSeconds = 0;
@@ -27,7 +28,7 @@ public abstract class DriveCommandBase extends Command {
* @param driveSubsystem The subsystem for the swerve drive
* @param vision The subsystem for vision measurements
*/
- public DriveCommandBase(SwerveDrive swerveDrive, Vision vision) {
+ public DriveCommandBase(SwerveDrive swerveDrive, VisionSubsystem vision) {
this.swerveDrive = swerveDrive;
this.vision = vision;
// It is important that you do addRequirements(driveSubsystem, vision) in whatever
@@ -40,12 +41,12 @@ public void execute() {
swerveDrive.addPoseEstimatorSwerveMeasurement();
vision.setHeadingInfo(
swerveDrive.getPose().getRotation().getDegrees(), swerveDrive.getGyroRate());
- calculatePoseFromLimelight(VisionConstants.SHOOTER_LIMELIGHT_NUMBER);
- calculatePoseFromLimelight(VisionConstants.FRONT_LEFT_LIMELIGHT_NUMBER);
- calculatePoseFromLimelight(VisionConstants.FRONT_RIGHT_LIMELIGHT_NUMBER);
+ calculatePoseFromLimelight(Limelight.SHOOTER);
+ calculatePoseFromLimelight(Limelight.FRONT_LEFT);
+ calculatePoseFromLimelight(Limelight.FRONT_RIGHT);
}
- public void calculatePoseFromLimelight(int limelightNumber) {
+ public void calculatePoseFromLimelight(Limelight limelightNumber) {
double currentTimeStampSeconds = lastTimeStampSeconds;
// Updates the robot's odometry with april tags
diff --git a/src/main/java/frc/robot/extras/debug/LoggedTunableNumber.java b/src/main/java/frc/robot/extras/debug/LoggedTunableNumber.java
deleted file mode 100644
index c0f62f1..0000000
--- a/src/main/java/frc/robot/extras/debug/LoggedTunableNumber.java
+++ /dev/null
@@ -1,122 +0,0 @@
-// Copyright (c) 2024 FRC 6328
-// http://github.com/Mechanical-Advantage
-//
-// Use of this source code is governed by an MIT-style
-// license that can be found in the LICENSE file at
-// the root directory of this project.
-
-package frc.robot.extras.debug;
-
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.function.Consumer;
-import java.util.function.DoubleSupplier;
-import org.littletonrobotics.junction.networktables.LoggedDashboardNumber;
-
-/**
- * Class for a tunable number. Gets value from dashboard in tuning mode, returns default if not or
- * value not in dashboard.
- */
-public class LoggedTunableNumber implements DoubleSupplier {
- private static final String tableKey = "TunableNumbers";
-
- private final String key;
- private boolean hasDefault = false;
- private double defaultValue;
- private LoggedDashboardNumber dashboardNumber;
- private Map lastHasChangedValues = new HashMap<>();
-
- /**
- * Create a new LoggedTunableNumber
- *
- * @param dashboardKey Key on dashboard
- */
- public LoggedTunableNumber(String dashboardKey) {
- this.key = tableKey + "/" + dashboardKey;
- }
-
- /**
- * Create a new LoggedTunableNumber with the default value
- *
- * @param dashboardKey Key on dashboard
- * @param defaultValue Default value
- */
- public LoggedTunableNumber(String dashboardKey, double defaultValue) {
- this(dashboardKey);
- initDefault(defaultValue);
- }
-
- /**
- * Set the default value of the number. The default value can only be set once.
- *
- * @param defaultValue The default value
- */
- public void initDefault(double defaultValue) {
- if (!hasDefault) {
- hasDefault = true;
- this.defaultValue = defaultValue;
- if (hasDefault) {
- dashboardNumber = new LoggedDashboardNumber(key, defaultValue);
- }
- }
- }
-
- /**
- * Get the current value, from dashboard if available and in tuning mode.
- *
- * @return The current value
- */
- public double get() {
- if (!hasDefault) {
- return 0.0;
- } else {
- return hasDefault ? dashboardNumber.get() : defaultValue;
- }
- }
-
- /**
- * Checks whether the number has changed since our last check
- *
- * @param id Unique identifier for the caller to avoid conflicts when shared between multiple
- * objects. Recommended approach is to pass the result of "hashCode()"
- * @return True if the number has changed since the last time this method was called, false
- * otherwise.
- */
- public boolean hasChanged(int id) {
- double currentValue = get();
- Double lastValue = lastHasChangedValues.get(id);
- if (lastValue == null || currentValue != lastValue) {
- lastHasChangedValues.put(id, currentValue);
- return true;
- }
-
- return false;
- }
-
- /**
- * Runs action if any of the tunableNumbers have changed
- *
- * @param id Unique identifier for the caller to avoid conflicts when shared between multiple *
- * objects. Recommended approach is to pass the result of "hashCode()"
- * @param action Callback to run when any of the tunable numbers have changed. Access tunable
- * numbers in order inputted in method
- * @param tunableNumbers All tunable numbers to check
- */
- public static void ifChanged(
- int id, Consumer action, LoggedTunableNumber... tunableNumbers) {
- if (Arrays.stream(tunableNumbers).anyMatch(tunableNumber -> tunableNumber.hasChanged(id))) {
- action.accept(Arrays.stream(tunableNumbers).mapToDouble(LoggedTunableNumber::get).toArray());
- }
- }
-
- /** Runs action if any of the tunableNumbers have changed */
- public static void ifChanged(int id, Runnable action, LoggedTunableNumber... tunableNumbers) {
- ifChanged(id, values -> action.run(), tunableNumbers);
- }
-
- @Override
- public double getAsDouble() {
- return get();
- }
-}
diff --git a/src/main/java/frc/robot/extras/interpolators/BilinearInterpolator.java b/src/main/java/frc/robot/extras/interpolators/BilinearInterpolator.java
deleted file mode 100644
index 4e65ecc..0000000
--- a/src/main/java/frc/robot/extras/interpolators/BilinearInterpolator.java
+++ /dev/null
@@ -1,266 +0,0 @@
-// Copyright (c) LukeLib
-
-package frc.robot.extras.interpolators;
-
-public class BilinearInterpolator {
-
- private final double[][] lookupTable;
- private final double dataRowSize;
-
- /**
- * Handles finding values through a lookup table in a linear fashion.
- *
- * @param lookupTable an array containing {x_input, y_input, x_val, y_val} points. The collected
- * data must be in ascending y order, collected from left to right, reading up.
- */
- public BilinearInterpolator(double[][] lookupTable) {
- this.lookupTable = lookupTable;
- // If you don't use rows of seven, replace the numbers 6, 7, 8 in the commands with the
- // dataRowSize variable with dataRowSize - 1, dataRowSize, dataRowSize + 1
- // TODO: POST Comp Incorporate this variable
- this.dataRowSize = lookupTable[0].length;
- }
-
- /**
- * Returns an interpolated array from the lookup table corresponding to the given input x and y
- * value.
- */
- public double[] getLookupValue(double inputXValue, double inputYValue) {
- // if the note center is approximately in the middle, then only need to do y linear
- // interpolation (no bilinear :)
- if (inputXValue >= 155 && inputXValue <= 165) {
- for (int i = 0; i < lookupTable.length - 8; i += 7) {
- // check if it is equal to a collected data point
- if (inputYValue == lookupTable[i][1]) {
- return new double[] {lookupTable[i][2], lookupTable[i][3]};
- }
-
- // interpolate in purely y-direction
- else if (inputYValue < lookupTable[i][1] && inputYValue > lookupTable[i + 7][1]) {
- // if x matches up with a data pt, interpolate between above's data pts' y's to determine
- // pose
- double ySlope =
- (lookupTable[i + 7][3] - lookupTable[i][3])
- / (lookupTable[i + 7][1] - lookupTable[i][1]);
- double yYIntercept = lookupTable[i][3];
- return new double[] {
- lookupTable[i][2], ySlope * (inputYValue - lookupTable[i][1]) + yYIntercept
- };
- }
- }
- // in case nothing works or like the note x is good but its too far away in y direction (test)
- return new double[] {0.0, -1.0};
- }
-
- // if the note center is less than half the x_pixels (160)
- if (inputXValue < 160) {
- // if note center is less than halfway, then we reflect across y-axis
- inputXValue = 320 - inputXValue;
- // Loop through the second row to the second to top most row drawing diagonal vectors. Cannot
- // start at top/bottom because there cannot be diagonal vectors in both directions
- for (int i = 7; i < lookupTable.length - 7; i++) {
- // check if it is equivalent to collected data point
- if (inputXValue == lookupTable[i][0]) {
- if (inputYValue == lookupTable[i][1]) {
- return new double[] {-(lookupTable[i][2]), lookupTable[i][3]};
- }
- }
-
- // bilinear interpolation approximation used diagnoal vectored pt (facing top right)
- if (inputXValue > lookupTable[i][0]
- && inputYValue < lookupTable[i][1]
- && inputXValue < lookupTable[i + 8][0]
- && inputYValue > lookupTable[i + 8][1]) {
- // Interpolate diagonal points
- double xSlope =
- (lookupTable[i + 8][2] - lookupTable[i][2])
- / (lookupTable[i + 8][0] - lookupTable[i][0]);
- double xYIntercept = lookupTable[i][2];
- double ySlope =
- (lookupTable[i + 8][3] - lookupTable[i][3])
- / (lookupTable[i + 8][1] - lookupTable[i][1]);
- double yYIntercept = lookupTable[i][3];
-
- return new double[] {
- -(xSlope * (inputXValue - lookupTable[i][0]) + xYIntercept),
- ySlope * (inputYValue - lookupTable[i][1]) + yYIntercept
- };
- }
-
- // bilinear interpolation approximation used diagnoal in opposite direction vectored pt
- // (facing bottom right)
- if (inputXValue > lookupTable[i][0]
- && inputYValue > lookupTable[i][1]
- && inputXValue < lookupTable[i - 6][0]
- && inputYValue < lookupTable[i - 6][1]) {
- // Interpolate diagonal points
- double xSlope =
- (lookupTable[i - 6][2] - lookupTable[i][2])
- / (lookupTable[i - 6][0] - lookupTable[i][0]);
- double xYIntercept = lookupTable[i][2];
- double ySlope =
- (lookupTable[i - 6][3] - lookupTable[i][3])
- / (lookupTable[i - 6][1] - lookupTable[i][1]);
- double yYIntercept = lookupTable[i][3];
-
- return new double[] {
- -(xSlope * (inputXValue - lookupTable[i][0]) + xYIntercept),
- ySlope * (inputYValue - lookupTable[i][1]) + yYIntercept
- };
- }
- }
-
- // check for first row for only x interpolation
- for (int i = 0; i < 7; i++) {
- if (inputYValue == lookupTable[i][1]) {
- if (inputXValue == lookupTable[i][0]) {
- return new double[] {-lookupTable[i][2], lookupTable[i][3]};
- } else {
- // if y matches up with a data pt, interpolate between adjacent (right for over 160)
- // data pts' x's to determine pose
- double xSlope =
- (lookupTable[i + 1][2] - lookupTable[i][2])
- / (lookupTable[i + 1][0] - lookupTable[i][0]);
- double xYIntercept = lookupTable[i][2];
- return new double[] {
- -(xSlope * (inputYValue - lookupTable[i][0]) + xYIntercept), lookupTable[i][3]
- };
- }
- }
- }
-
- // check for last row just to check for x only interpolation or point checking. (You can't do
- // y_interpolation or diagonal)
- for (int i = lookupTable.length - 7; i < lookupTable.length; i++) {
- if (inputYValue == lookupTable[i][1]) {
- if (inputXValue == lookupTable[i][0]) {
- return new double[] {-lookupTable[i][2], lookupTable[i][3]};
- } else {
- // if y matches up with a data pt, interpolate between adjacent (right for over 160)
- // data pts' x's to determine pose
- double xSlope =
- (lookupTable[i + 1][2] - lookupTable[i][2])
- / (lookupTable[i + 1][0] - lookupTable[i][0]);
- double xYIntercept = lookupTable[i][2];
- return new double[] {
- -(xSlope * (inputYValue - lookupTable[i][0]) + xYIntercept), lookupTable[i][3]
- };
- }
- }
- }
-
- // if outside the collected data pt range,
- // then return unused values for now. I could make a handle to get the max one and then
- // interpolate or get max for both if its outside and thats what the above 4 cases are
- // supposed to do but the
- // logic may need to be changed
- return new double[] {-1.0, -1.0, 69.0};
- }
-
- // if the note center is greater than half the x_pixels (160)
- if (inputXValue > 160) {
- // Loop through the second row to the second to top most row drawing diagonal vectors. Cannot
- // start at top/bottom because there cannot be diagonal vectors in both directions
- for (int i = 7; i < lookupTable.length - 7; i++) {
- // check to see if the found note center is equivalent to a collected data point
- if (inputXValue == lookupTable[i][0]) {
- if (inputYValue == lookupTable[i][1]) {
- return new double[] {lookupTable[i][2], lookupTable[i][3]};
- }
- }
-
- // bilinear interpolation approximation used diagnoal vectored pt (facing top right)
- if (inputXValue > lookupTable[i][0]
- && inputYValue < lookupTable[i][1]
- && inputXValue < lookupTable[i + 8][0]
- && inputYValue > lookupTable[i + 8][1]) {
- // interpolate in both directions using vector
- double xSlope =
- (lookupTable[i + 8][2] - lookupTable[i][2])
- / (lookupTable[i + 8][0] - lookupTable[i][0]);
- double xYIntercept = lookupTable[i][2];
- double ySlope =
- (lookupTable[i + 8][3] - lookupTable[i][3])
- / (lookupTable[i + 8][1] - lookupTable[i][1]);
- double yYIntercept = lookupTable[i][3];
-
- return new double[] {
- xSlope * (inputXValue - lookupTable[i][0]) + xYIntercept,
- ySlope * (inputYValue - lookupTable[i][1]) + yYIntercept
- };
- }
-
- // bilinear interpolation approximation used diagnoal in opposite direction vectored pt
- if (inputXValue > lookupTable[i][0]
- && inputYValue > lookupTable[i][1]
- && inputXValue < lookupTable[i - 6][0]
- && inputYValue < lookupTable[i - 6][1]) {
- // interpolate in both directions using vector
- double xSlope =
- (lookupTable[i - 6][2] - lookupTable[i][2])
- / (lookupTable[i - 6][0] - lookupTable[i][0]);
- double xYIntercept = lookupTable[i][2];
- double ySlope =
- (lookupTable[i - 6][3] - lookupTable[i][3])
- / (lookupTable[i - 6][1] - lookupTable[i][1]);
- double yYIntercept = lookupTable[i][3];
-
- return new double[] {
- xSlope * (inputXValue - lookupTable[i][0]) + xYIntercept,
- ySlope * (inputYValue - lookupTable[i][1]) + yYIntercept
- };
- }
- }
-
- // check for first row for only x interpolation
- for (int i = 0; i < 7; i++) {
- if (inputYValue == lookupTable[i][1]) {
- if (inputXValue == lookupTable[i][0]) {
- return new double[] {lookupTable[i][2], lookupTable[i][3]};
- } else {
- // if y matches up with a data pt, interpolate between adjacent (right for over 160)
- // data pts' x's to determine pose
- double xSlope =
- (lookupTable[i + 1][2] - lookupTable[i][2])
- / (lookupTable[i + 1][0] - lookupTable[i][0]);
- double xYIntercept = lookupTable[i][2];
- return new double[] {
- xSlope * (inputYValue - lookupTable[i][0]) + xYIntercept, lookupTable[i][3]
- };
- }
- }
- }
-
- // check for last row just to check for x only interpolation or point checking. (You can't do
- // y_interpolation or diagonal)
- for (int i = lookupTable.length - 7; i < lookupTable.length; i++) {
- if (inputYValue == lookupTable[i][1]) {
- if (inputXValue == lookupTable[i][0]) {
- return new double[] {lookupTable[i][2], lookupTable[i][3]};
- } else {
- // if y matches up with a data pt, interpolate between adjacent (right for over 160)
- // data pts' x's to determine pose
- double xSlope =
- (lookupTable[i + 1][2] - lookupTable[i][2])
- / (lookupTable[i + 1][0] - lookupTable[i][0]);
- double xYIntercept = lookupTable[i][2];
- return new double[] {
- xSlope * (inputYValue - lookupTable[i][0]) + xYIntercept, lookupTable[i][3]
- };
- }
- }
- }
-
- // if outside the collected data pt range,
- // then return unused values for now. I could make a handle to get the max one and then
- // interpolate or get max for both if its outside and thats what the above 4 cases are
- // supposed to do but the
- // logic may need to be changed
- return new double[] {-1.0, -1.0, 96.0};
- }
-
- // The third number in each double is just for debugging purposes to determine which case was
- // selected
- return new double[] {-1.0, -1.0, 77.0};
- }
-}
diff --git a/src/main/java/frc/robot/extras/setpointGen/AdvancedSwerveModuleState.java b/src/main/java/frc/robot/extras/setpointGen/AdvancedSwerveModuleState.java
new file mode 100644
index 0000000..1559662
--- /dev/null
+++ b/src/main/java/frc/robot/extras/setpointGen/AdvancedSwerveModuleState.java
@@ -0,0 +1,27 @@
+package frc.robot.extras.setpointGen;
+
+import edu.wpi.first.math.geometry.Rotation2d;
+import edu.wpi.first.math.kinematics.SwerveModuleState;
+import edu.wpi.first.util.struct.Struct;
+
+public final class AdvancedSwerveModuleState extends SwerveModuleState {
+ public double steerVelocityFF;
+ public double driveAccelerationFF;
+
+ public AdvancedSwerveModuleState(
+ double speedMetersPerSecond,
+ Rotation2d angle,
+ double steerVelocityFF,
+ double driveAccelerationFF) {
+ super(speedMetersPerSecond, angle);
+ this.steerVelocityFF = steerVelocityFF;
+ this.driveAccelerationFF = driveAccelerationFF;
+ }
+
+ // todo: implement custom struct
+ public static final Struct struct = SwerveModuleState.struct;
+
+ public static AdvancedSwerveModuleState fromBase(SwerveModuleState base) {
+ return new AdvancedSwerveModuleState(base.speedMetersPerSecond, base.angle, 0.0, 0.0);
+ }
+}
diff --git a/src/main/java/frc/robot/extras/setpointGen/SPGCalcs.java b/src/main/java/frc/robot/extras/setpointGen/SPGCalcs.java
new file mode 100644
index 0000000..a01f64d
--- /dev/null
+++ b/src/main/java/frc/robot/extras/setpointGen/SPGCalcs.java
@@ -0,0 +1,285 @@
+package frc.robot.extras.setpointGen;
+
+import edu.wpi.first.math.geometry.Rotation2d;
+import edu.wpi.first.math.kinematics.ChassisSpeeds;
+import edu.wpi.first.math.kinematics.SwerveModuleState;
+import edu.wpi.first.util.struct.Struct;
+import edu.wpi.first.util.struct.StructSerializable;
+import frc.robot.extras.util.ProceduralStructGenerator;
+import frc.robot.extras.util.ProceduralStructGenerator.FixedSizeArray;
+import frc.robot.extras.util.ProceduralStructGenerator.IgnoreStructField;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+class SPGCalcs {
+ private static final double kEpsilon = 1E-8;
+ static final int NUM_MODULES = 4;
+
+ static double unwrapAngle(double ref, double angle) {
+ double diff = angle - ref;
+ if (diff > Math.PI) {
+ return angle - 2.0 * Math.PI;
+ } else if (diff < -Math.PI) {
+ return angle + 2.0 * Math.PI;
+ } else {
+ return angle;
+ }
+ }
+
+ static double findSteeringMaxS(
+ double prevVx,
+ double prevVy,
+ double prevHeading,
+ double desiredVx,
+ double desiredVy,
+ double desiredHeading,
+ double maxDeviation) {
+ desiredHeading = unwrapAngle(prevHeading, desiredHeading);
+ double diff = desiredHeading - prevHeading;
+ if (Math.abs(diff) <= maxDeviation) {
+ // Can go all the way to s=1.
+ return 1.0;
+ }
+
+ double target = prevHeading + Math.copySign(maxDeviation, diff);
+
+ // Rotate the velocity vectors such that the target angle becomes the +X
+ // axis. We only need find the Y components, h_0 and h_1, since they are
+ // proportional to the distances from the two points to the solution
+ // point (x_0 + (x_1 - x_0)s, y_0 + (y_1 - y_0)s).
+ double sin = Math.sin(-target);
+ double cos = Math.cos(-target);
+ double h_0 = sin * prevVx + cos * prevVy;
+ double h_1 = sin * desiredVx + cos * desiredVy;
+
+ // Undo linear interpolation from h_0 to h_1:
+ // 0 = h_0 + (h_1 - h_0) * s
+ // -h_0 = (h_1 - h_0) * s
+ // -h_0 / (h_1 - h_0) = s
+ // h_0 / (h_0 - h_1) = s
+ // Guaranteed to not divide by zero, since if h_0 was equal to h_1, theta_0
+ // would be equal to theta_1, which is caught by the difference check.
+ return h_0 / (h_0 - h_1);
+ }
+
+ private static boolean isValidS(double s) {
+ return Double.isFinite(s) && s >= 0 && s <= 1;
+ }
+
+ static double findDriveMaxS(double x_0, double y_0, double x_1, double y_1, double max_vel_step) {
+ // Derivation:
+ // Want to find point P(s) between (x_0, y_0) and (x_1, y_1) where the
+ // length of P(s) is the target T. P(s) is linearly interpolated between the
+ // points, so P(s) = (x_0 + (x_1 - x_0) * s, y_0 + (y_1 - y_0) * s).
+ // Then,
+ // T = sqrt(P(s).x^2 + P(s).y^2)
+ // T^2 = (x_0 + (x_1 - x_0) * s)^2 + (y_0 + (y_1 - y_0) * s)^2
+ // T^2 = x_0^2 + 2x_0(x_1-x_0)s + (x_1-x_0)^2*s^2
+ // + y_0^2 + 2y_0(y_1-y_0)s + (y_1-y_0)^2*s^2
+ // T^2 = x_0^2 + 2x_0x_1s - 2x_0^2*s + x_1^2*s^2 - 2x_0x_1s^2 + x_0^2*s^2
+ // + y_0^2 + 2y_0y_1s - 2y_0^2*s + y_1^2*s^2 - 2y_0y_1s^2 + y_0^2*s^2
+ // 0 = (x_0^2 + y_0^2 + x_1^2 + y_1^2 - 2x_0x_1 - 2y_0y_1)s^2
+ // + (2x_0x_1 + 2y_0y_1 - 2x_0^2 - 2y_0^2)s
+ // + (x_0^2 + y_0^2 - T^2).
+ //
+ // To simplify, we can factor out some common parts:
+ // Let l_0 = x_0^2 + y_0^2, l_1 = x_1^2 + y_1^2, and
+ // p = x_0 * x_1 + y_0 * y_1.
+ // Then we have
+ // 0 = (l_0 + l_1 - 2p)s^2 + 2(p - l_0)s + (l_0 - T^2),
+ // with which we can solve for s using the quadratic formula.
+
+ double l_0 = x_0 * x_0 + y_0 * y_0;
+ double l_1 = x_1 * x_1 + y_1 * y_1;
+ double sqrt_l_0 = Math.sqrt(l_0);
+ double diff = Math.sqrt(l_1) - sqrt_l_0;
+ if (Math.abs(diff) <= max_vel_step) {
+ // Can go all the way to s=1.
+ return 1.0;
+ }
+
+ double target = sqrt_l_0 + Math.copySign(max_vel_step, diff);
+ double p = x_0 * x_1 + y_0 * y_1;
+
+ // Quadratic of s
+ double a = l_0 + l_1 - 2 * p;
+ double b = 2 * (p - l_0);
+ double c = l_0 - target * target;
+ double root = Math.sqrt(b * b - 4 * a * c);
+
+ // Check if either of the solutions are valid
+ // Won't divide by zero because it is only possible for a to be zero if the
+ // target velocity is exactly the same or the reverse of the current
+ // velocity, which would be caught by the difference check.
+ double s_1 = (-b + root) / (2 * a);
+ if (isValidS(s_1)) {
+ return s_1;
+ }
+ double s_2 = (-b - root) / (2 * a);
+ if (isValidS(s_2)) {
+ return s_2;
+ }
+
+ // Since we passed the initial max_vel_step check, a solution should exist,
+ // but if no solution was found anyway, just don't limit movement
+ return 1.0;
+ }
+
+ static boolean epsilonEquals(double a, double b, double epsilon) {
+ return (a - epsilon <= b) && (a + epsilon >= b);
+ }
+
+ static boolean epsilonEquals(double a, double b) {
+ return epsilonEquals(a, b, kEpsilon);
+ }
+
+ static boolean epsilonEquals(ChassisSpeeds s1, ChassisSpeeds s2) {
+ return epsilonEquals(s1.vxMetersPerSecond, s2.vxMetersPerSecond)
+ && epsilonEquals(s1.vyMetersPerSecond, s2.vyMetersPerSecond)
+ && epsilonEquals(s1.omegaRadiansPerSecond, s2.omegaRadiansPerSecond);
+ }
+
+ /**
+ * Check if it would be faster to go to the opposite of the goal heading (and reverse drive
+ * direction).
+ *
+ * @param prevToGoal The rotation from the previous state to the goal state (i.e.
+ * prev.inverse().rotateBy(goal)).
+ * @return True if the shortest path to achieve this rotation involves flipping the drive
+ * direction.
+ */
+ static boolean flipHeading(double prevToGoal) {
+ return Math.abs(prevToGoal) > Math.PI / 2.0;
+ }
+
+ private static final Struct structVectors;
+ private static final Struct structVars;
+
+ static {
+ final var structVectorsProc =
+ ProceduralStructGenerator.genObject(LocalVectors.class, LocalVectors::new);
+ structVectors =
+ new Struct() {
+ @Override
+ public String getSchema() {
+ return "float64 vx;float64 vy;float64 cos;float64 sin;";
+ }
+
+ @Override
+ public int getSize() {
+ return 32;
+ }
+
+ @Override
+ public Class getTypeClass() {
+ return LocalVectors.class;
+ }
+
+ @Override
+ public String getTypeName() {
+ return "LocalVectors";
+ }
+
+ @Override
+ public void pack(ByteBuffer bb, LocalVectors value) {
+ bb.putDouble(value.vx);
+ bb.putDouble(value.vy);
+ bb.putDouble(value.cos);
+ bb.putDouble(value.sin);
+ }
+
+ @Override
+ public LocalVectors unpack(ByteBuffer bb) {
+ return structVectorsProc.unpack(bb);
+ }
+
+ @Override
+ public String toString() {
+ return this.getTypeName() + "<" + this.getSize() + ">" + " {" + this.getSchema() + "}";
+ }
+ };
+ structVars = ProceduralStructGenerator.genObject(LocalVars.class, LocalVars::new);
+ System.out.println(structVectors);
+ System.out.println(structVars);
+ }
+
+ static final class LocalVectors implements StructSerializable {
+ public double vx, vy, cos, sin;
+
+ public LocalVectors() {}
+
+ public void reset() {
+ vx = vy = cos = sin = 0.0;
+ }
+
+ public void applyModuleState(SwerveModuleState state) {
+ cos = state.angle.getCos();
+ sin = state.angle.getSin();
+ vx = cos * state.speedMetersPerSecond;
+ vy = sin * state.speedMetersPerSecond;
+ if (state.speedMetersPerSecond < 0.0) {
+ applyRotation(Rotation2d.k180deg.getCos(), Rotation2d.k180deg.getSin());
+ }
+ }
+
+ public LocalVectors applyRotation(double otherCos, double otherSin) {
+ double newCos = cos * otherCos - sin * otherSin;
+ double newSin = cos * otherSin + sin * otherCos;
+ cos = newCos;
+ sin = newSin;
+
+ return this;
+ }
+
+ public double radians() {
+ return Math.atan2(sin, cos);
+ }
+
+ public static final Struct struct = structVectors;
+ }
+
+ static final class LocalVars implements StructSerializable {
+ @FixedSizeArray(size = NUM_MODULES)
+ public LocalVectors[] prev;
+
+ @FixedSizeArray(size = NUM_MODULES)
+ public LocalVectors[] desired;
+
+ public boolean needToSteer = true, allModulesShouldFlip = true;
+ public double minS, dt;
+ public double dx, dy, dtheta;
+ public ChassisSpeeds prevSpeeds, desiredSpeeds;
+
+ @FixedSizeArray(size = NUM_MODULES)
+ public SwerveModuleState[] prevModuleStates;
+
+ @FixedSizeArray(size = NUM_MODULES)
+ public SwerveModuleState[] desiredModuleStates;
+
+ @IgnoreStructField public Rotation2d[] steeringOverride;
+
+ public LocalVars() {
+ desiredSpeeds = prevSpeeds = new ChassisSpeeds();
+ prev = new LocalVectors[NUM_MODULES];
+ desired = new LocalVectors[NUM_MODULES];
+ steeringOverride = new Rotation2d[NUM_MODULES];
+ for (int i = 0; i < NUM_MODULES; i++) {
+ prev[i] = new LocalVectors();
+ desired[i] = new LocalVectors();
+ }
+ }
+
+ public LocalVars reset() {
+ needToSteer = allModulesShouldFlip = true;
+ Arrays.fill(steeringOverride, null);
+ for (int i = 0; i < NUM_MODULES; i++) {
+ prev[i].reset();
+ desired[i].reset();
+ }
+
+ return this;
+ }
+
+ public static final Struct struct = structVars;
+ }
+}
diff --git a/src/main/java/frc/robot/extras/setpointGen/SwerveSetpoint.java b/src/main/java/frc/robot/extras/setpointGen/SwerveSetpoint.java
new file mode 100644
index 0000000..7bb5e7c
--- /dev/null
+++ b/src/main/java/frc/robot/extras/setpointGen/SwerveSetpoint.java
@@ -0,0 +1,30 @@
+package frc.robot.extras.setpointGen;
+
+import edu.wpi.first.math.geometry.Rotation2d;
+import edu.wpi.first.math.kinematics.ChassisSpeeds;
+import edu.wpi.first.util.struct.Struct;
+import edu.wpi.first.util.struct.StructSerializable;
+import frc.robot.extras.util.ProceduralStructGenerator;
+import frc.robot.extras.util.ProceduralStructGenerator.FixedSizeArray;
+
+public record SwerveSetpoint(
+ ChassisSpeeds chassisSpeeds,
+ @FixedSizeArray(size = 4) AdvancedSwerveModuleState[] moduleStates) //
+ implements StructSerializable {
+ public static SwerveSetpoint zeroed() {
+ return new SwerveSetpoint(
+ new ChassisSpeeds(),
+ new AdvancedSwerveModuleState[] {
+ new AdvancedSwerveModuleState(0, Rotation2d.kZero, 0, 0),
+ new AdvancedSwerveModuleState(0, Rotation2d.kZero, 0, 0),
+ new AdvancedSwerveModuleState(0, Rotation2d.kZero, 0, 0),
+ new AdvancedSwerveModuleState(0, Rotation2d.kZero, 0, 0)
+ });
+ }
+
+ public static final Struct struct;
+
+ static {
+ struct = ProceduralStructGenerator.genRecord(SwerveSetpoint.class);
+ }
+}
diff --git a/src/main/java/frc/robot/extras/setpointGen/SwerveSetpointGenerator.java b/src/main/java/frc/robot/extras/setpointGen/SwerveSetpointGenerator.java
new file mode 100644
index 0000000..3f55029
--- /dev/null
+++ b/src/main/java/frc/robot/extras/setpointGen/SwerveSetpointGenerator.java
@@ -0,0 +1,363 @@
+package frc.robot.extras.setpointGen;
+
+import static frc.robot.extras.setpointGen.SPGCalcs.*;
+
+import edu.wpi.first.math.geometry.Rotation2d;
+import edu.wpi.first.math.geometry.Translation2d;
+import edu.wpi.first.math.kinematics.ChassisSpeeds;
+import edu.wpi.first.math.kinematics.SwerveDriveKinematics;
+import edu.wpi.first.math.kinematics.SwerveModuleState;
+import edu.wpi.first.math.system.plant.DCMotor;
+import edu.wpi.first.wpilibj.RobotController;
+
+/**
+ * Swerve setpoint generatoR that has been passed around so many times its hard to keep track, just
+ * know i didn't write most the logic in this code that credit goes to 254 and mjansen
+ *
+ *
Takes a prior setpoint, a desired setpoint, and outputs a new setpoint that respects all the
+ * kinematic constraints on module rotation and wheel velocity/torque, as well as preventing any
+ * forces acting on a module's wheel from exceeding the force of friction.
+ */
+public class SwerveSetpointGenerator {
+ private static final ChassisSpeeds ZERO_SPEEDS = new ChassisSpeeds();
+
+ private static final int NUM_MODULES = SPGCalcs.NUM_MODULES;
+
+ private final SwerveDriveKinematics kinematics;
+ private final Translation2d[] moduleLocations;
+ private final DCMotor driveMotor;
+ private final double driveCurrentLimitAmps,
+ maxDriveVelocityMPS,
+ maxSteerVelocityRadsPerSec,
+ massKg,
+ moiKgMetersSquared,
+ wheelRadiusMeters,
+ wheelFrictionForce,
+ // maxTorqueFriction,
+ torqueLoss;
+
+ public SwerveSetpointGenerator(
+ final Translation2d[] moduleLocations,
+ final DCMotor driveMotor,
+ final DCMotor angleMotor,
+ final double driveCurrentLimitAmps,
+ final double massKg,
+ final double moiKgMetersSquared,
+ final double wheelDiameterMeters,
+ final double wheelCoF,
+ final double torqueLoss) {
+
+ if (moduleLocations.length != NUM_MODULES) {
+ throw new IllegalArgumentException("Module locations must have 4 elements");
+ }
+
+ this.driveMotor = driveMotor;
+ this.driveCurrentLimitAmps = driveCurrentLimitAmps;
+ this.maxSteerVelocityRadsPerSec = angleMotor.freeSpeedRadPerSec;
+ kinematics = new SwerveDriveKinematics(moduleLocations);
+ this.moduleLocations = moduleLocations;
+ this.massKg = massKg;
+ this.moiKgMetersSquared = moiKgMetersSquared;
+ this.wheelRadiusMeters = wheelDiameterMeters / 2;
+ this.maxDriveVelocityMPS = driveMotor.freeSpeedRadPerSec * wheelRadiusMeters;
+
+ wheelFrictionForce = wheelCoF * ((massKg / 4) * 9.81);
+ // maxTorqueFriction = this.wheelFrictionForce * wheelRadiusMeters;
+ this.torqueLoss = torqueLoss;
+ }
+
+ // alot of work was done to reduce allocations in this hot loop,
+ // migrating everything over to a vars object that gets reused
+ // was the best way to do this.
+ private static final LocalVars VARS_TEMPLATE = new LocalVars();
+
+ /**
+ * Generate a new setpoint. Note: Do not discretize ChassisSpeeds passed into or returned from
+ * this method. This method will discretize the speeds for you.
+ *
+ * @param prevSetpoint The previous setpoint motion. Normally, you'd pass in the previous
+ * iteration setpoint instead of the actual measured/estimated kinematic state.
+ * @param desiredRobotRelativeSpeeds The desired state of motion, such as from the driver sticks
+ * or a path following algorithm.
+ * @param dt The loop time.
+ * @return A Setpoint object that satisfies all the kinematic/friction limits while converging to
+ * desiredState quickly.
+ */
+ public SwerveSetpoint generateSetpoint(
+ final SwerveSetpoint prevSetpoint, ChassisSpeeds desiredRobotRelativeSpeeds, double dt) {
+ // https://github.com/wpilibsuite/allwpilib/issues/7332
+ SwerveModuleState[] desiredModuleStates =
+ kinematics.toSwerveModuleStates(desiredRobotRelativeSpeeds);
+ // Make sure desiredState respects velocity limits.
+ SwerveDriveKinematics.desaturateWheelSpeeds(desiredModuleStates, maxDriveVelocityMPS);
+ desiredRobotRelativeSpeeds = kinematics.toChassisSpeeds(desiredModuleStates);
+
+ final LocalVars vars = VARS_TEMPLATE.reset();
+ vars.dt = dt;
+ vars.prevSpeeds = prevSetpoint.chassisSpeeds();
+ vars.desiredSpeeds = desiredRobotRelativeSpeeds;
+ vars.desiredModuleStates = desiredModuleStates;
+ vars.prevModuleStates = prevSetpoint.moduleStates();
+ vars.dx =
+ desiredRobotRelativeSpeeds.vxMetersPerSecond
+ - prevSetpoint.chassisSpeeds().vxMetersPerSecond;
+ vars.dy =
+ desiredRobotRelativeSpeeds.vyMetersPerSecond
+ - prevSetpoint.chassisSpeeds().vyMetersPerSecond;
+ vars.dtheta =
+ desiredRobotRelativeSpeeds.omegaRadiansPerSecond
+ - prevSetpoint.chassisSpeeds().omegaRadiansPerSecond;
+ vars.minS = 1.0;
+
+ checkNeedToSteer(vars);
+ makeVectors(vars);
+
+ // if (vars.allModulesShouldFlip
+ // && !epsilonEquals(prevSetpoint.chassisSpeeds(), ZERO_SPEEDS)
+ // && !epsilonEquals(desiredRobotRelativeSpeeds, ZERO_SPEEDS)) {
+ // // It will (likely) be faster to stop the robot, rotate the modules in place to the
+ // complement
+ // // of the desired angle, and accelerate again.
+ // return generateSetpoint(prevSetpoint, ZERO_SPEEDS, dt);
+ // }
+
+ solveSteering(vars);
+
+ solveDriving(vars);
+
+ ChassisSpeeds retSpeeds =
+ new ChassisSpeeds(
+ vars.prevSpeeds.vxMetersPerSecond + vars.minS * vars.dx,
+ vars.prevSpeeds.vyMetersPerSecond + vars.minS * vars.dy,
+ vars.prevSpeeds.omegaRadiansPerSecond + vars.minS * vars.dtheta);
+ retSpeeds.discretize(retSpeeds, dt);
+
+ double chassisAccelX = (retSpeeds.vxMetersPerSecond - vars.prevSpeeds.vxMetersPerSecond) / dt;
+ double chassisAccelY = (retSpeeds.vyMetersPerSecond - vars.prevSpeeds.vyMetersPerSecond) / dt;
+ double angularAccel =
+ (retSpeeds.omegaRadiansPerSecond - vars.prevSpeeds.omegaRadiansPerSecond) / dt;
+
+ SwerveModuleState[] retStates = kinematics.toSwerveModuleStates(retSpeeds);
+ SwerveModuleState[] accelStates =
+ kinematics.toSwerveModuleStates(
+ new ChassisSpeeds(chassisAccelX, chassisAccelY, angularAccel));
+
+ AdvancedSwerveModuleState[] outputStates = new AdvancedSwerveModuleState[NUM_MODULES];
+ for (int m = 0; m < NUM_MODULES; m++) {
+ retStates[m].optimize(vars.prevModuleStates[m].angle);
+ double steerVelocity =
+ (vars.prevModuleStates[m].angle.getRadians() - retStates[m].angle.getRadians()) / dt;
+ outputStates[m] =
+ new AdvancedSwerveModuleState(
+ retStates[m].speedMetersPerSecond,
+ retStates[m].angle,
+ steerVelocity,
+ accelStates[m].speedMetersPerSecond);
+ }
+
+ // log("output",
+ return new SwerveSetpoint(retSpeeds, outputStates);
+ }
+
+ public SwerveSetpoint generateSimpleSetpoint(
+ final SwerveSetpoint prevSetpoint, ChassisSpeeds desiredRobotRelativeSpeeds, double dt) {
+ AdvancedSwerveModuleState[] outputStates = new AdvancedSwerveModuleState[NUM_MODULES];
+ SwerveModuleState[] desiredModuleStates =
+ kinematics.toSwerveModuleStates(desiredRobotRelativeSpeeds);
+ SwerveDriveKinematics.desaturateWheelSpeeds(desiredModuleStates, maxDriveVelocityMPS);
+ for (int m = 0; m < NUM_MODULES; m++) {
+ desiredModuleStates[m].optimize(prevSetpoint.moduleStates()[m].angle);
+ outputStates[m] = AdvancedSwerveModuleState.fromBase(desiredModuleStates[m]);
+ }
+
+ return new SwerveSetpoint(kinematics.toChassisSpeeds(desiredModuleStates), outputStates);
+ }
+
+ private static void checkNeedToSteer(LocalVars vars) {
+ if (epsilonEquals(vars.desiredSpeeds, ZERO_SPEEDS)) {
+ vars.needToSteer = false;
+ for (int m = 0; m < NUM_MODULES; m++) {
+ vars.desiredModuleStates[m].angle = vars.prevModuleStates[m].angle;
+ vars.desiredModuleStates[m].speedMetersPerSecond = 0.0;
+ }
+ }
+ }
+
+ private static double rotateBy(double rad, double otherCos, double otherSin) {
+ return Math.atan2(
+ Math.sin(rad) * otherCos + Math.cos(rad) * otherSin,
+ Math.cos(rad) * otherCos - Math.sin(rad) * otherSin);
+ }
+
+ private static double requiredRotation(double prevRadians, double desiredRads) {
+ // this looks messy without using Rotation2d methods.
+ // this is roughly equivalent to:
+ //
+ // double r = vars.prev[m].rot2d().unaryMinus()
+ // .rotateBy(vars.desired[m].rot2d()).getRadians();
+ double unaryMinusPrevRads = -prevRadians;
+ return rotateBy(unaryMinusPrevRads, Math.cos(desiredRads), Math.sin(desiredRads));
+ }
+
+ private static void makeVectors(LocalVars vars) {
+ for (int m = 0; m < NUM_MODULES; m++) {
+ vars.prev[m].applyModuleState(vars.prevModuleStates[m]);
+ vars.desired[m].applyModuleState(vars.desiredModuleStates[m]);
+ if (vars.allModulesShouldFlip) {
+ double requiredRots = requiredRotation(vars.prev[m].radians(), vars.desired[m].radians());
+ if (flipHeading(requiredRots)) {
+ vars.allModulesShouldFlip = false;
+ }
+ }
+ }
+ }
+
+ private void solveSteering(LocalVars vars) {
+ // In cases where an individual module is stopped, we want to remember the right steering angle
+ // to command (since inverse kinematics doesn't care about angle, we can be opportunistically
+ // lazy).
+ // Enforce steering velocity limits. We do this by taking the derivative of steering angle at
+ // the current angle, and then backing out the maximum interpolant between start and goal
+ // states. We remember the minimum across all modules, since that is the active constraint.
+ for (int m = 0; m < NUM_MODULES; m++) {
+ if (!vars.needToSteer) {
+ vars.steeringOverride[m] = vars.prevModuleStates[m].angle;
+ continue;
+ }
+
+ double maxThetaStep = vars.dt * maxSteerVelocityRadsPerSec;
+
+ if (epsilonEquals(vars.prevModuleStates[m].speedMetersPerSecond, 0.0)) {
+ // If module is stopped, we know that we will need to move straight to the final steering
+ // angle, so limit based purely on rotation in place.
+ if (epsilonEquals(vars.desiredModuleStates[m].speedMetersPerSecond, 0.0)) {
+ // Goal angle doesn't matter. Just leave module at its current angle.
+ vars.steeringOverride[m] = vars.prevModuleStates[m].angle;
+ continue;
+ }
+
+ double requiredRots = requiredRotation(vars.prev[m].radians(), vars.desired[m].radians());
+ if (flipHeading(requiredRots)) {
+ requiredRots =
+ rotateBy(requiredRots, Rotation2d.k180deg.getCos(), Rotation2d.k180deg.getSin());
+ }
+
+ // radians bounds to +/- Pi.
+ final double numStepsNeeded = Math.abs(requiredRots) / maxThetaStep;
+
+ if (numStepsNeeded <= 1.0) {
+ // Steer directly to goal angle.
+ vars.steeringOverride[m] = vars.desiredModuleStates[m].angle;
+ } else {
+ // Adjust steering by max_theta_step.
+ // there really is no way to avoid this allocation.
+ Rotation2d adjusted =
+ vars.prevModuleStates[m].angle.rotateBy(
+ Rotation2d.fromRadians(Math.signum(requiredRots) * maxThetaStep));
+ vars.steeringOverride[m] = adjusted;
+ vars.minS = 0.0;
+ }
+ continue;
+ }
+ if (vars.minS == 0.0) {
+ // s can't get any lower. Save some CPU.
+ continue;
+ }
+
+ double maxS =
+ findSteeringMaxS(
+ vars.prev[m].vx,
+ vars.prev[m].vy,
+ vars.prev[m].radians(),
+ vars.desired[m].vx,
+ vars.desired[m].vy,
+ vars.desired[m].radians(),
+ maxThetaStep);
+ vars.minS = Math.min(vars.minS, maxS);
+ }
+ }
+
+ private void solveDriving(LocalVars vars) {
+ // Enforce drive wheel torque limits
+ Translation2d chassisForceVec = Translation2d.kZero;
+ double chassisTorque = 0.0;
+ for (int m = 0; m < NUM_MODULES; m++) {
+ double lastVelRadPerSec = vars.prevModuleStates[m].speedMetersPerSecond / wheelRadiusMeters;
+ // Use the current battery voltage since we won't be able to supply 12v if the
+ // battery is sagging down to 11v, which will affect the max torque output
+ double currentDraw =
+ driveMotor.getCurrent(Math.abs(lastVelRadPerSec), RobotController.getInputVoltage());
+ currentDraw = Math.min(currentDraw, driveCurrentLimitAmps);
+ double moduleTorque = driveMotor.getTorque(currentDraw);
+
+ double prevSpeed = vars.prevModuleStates[m].speedMetersPerSecond;
+ vars.desiredModuleStates[m].optimize(vars.prevModuleStates[m].angle);
+ double desiredSpeed = vars.desiredModuleStates[m].speedMetersPerSecond;
+
+ int forceSign;
+ Rotation2d forceAngle = vars.prevModuleStates[m].angle;
+ if (epsilonEquals(prevSpeed, 0.0)
+ || (prevSpeed > 0 && desiredSpeed >= prevSpeed)
+ || (prevSpeed < 0 && desiredSpeed <= prevSpeed)) {
+ // Torque loss will be fighting motor
+ moduleTorque -= torqueLoss;
+ forceSign = 1; // Force will be applied in direction of module
+ if (prevSpeed < 0) {
+ forceAngle = forceAngle.plus(Rotation2d.k180deg);
+ }
+ } else {
+ // Torque loss will be helping the motor
+ moduleTorque += torqueLoss;
+ forceSign = -1; // Force will be applied in opposite direction of module
+ if (prevSpeed > 0) {
+ forceAngle = forceAngle.plus(Rotation2d.k180deg);
+ }
+ }
+
+ // Limit torque to prevent wheel slip
+ moduleTorque = Math.min(moduleTorque, wheelFrictionForce * wheelRadiusMeters);
+
+ double forceAtCarpet = moduleTorque / wheelRadiusMeters;
+ Translation2d moduleForceVec = new Translation2d(forceAtCarpet * forceSign, forceAngle);
+
+ // Add the module force vector to the chassis force vector
+ chassisForceVec = chassisForceVec.plus(moduleForceVec);
+
+ // Calculate the torque this module will apply to the chassis
+ Rotation2d angleToModule = moduleLocations[m].getAngle();
+ Rotation2d theta = moduleForceVec.getAngle().minus(angleToModule);
+ chassisTorque += forceAtCarpet * moduleLocations[m].getNorm() * theta.getSin();
+ }
+
+ Translation2d chassisAccelVec = chassisForceVec.div(massKg);
+ double chassisAngularAccel = chassisTorque / moiKgMetersSquared;
+
+ // Use kinematics to convert chassis accelerations to module accelerations
+ ChassisSpeeds chassisAccel =
+ new ChassisSpeeds(chassisAccelVec.getX(), chassisAccelVec.getY(), chassisAngularAccel);
+ var accelStates = kinematics.toSwerveModuleStates(chassisAccel);
+
+ for (int m = 0; m < NUM_MODULES; m++) {
+ if (vars.minS == 0.0) {
+ // No need to carry on.
+ break;
+ }
+
+ double maxVelStep = Math.abs(accelStates[m].speedMetersPerSecond * vars.dt);
+
+ double vxMinS;
+ double vyMinS;
+ if (vars.minS == 1.0) {
+ vxMinS = vars.desired[m].vx;
+ vyMinS = vars.desired[m].vy;
+ } else {
+ vxMinS = (vars.desired[m].vx - vars.prev[m].vx) * vars.minS + vars.prev[m].vx;
+ vyMinS = (vars.desired[m].vy - vars.prev[m].vy) * vars.minS + vars.prev[m].vy;
+ }
+ // Find the max s for this drive wheel. Search on the interval between 0 and min_s, because we
+ // already know we can't go faster than that.
+ double s = findDriveMaxS(vars.prev[m].vx, vars.prev[m].vy, vxMinS, vyMinS, maxVelStep);
+ vars.minS = Math.min(vars.minS, s);
+ }
+ }
+}
diff --git a/src/main/java/frc/robot/extras/simulation/field/SimulatedField.java b/src/main/java/frc/robot/extras/simulation/field/SimulatedField.java
index d991fa8..e73bc04 100644
--- a/src/main/java/frc/robot/extras/simulation/field/SimulatedField.java
+++ b/src/main/java/frc/robot/extras/simulation/field/SimulatedField.java
@@ -3,12 +3,17 @@
import edu.wpi.first.math.geometry.Pose2d;
import edu.wpi.first.math.geometry.Pose3d;
import edu.wpi.first.math.geometry.Translation2d;
+import edu.wpi.first.wpilibj.TimedRobot;
+import edu.wpi.first.wpilibj.simulation.BatterySim;
+import edu.wpi.first.wpilibj.simulation.RoboRioSim;
import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard;
import frc.robot.extras.simulation.gamePiece.CrescendoNoteSimulation;
import frc.robot.extras.simulation.gamePiece.GamePieceSimulation;
import frc.robot.extras.simulation.mechanismSim.IntakeSimulation;
import frc.robot.extras.simulation.mechanismSim.swerve.AbstractDriveTrainSimulation;
+import frc.robot.extras.simulation.mechanismSim.swerve.BrushlessMotorSim;
import frc.robot.extras.util.GeomUtil;
+// import java.lang.ref.WeakReference;
import java.util.*;
import org.dyn4j.dynamics.Body;
import org.dyn4j.dynamics.BodyFixture;
@@ -125,6 +130,7 @@ public static void overrideSimulationTimings(
protected final Set driveTrainSimulations;
protected final Set gamePieces;
protected final List simulationSubTickActions;
+ protected final List motors;
private final List intakeSimulations;
/**
@@ -149,6 +155,7 @@ protected SimulatedField(FieldMap obstaclesMap) {
simulationSubTickActions = new ArrayList<>();
this.gamePieces = new HashSet<>();
this.intakeSimulations = new ArrayList<>();
+ motors = new ArrayList<>();
}
/**
@@ -276,6 +283,10 @@ public void simulationPeriodic() {
"MapleArenaSimulation/Dyn4jEngineCPUTimeMS", (System.nanoTime() - t0) / 1000000.0);
}
+ public void addMotor(BrushlessMotorSim motor) {
+ motors.add(motor);
+ }
+
/**
*
*
@@ -293,6 +304,17 @@ public void simulationPeriodic() {
*
*/
private void simulationSubTick() {
+ ArrayList motorCurrents = new ArrayList<>();
+ for (var motor : motors) {
+ BrushlessMotorSim motorRef = motor;
+ if (motorRef != null) {
+ motorRef.update();
+ }
+ }
+ double vin =
+ BatterySim.calculateLoadedBatteryVoltage(
+ 12.2, 0.015, motorCurrents.stream().mapToDouble(Double::doubleValue).toArray());
+ RoboRioSim.setVInVoltage(vin);
for (AbstractDriveTrainSimulation driveTrainSimulation : driveTrainSimulations)
driveTrainSimulation.simulationSubTick();
diff --git a/src/main/java/frc/robot/extras/simulation/mechanismSim/swerve/BrushlessMotorSim.java b/src/main/java/frc/robot/extras/simulation/mechanismSim/swerve/BrushlessMotorSim.java
index 5ba3122..76f1e64 100644
--- a/src/main/java/frc/robot/extras/simulation/mechanismSim/swerve/BrushlessMotorSim.java
+++ b/src/main/java/frc/robot/extras/simulation/mechanismSim/swerve/BrushlessMotorSim.java
@@ -1,41 +1,41 @@
-// A drop-in replacement for WPILib's DCMotorSim to simulate modern brushless motors
-// Copyright (C) 2024 Team-5516-"Iron Maple"
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
-// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in
-// all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-// SOFTWARE.
-//
-// Original source:
-// https://github.com/Shenzhen-Robotics-Alliance/maple-sim/blob/main/src/main/java/org/ironmaple/simulation/drivesims/BrushlessMotorSim.java
-
package frc.robot.extras.simulation.mechanismSim.swerve;
+import static edu.wpi.first.units.Units.Amps;
+import static edu.wpi.first.units.Units.KilogramSquareMeters;
+import static edu.wpi.first.units.Units.NewtonMeters;
+import static edu.wpi.first.units.Units.Radians;
+import static edu.wpi.first.units.Units.RadiansPerSecond;
+import static edu.wpi.first.units.Units.RadiansPerSecondPerSecond;
+import static edu.wpi.first.units.Units.Volts;
+
import edu.wpi.first.math.MathUtil;
+import edu.wpi.first.math.controller.PIDController;
+import edu.wpi.first.math.controller.SimpleMotorFeedforward;
import edu.wpi.first.math.system.plant.DCMotor;
+import edu.wpi.first.math.system.plant.LinearSystemId;
+import edu.wpi.first.units.AngleUnit;
+import edu.wpi.first.units.AngularAccelerationUnit;
+import edu.wpi.first.units.AngularVelocityUnit;
+import edu.wpi.first.units.CurrentUnit;
+import edu.wpi.first.units.PerUnit;
+import edu.wpi.first.units.VoltageUnit;
+import edu.wpi.first.units.measure.Angle;
+import edu.wpi.first.units.measure.AngularAcceleration;
+import edu.wpi.first.units.measure.AngularVelocity;
+import edu.wpi.first.units.measure.Current;
+import edu.wpi.first.units.measure.MomentOfInertia;
+import edu.wpi.first.units.measure.Per;
+import edu.wpi.first.units.measure.Torque;
+import edu.wpi.first.units.measure.Voltage;
+import edu.wpi.first.wpilibj.RobotController;
import edu.wpi.first.wpilibj.simulation.DCMotorSim;
-import edu.wpi.first.wpilibj.simulation.RoboRioSim;
+import frc.robot.extras.simulation.field.SimulatedField;
/**
*
*
*
{@link DCMotorSim} with a bit of extra spice.
*
- *
By Team 5516 "IRON MAPLE".
- *
*
This class extends the functionality of the original {@link DCMotorSim} and models the
* following aspects in addition:
*
@@ -43,16 +43,47 @@
*
Friction force on the rotor.
*
Smart current limiting.
*
Brake and coast modes (only for simulating brushless motors).
- *
Simulated encoder readings.
*
*/
public class BrushlessMotorSim {
+ public enum OutputType {
+ VOLTAGE,
+ CURRENT
+ }
+
+ public enum OutputMode {
+ VELOCITY,
+ POSITION,
+ OPEN_LOOP
+ }
+
+ /** The Constants for the motor */
private final DCMotor motor;
- private final double gearRatio, frictionTorque, loadInertiaJKgMetersSquared;
- private double requestedVoltageOutput, currentLimitAmps;
- private boolean breakModeEnabled;
- private double angularPositionRad, angularVelocityRadPerSec, appliedVolts, currentDrawAmps;
+ /** The dynamics simulation for the motor */
+ private final DCMotorSim sim;
+
+ /** The gear ratio, value above 1.0 are a reduction */
+ private final double gearing;
+
+ /** The voltage required to overcome friction */
+ private final Voltage frictionVoltage;
+
+ private final PIDController poseVoltController = new PIDController(0, 0, 0);
+ private final PIDController veloVoltController = new PIDController(0, 0, 0);
+ private final PIDController poseCurrentController = new PIDController(0, 0, 0);
+ private final PIDController veloCurrentController = new PIDController(0, 0, 0);
+
+ private SimpleMotorFeedforward feedforward = new SimpleMotorFeedforward(0, 0, 0);
+
+ private Current currentLimit = Amps.of(300.0);
+
+ private OutputType outputType = OutputType.VOLTAGE;
+ private OutputMode outputMode = OutputMode.OPEN_LOOP;
+ private double output = 0.0;
+
+ private Angle forwardLimit = Radians.of(Double.POSITIVE_INFINITY);
+ private Angle reverseLimit = Radians.of(Double.NEGATIVE_INFINITY);
/**
*
@@ -61,283 +92,413 @@ public class BrushlessMotorSim {
*
* @param motor the {@link DCMotor} model representing the motor(s) in the simulation
* @param gearRatio the gear ratio of the mechanism; values greater than 1 indicate a reduction
- * @param loadIntertiaJKgMetersSquared the rotational inertia of the mechanism, in kg·m²
- * @param frictionVoltage the voltage required to overcome friction and make the mechanism move
+ * @param loadIntertia the rotational inertia of the mechanism
+ * @param frictionVoltage the voltage required to keep the motor moving at a constant velocity
*/
public BrushlessMotorSim(
+ SimulatedField arena,
DCMotor motor,
double gearRatio,
- double loadIntertiaJKgMetersSquared,
- double frictionVoltage) {
+ MomentOfInertia loadIntertia,
+ Voltage frictionVoltage) {
+ this.sim =
+ new DCMotorSim(
+ LinearSystemId.createDCMotorSystem(
+ motor, loadIntertia.in(KilogramSquareMeters), gearRatio),
+ motor);
this.motor = motor;
- this.gearRatio = gearRatio;
- this.frictionTorque = motor.getTorque(motor.getCurrent(0, frictionVoltage)) * gearRatio;
-
- this.loadInertiaJKgMetersSquared = loadIntertiaJKgMetersSquared;
- disableCurrentLimit();
- this.breakModeEnabled = true;
+ this.gearing = gearRatio;
+ this.frictionVoltage = frictionVoltage;
- this.angularPositionRad = angularVelocityRadPerSec = appliedVolts = currentDrawAmps = 0;
+ arena.addMotor(this);
}
/**
+ * Configures the angle of the motor.
*
- *
- *
Requests an Output Voltage for the Motor.
- *
- *
Note: The requested voltage may not always be fully applied due to current
- * limits. For details on current limiting, refer to {@link #enableCurrentLimit(double)}.
- *
- * @param volts the requested output voltage for the motor
+ * @param angle the angle of the motor
+ * @return this instance for method chaining
*/
- public void requestVoltageOutput(double volts) {
- this.requestedVoltageOutput = volts;
+ public BrushlessMotorSim withOverrideAngle(Angle angle) {
+ sim.setAngle(angle.in(Radians));
+ return this;
}
/**
+ * Configures the angular velocity of the motor.
*
- *
- *
Configures a Current Limit for the Motor.
- *
- *
If the motor's supply current exceeds the specified limit, the motor will reduce its output
- * to prevent exceeding the limit.
- *
- * @param currentLimitAmps the maximum allowed current, in amperes
+ * @param angularVelocity the angular velocity of the motor
+ * @return this instance for method chaining
*/
- public void enableCurrentLimit(double currentLimitAmps) {
- this.currentLimitAmps = currentLimitAmps;
+ public BrushlessMotorSim withOverrideAngularVelocity(AngularVelocity angularVelocity) {
+ sim.setAngularVelocity(angularVelocity.in(RadiansPerSecond));
+ return this;
+ }
+
+ public BrushlessMotorSim withFeedForward(
+ Voltage kS,
+ Per kV,
+ Per kA) {
+ var kVUnit = PerUnit.combine(Volts, RadiansPerSecond);
+ var kAUnit = PerUnit.combine(Volts, RadiansPerSecondPerSecond);
+ feedforward =
+ new SimpleMotorFeedforward(
+ kS.in(Volts), kV.in(kVUnit), kA.in(kAUnit), SimulatedField.getSimulationDt());
+ return this;
}
/**
+ * Configures the PD controller for Positional Requests using {@link OutputType#VOLTAGE}.
*
+ *
This is unit safe and can be configure like so:
*
- *
Disables the Current Limit of the Motor.
+ *
+ * // Volts per Rotation of error is how CTRE handles PID when used with voltage requests
+ * sim.withPositionalVoltageController(
+ * Volts.per(Rotation).ofNative(100.0),
+ * Volts.per(RotationsPerSecond).ofNative(5.0)
+ * );
+ *
*
- *
This method removes the current limit, allowing the motor to operate without any current
- * restrictions.
+ * @param kP the proportional gain
+ * @param kD the derivative gain
+ * @return this instance for method chaining
*/
- public void disableCurrentLimit() {
- this.currentLimitAmps = Double.POSITIVE_INFINITY;
+ public BrushlessMotorSim withPositionalVoltageController(
+ Per kP, Per kD) {
+ var kPUnit = PerUnit.combine(Volts, Radians);
+ var kDUnit = PerUnit.combine(Volts, RadiansPerSecond);
+ poseVoltController.setP(kP.in(kPUnit));
+ poseVoltController.setD(kD.in(kDUnit));
+ return this;
}
/**
+ * Configures the PD controller for Velocity Requests using {@link OutputType#VOLTAGE}.
*
+ *
This is unit safe and can be configure like so:
*
- *
Configures the Zero Power Behavior of the Motor.
- *
- *
This method sets the motor's zero power behavior, similar to the
- * setZeroPowerBehavior() method for brushless motors.
+ *
+ * // Volts per RPS of error is how CTRE handles PID when used with voltage requests
+ * sim.withVelocityVoltageController(
+ * Volts.per(RotationsPerSecond).ofNative(0.4)
+ * );
+ *
*
- *
Use this feature only when simulating brushless motors.
- *
- * @param enabled true to enable brake mode, false to enable coast mode
+ * @param kP the proportional gain
+ * @return this instance for method chaining
*/
- public void setMotorBrakeEnabled(boolean enabled) {
- this.breakModeEnabled = enabled;
+ public BrushlessMotorSim withVelocityVoltageController(Per kP) {
+ var kPUnit = PerUnit.combine(Volts, Radians);
+ veloVoltController.setP(kP.in(kPUnit));
+ return this;
}
/**
+ * Configures the PD controller for Positional Requests using {@link OutputType#CURRENT}.
*
+ *
This is unit safe and can be configure like so:
*
- *
Sets the Current State of the Motor.
- *
- *
This method instantly shifts the motor to a given state, setting its angular position and
- * velocity.
+ *
+ * // Amps per Rotation of error is how CTRE handles PID when used with current requests
+ * sim.withPositionalCurrentController(
+ * Amps.per(Rotation).ofNative(100.0),
+ * Amps.per(RotationsPerSecond).ofNative(5.0)
+ * );
+ *
*
- *
Equivalent to the {@link DCMotorSim#setState(double, double)} method.
- *
- * @param angularPositionRad the angular position of the motor, in radians
- * @param angularVelocityRadPerSec the angular velocity of the motor, in radians per second
+ * @param kP the proportional gain
+ * @param kD the derivative gain
+ * @return this instance for method chaining
*/
- public void setState(double angularPositionRad, double angularVelocityRadPerSec) {
- this.angularPositionRad = angularPositionRad;
- this.angularVelocityRadPerSec = angularVelocityRadPerSec;
+ public BrushlessMotorSim withPositionalCurrentController(
+ Per kP, Per kD) {
+ var kPUnit = PerUnit.combine(Amps, Radians);
+ var kDUnit = PerUnit.combine(Amps, RadiansPerSecond);
+ poseCurrentController.setP(kP.in(kPUnit));
+ poseCurrentController.setD(kD.in(kDUnit));
+ return this;
}
/**
+ * Configures the PD controller for Velocity Requests using {@link OutputType#CURRENT}.
*
+ *
This is unit safe and can be configure like so:
*
- *
Updates the Simulation.
- *
- *
This method steps the motor simulation forward by a given time interval (dt),
- * recalculating and storing the states of the motor.
+ *
+ * // Amps per RPS of error is how CTRE handles PID when used with current requests
+ * sim.withVelocityCurrentController(
+ * Amps.per(RotationsPerSecond).ofNative(0.4)
+ * );
+ *
*
- *
Equivalent to {@link DCMotorSim#update(double)}.
- *
- * @param dtSeconds the time step for the simulation, in seconds
+ * @param kP the proportional gain
+ * @return this instance for method chaining
*/
- public void update(double dtSeconds) {
- appliedVolts =
- constrainOutputVoltage(
- motor, angularVelocityRadPerSec * gearRatio, currentLimitAmps, requestedVoltageOutput);
-
- double totalTorque = getMotorElectricTorque();
-
- /* apply friction force */
- final boolean electricTorqueResultingInAcceleration =
- totalTorque * angularVelocityRadPerSec > 0;
- if (electricTorqueResultingInAcceleration)
- totalTorque = MathUtil.applyDeadband(totalTorque, frictionTorque, Double.POSITIVE_INFINITY);
- else totalTorque += getCurrentFrictionTorque();
-
- this.angularVelocityRadPerSec += totalTorque / loadInertiaJKgMetersSquared * dtSeconds;
- this.angularPositionRad += this.angularVelocityRadPerSec * dtSeconds;
+ public BrushlessMotorSim withVelocityCurrentController(Per kP) {
+ var kPUnit = PerUnit.combine(Amps, Radians);
+ veloCurrentController.setP(kP.in(kPUnit));
+ return this;
}
/**
+ * Configures the positionaly controllers to use continuous wrap.
*
- *
- *
Calculates the Electric Torque on the Rotor.
- *
- * @return the torque applied to the mechanism by the motor's electrical output
+ * @param min the minimum angle
+ * @param max the maximum angle
+ * @return this instance for method chaining
+ * @see PIDController#enableContinuousInput(double, double)
*/
- private double getMotorElectricTorque() {
- if (!breakModeEnabled && appliedVolts == 0) return currentDrawAmps = 0;
- currentDrawAmps = motor.getCurrent(angularVelocityRadPerSec * gearRatio, appliedVolts);
- return motor.getTorque(currentDrawAmps) * gearRatio;
+ public BrushlessMotorSim withControllerContinousInput(Angle min, Angle max) {
+ poseVoltController.enableContinuousInput(min.in(Radians), max.in(Radians));
+ poseCurrentController.enableContinuousInput(min.in(Radians), max.in(Radians));
+ return this;
}
/**
+ * Configures the current limit for the motor.
*
+ *
This is the total current limit for the sim
*
- *
Calculates the Dynamic Friction Torque on the Mechanism.
- *
- *
This method simulates the amount of dynamic friction acting on the rotor when the mechanism
- * is rotating.
- *
- *
In the real world, dynamic friction torque is typically constant. However, in the
- * simulation, it is made proportional to the rotor speed when the speed is small to avoid
- * oscillation.
- *
- * @return the amount of dynamic friction torque on the mechanism, in Newton-meters
+ * @param currentLimit the current limit for the motor
+ * @return
*/
- private double getCurrentFrictionTorque() {
- final double kFriction = 3.0,
- percentAngularVelocity =
- Math.abs(angularVelocityRadPerSec) * gearRatio / motor.freeSpeedRadPerSec,
- currentFrictionTorqueMagnitude =
- Math.min(percentAngularVelocity * kFriction * frictionTorque, frictionTorque);
- return Math.copySign(currentFrictionTorqueMagnitude, -angularVelocityRadPerSec);
+ public BrushlessMotorSim withStatorCurrentLimit(Current currentLimit) {
+ // this is a limit across the sum of all motors output,
+ // so it should be set to the total current limit of the mechanism
+ this.currentLimit = currentLimit;
+ return this;
}
/**
+ * Configures the hard limits for the motor.
*
- *
- *
Constrains the Output Voltage of the Motor.
- *
- *
This method constrains the output voltage of the motor to ensure it operates within the
- * current limit and does not exceed the available voltage from the robot's system.
- *
- *
The output voltage is constrained such that the motor does not function outside of the
- * specified current limit. Additionally, it ensures that the voltage does not exceed the robot's
- * input voltage, which can be obtained from {@link RoboRioSim#getVInVoltage()}.
- *
- * @param motor the {@link DCMotor} that models the motor
- * @param motorCurrentVelocityRadPerSec the current velocity of the motor's rotor (geared), in
- * radians per second
- * @param currentLimitAmps the configured current limit, in amperes. You can configure the current
- * limit using {@link #enableCurrentLimit(double)}.
- * @param requestedOutputVoltage the requested output voltage
- * @return the constrained output voltage based on the current limit and system voltage
+ * @param forwardLimit the forward limit
+ * @param reverseLimit the reverse limit
+ * @return this instance for method chaining
*/
- public static double constrainOutputVoltage(
- DCMotor motor,
- double motorCurrentVelocityRadPerSec,
- double currentLimitAmps,
- double requestedOutputVoltage) {
- final double currentAtRequestedVolts =
- motor.getCurrent(motorCurrentVelocityRadPerSec, requestedOutputVoltage);
+ public BrushlessMotorSim withHardLimits(Angle forwardLimit, Angle reverseLimit) {
+ this.forwardLimit = forwardLimit;
+ this.reverseLimit = reverseLimit;
+ return this;
+ }
- /* normally, motor controller starts cutting the supply voltage when the current exceed 120% the current limit */
- final boolean currentTooHigh = Math.abs(currentAtRequestedVolts) > 1.2 * currentLimitAmps;
- double limitedVoltage = requestedOutputVoltage;
- if (currentTooHigh) {
- final double limitedCurrent = Math.copySign(currentLimitAmps, currentAtRequestedVolts);
- limitedVoltage =
- motor.getVoltage(motor.getTorque(limitedCurrent), motorCurrentVelocityRadPerSec);
+ public MomentOfInertia getMOI() {
+ return KilogramSquareMeters.of(sim.getJKgMetersSquared());
+ }
+
+ public Angle getPosition() {
+ return Radians.of(sim.getAngularPositionRad());
+ }
+
+ public AngularVelocity getVelocity() {
+ return RadiansPerSecond.of(sim.getAngularVelocityRadPerSec());
+ }
+
+ public AngularAcceleration getAcceleration() {
+ return RadiansPerSecondPerSecond.of(sim.getAngularAccelerationRadPerSecSq());
+ }
+
+ public Current getStatorCurrentDraw() {
+ return Amps.of(sim.getCurrentDrawAmps());
+ }
+
+ public Current getSupplyCurrent() {
+ // https://www.chiefdelphi.com/t/current-limiting-talonfx-values/374780/10
+ return getStatorCurrentDraw()
+ .times(sim.getInputVoltage() / RobotController.getBatteryVoltage());
+ }
+
+ public Voltage getRotorVoltage() {
+ return Volts.of(sim.getInputVoltage());
+ }
+
+ public Voltage getSupplyVoltage() {
+ return Volts.of(RobotController.getBatteryVoltage());
+ }
+
+ public void setControl(OutputType outputType, AngularVelocity velo) {
+ this.outputType = outputType;
+ this.outputMode = OutputMode.VELOCITY;
+ this.output = velo.in(RadiansPerSecond);
+ }
+
+ public void setControl(OutputType outputType, Angle pos) {
+ this.outputType = outputType;
+ this.outputMode = OutputMode.POSITION;
+ this.output = pos.in(Radians);
+ }
+
+ public void setControl(Current amps) {
+ this.outputType = OutputType.CURRENT;
+ this.outputMode = OutputMode.OPEN_LOOP;
+ this.output = amps.in(Amps);
+ }
+
+ public void setControl(Voltage volts) {
+ this.outputType = OutputType.VOLTAGE;
+ this.outputMode = OutputMode.OPEN_LOOP;
+ this.output = volts.in(Volts);
+ }
+
+ public void setControl() {
+ this.outputType = OutputType.VOLTAGE;
+ this.outputMode = OutputMode.OPEN_LOOP;
+ this.output = 0.0;
+ }
+
+ public void update() {
+ double dtSeconds = SimulatedField.getSimulationDt();
+ switch (this.outputType) {
+ case VOLTAGE -> {
+ switch (this.outputMode) {
+ case OPEN_LOOP -> {
+ driveAtVoltage(Volts.of(output));
+ }
+ case POSITION -> {
+ Voltage voltage =
+ Volts.of(poseVoltController.calculate(getPosition().in(Radians), output));
+ Voltage feedforwardVoltage =
+ Volts.of(
+ feedforward.calculate(
+ getVelocity().in(RadiansPerSecond),
+ velocityForVolts(voltage).in(RadiansPerSecond)));
+ driveAtVoltage(feedforwardVoltage.plus(voltage));
+ }
+ case VELOCITY -> {
+ Voltage voltage =
+ Volts.of(veloVoltController.calculate(getVelocity().in(RadiansPerSecond), output));
+ Voltage feedforwardVoltage =
+ Volts.of(
+ feedforward.calculate(
+ getVelocity().in(RadiansPerSecond),
+ RadiansPerSecond.of(output).in(RadiansPerSecond)));
+ driveAtVoltage(voltage.plus(feedforwardVoltage));
+ }
+ }
+ }
+ case CURRENT -> {
+ switch (this.outputMode) {
+ case OPEN_LOOP -> {
+ sim.setInputVoltage(voltsForAmps(Amps.of(output), getVelocity()).in(Volts));
+ }
+ case POSITION -> {
+ Current current =
+ Amps.of(poseCurrentController.calculate(getPosition().in(Radians), output));
+ Voltage voltage = voltsForAmps(current, getVelocity());
+ Voltage feedforwardVoltage =
+ Volts.of(
+ feedforward.calculate(
+ getVelocity().in(RadiansPerSecond),
+ velocityForVolts(voltage).in(RadiansPerSecond)));
+ driveAtVoltage(feedforwardVoltage.plus(voltage));
+ }
+ case VELOCITY -> {
+ Current current =
+ Amps.of(veloCurrentController.calculate(getPosition().in(Radians), output));
+ Voltage feedforwardVoltage =
+ Volts.of(
+ feedforward.calculate(
+ getVelocity().in(RadiansPerSecond),
+ RadiansPerSecond.of(output).in(RadiansPerSecond)));
+ Voltage voltage = voltsForAmps(current, getVelocity()).plus(feedforwardVoltage);
+ driveAtVoltage(voltage);
+ }
+ }
+ }
}
- if (Math.abs(limitedVoltage) > Math.abs(requestedOutputVoltage))
- limitedVoltage = requestedOutputVoltage;
+ sim.update(dtSeconds);
- return MathUtil.clamp(limitedVoltage, -RoboRioSim.getVInVoltage(), RoboRioSim.getVInVoltage());
+ if (getPosition().lte(reverseLimit)) {
+ sim.setState(reverseLimit.in(Radians), 0.0);
+ } else if (getPosition().gte(forwardLimit)) {
+ sim.setState(forwardLimit.in(Radians), 0.0);
+ }
}
- /**
- *
- *
- *
Obtains the Voltage Actually Applied to the Motor.
- *
- *
This method returns the voltage that is actually applied to the motor after being
- * constrained by {@link #constrainOutputVoltage(DCMotor, double, double, double)}.
- *
- *
The voltage is constrained to ensure that the current limit is not exceeded and that it does
- * not surpass the robot's battery voltage.
- *
- * @return the constrained voltage actually applied to the motor
- */
- public double getAppliedVolts() {
- return appliedVolts;
+ private void driveAtVoltage(Voltage voltage) {
+ // The voltage constrained to current limits and battery voltage
+ Voltage constrained = constrainOutputVoltage(voltage);
+ Voltage frictionVoltage = applyFriction(constrained);
+
+ sim.setInputVoltage(frictionVoltage.in(Volts));
}
- /**
- *
- *
- *
Obtains the Angular Position of the Mechanism.
- *
- *
This method is equivalent to {@link DCMotorSim#getAngularPositionRad()}.
- *
- * @return the final angular position of the mechanism, in radians
- */
- public double getAngularPositionRad() {
- return angularPositionRad;
+ private Voltage applyFriction(Voltage voltage) {
+ // This function is responsible for slowing down acceleration
+ // and slowing down the velocity of the motor when at lowere output.
+ // This is not the same as the static friction, which is the force
+ // required to get the motor moving.
+
+ // to apply friction we convert the motors output to torque then back to voltage
+ double current =
+ motor.getCurrent(sim.getAngularVelocityRadPerSec() * gearing, voltage.in(Volts));
+ double currentVelo = getVelocity().in(RadiansPerSecond) * gearing;
+ double torque = motor.getTorque(current);
+ double friction = frictionTorque().in(NewtonMeters);
+
+ boolean movingForward = currentVelo > 0;
+
+ if (movingForward && currentVelo > motor.getSpeed(torque, sim.getInputVoltage())) {
+ // the motor is moving faster than it should based on the output voltage
+ // apply the friction to slow it down
+ torque -= friction;
+ } else if (!movingForward && currentVelo < motor.getSpeed(torque, sim.getInputVoltage())) {
+ // the motor is moving slower than it should based on the output voltage
+ // apply the friction to speed it up
+ torque += friction;
+ }
+
+ return Volts.of(motor.getVoltage(torque, currentVelo));
}
- /**
- *
- *
- *
Obtains the Angular Position of the Motor.
- *
- * @return the un-geared angular position of the motor (encoder reading), in radians
- */
- public double getEncoderPositionRad() {
- return angularPositionRad * gearRatio;
+ private Voltage voltsForAmps(Current current, AngularVelocity angularVelocity) {
+ // find what voltage is needed to get the current
+ return Volts.of(
+ motor.getVoltage(current.in(Amps), angularVelocity.in(RadiansPerSecond) * gearing));
}
- /**
- *
- *
- *
Obtains the Angular Velocity of the Mechanism.
- *
- *
This method is equivalent to {@link DCMotorSim#getAngularVelocityRadPerSec()}.
- *
- * @return the final angular velocity of the mechanism, in radians per second
- */
- public double getAngularVelocityRadPerSec() {
- return angularVelocityRadPerSec;
+ private AngularVelocity velocityForVolts(Voltage voltage) {
+ return RadiansPerSecond.of(
+ motor.getSpeed(motor.getTorque(getStatorCurrentDraw().in(Amps)), voltage.in(Volts)));
}
- /**
- *
- *
- *
Obtains the Angular Velocity of the Encoder.
- *
- * @return the un-geared angular velocity of the motor (encoder velocity), in radians per second
- */
- public double getEncoderVelocityRadPerSec() {
- return angularVelocityRadPerSec * gearRatio;
+ private Torque frictionTorque() {
+ return NewtonMeters.of(
+ motor.getTorque(motor.getCurrent(0.0, frictionVoltage.in(Volts))) * gearing);
}
- /**
- *
- *
- *
Obtains the Amount of Current Flowing into the Motor.
- *
- *
This method is equivalent to {@link DCMotorSim#getCurrentDrawAmps()}.
- *
- * @return the amount of current flowing into the motor, in amperes
- */
- public double getCurrentDrawAmps() {
- return currentDrawAmps;
+ private Voltage constrainOutputVoltage(Voltage requestedOutput) {
+ final double kCurrentThreshold = 1.2;
+
+ final double motorCurrentVelocityRadPerSec = getVelocity().in(RadiansPerSecond);
+ final double currentLimitAmps = currentLimit.in(Amps);
+ final double requestedOutputVoltage = requestedOutput.in(Volts);
+ final double currentAtRequestedVolts =
+ motor.getCurrent(motorCurrentVelocityRadPerSec, requestedOutputVoltage);
+
+ // Resource for current limiting:
+ // https://file.tavsys.net/control/controls-engineering-in-frc.pdf (sec 12.1.3)
+ final boolean currentTooHigh =
+ Math.abs(currentAtRequestedVolts) > (kCurrentThreshold * currentLimitAmps);
+ double limitedVoltage = requestedOutputVoltage;
+ if (currentTooHigh) {
+ final double limitedCurrent = Math.copySign(currentLimitAmps, currentAtRequestedVolts);
+ limitedVoltage =
+ motor.getVoltage(motor.getTorque(limitedCurrent), motorCurrentVelocityRadPerSec);
+ }
+
+ // ensure the current limit doesn't cause an increase to output voltage
+ if (Math.abs(limitedVoltage) > Math.abs(requestedOutputVoltage)) {
+ limitedVoltage = requestedOutputVoltage;
+ }
+
+ // constrain the output voltage to the battery voltage
+ return Volts.of(
+ MathUtil.clamp(
+ limitedVoltage,
+ -RobotController.getBatteryVoltage(),
+ RobotController.getBatteryVoltage()));
}
}
diff --git a/src/main/java/frc/robot/extras/simulation/mechanismSim/swerve/GyroSimulation.java b/src/main/java/frc/robot/extras/simulation/mechanismSim/swerve/GyroSimulation.java
index 74235ba..0702d5e 100644
--- a/src/main/java/frc/robot/extras/simulation/mechanismSim/swerve/GyroSimulation.java
+++ b/src/main/java/frc/robot/extras/simulation/mechanismSim/swerve/GyroSimulation.java
@@ -170,7 +170,7 @@ public static GyroSimulation createPigeon2() {
* IMU
*/
public static GyroSimulation createNavX2() {
- return new GyroSimulation(2, 0.04);
+ return new GyroSimulation(0.00208333333, 0.04);
}
/**
diff --git a/src/main/java/frc/robot/extras/simulation/mechanismSim/swerve/SwerveModuleSimulation.java b/src/main/java/frc/robot/extras/simulation/mechanismSim/swerve/SwerveModuleSimulation.java
index cd5fa62..5c2c25e 100644
--- a/src/main/java/frc/robot/extras/simulation/mechanismSim/swerve/SwerveModuleSimulation.java
+++ b/src/main/java/frc/robot/extras/simulation/mechanismSim/swerve/SwerveModuleSimulation.java
@@ -1,42 +1,85 @@
package frc.robot.extras.simulation.mechanismSim.swerve;
+import static edu.wpi.first.units.Units.Amps;
+import static edu.wpi.first.units.Units.KilogramSquareMeters;
+import static edu.wpi.first.units.Units.Radians;
+import static edu.wpi.first.units.Units.RadiansPerSecond;
import static edu.wpi.first.units.Units.Volts;
-import static frc.robot.extras.simulation.field.SimulatedField.SIMULATION_DT;
-import static frc.robot.extras.simulation.field.SimulatedField.SIMULATION_SUB_TICKS_IN_1_PERIOD;
import edu.wpi.first.math.MathUtil;
+import edu.wpi.first.math.estimator.SwerveDrivePoseEstimator;
import edu.wpi.first.math.geometry.Rotation2d;
+import edu.wpi.first.math.kinematics.SwerveDriveOdometry;
import edu.wpi.first.math.kinematics.SwerveModuleState;
import edu.wpi.first.math.system.plant.DCMotor;
import edu.wpi.first.math.util.Units;
import edu.wpi.first.units.measure.Voltage;
+import frc.robot.extras.simulation.field.SimulatedField;
+import frc.robot.subsystems.swerve.SwerveConstants.ModuleConstants;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.function.Supplier;
import org.dyn4j.geometry.Vector2;
+/**
+ *
+ *
+ *
Simulation for a Single Swerve Module.
+ *
+ *
This class provides a simulation for a single swerve module in the {@link
+ * SwerveDriveSimulation}.
+ *
+ *
1. Purpose
+ *
+ *
This class serves as the bridge between your code and the physics engine.
+ *
+ *
You will apply voltage outputs to the drive/steer motor of the module and obtain their encoder
+ * readings in your code, just as how you deal with your physical motors.
+ *
+ *
2. Perspectives
+ *
+ *
+ *
Simulates the steering mechanism using a custom brushless motor simulator.
+ *
Simulates the propelling force generated by the driving motor, with a current limit.
+ *
Simulates encoder readings, which can be used to simulate a {@link SwerveDriveOdometry}.
+ *
+ *
+ *
3. Simulating Odometry
+ *
+ *
+ *
Retrieve the encoder readings from {@link #getDriveEncoderUnGearedPositionRad()}} and
+ * {@link #getTurnAbsolutePosition()}.
+ *
Use {@link SwerveDriveOdometry} to estimate the pose of your robot.
+ *
250Hz
+ * Odometry is supported. You can retrive cached encoder readings from every sub-tick
+ * through {@link #getCachedDriveEncoderUnGearedPositionsRad()} and {@link
+ * #getCachedTurnAbsolutePositions()}.
+ *
+ *
+ *
An example of how to simulate odometry using this class is the ModuleIOSim.java
+ * from the Advanced Swerve Drive with maple-sim example.
+ */
public class SwerveModuleSimulation {
- private final DCMotor DRIVE_MOTOR;
+ public final DCMotor DRIVE_MOTOR;
private final BrushlessMotorSim turnMotorSim;
- private final double DRIVE_CURRENT_LIMIT,
+ public final double DRIVE_CURRENT_LIMIT,
DRIVE_GEAR_RATIO,
TURN_GEAR_RATIO,
DRIVE_FRICTION_VOLTAGE,
- TURN_FRICTION_VOLTAGE,
WHEELS_COEFFICIENT_OF_FRICTION,
WHEEL_RADIUS_METERS,
DRIVE_WHEEL_INERTIA = 0.01;
private double driveMotorRequestedVolts = 0.0,
- turnMotorAppliedVolts = 0.0,
driveMotorAppliedVolts = 0.0,
driveMotorSupplyCurrentAmps = 0.0,
- turnMotorSupplyCurrentAmps = 0.0,
turnRelativeEncoderPositionRad = 0.0,
turnRelativeEncoderSpeedRadPerSec = 0.0,
turnAbsoluteEncoderSpeedRadPerSec = 0.0,
driveEncoderUnGearedPositionRad = 0.0,
driveEncoderUnGearedSpeedRadPerSec = 0.0;
- private Rotation2d turnAbsoluteRotation = Rotation2d.fromRotations(Math.random());
+ private Rotation2d turnAbsolutePosition = Rotation2d.fromRotations(Math.random());
private final double turnRelativeEncoderOffSet = (Math.random() - 0.5) * 30;
@@ -44,6 +87,31 @@ public class SwerveModuleSimulation {
cachedDriveEncoderUnGearedPositionsRad;
private final Queue cachedTurnAbsolutePositions;
+ /**
+ *
+ *
+ *
Constructs a Swerve Module Simulation.
+ *
+ *
If you are using {@link SimulatedField#overrideSimulationTimings(double, int)} to use custom
+ * timings, you must call the method before constructing any swerve module simulations using this
+ * constructor.
+ *
+ * @param driveMotor the model of the driving motor
+ * @param turnMotor the model of the steering motor
+ * @param driveCurrentLimit the current limit for the driving motor, in amperes
+ * @param driveGearRatio the gear ratio for the driving motor, >1 is reduction
+ * @param turnGearRatio the gear ratio for the steering motor, >1 is reduction
+ * @param driveFrictionVoltage the measured minimum amount of voltage that can turn the driving
+ * rotter, in volts
+ * @param turnFrictionVoltage the measured minimum amount of voltage that can turn the steering
+ * rotter, in volts
+ * @param tireCoefficientOfFriction the coefficient
+ * of friction of the tires, normally around 1.5
+ * @param wheelsRadiusMeters the radius of the wheels, in meters. Calculate it using {@link
+ * Units#inchesToMeters(double)}.
+ * @param turnRotationalInertia the rotational inertia of the steering mechanism
+ */
public SwerveModuleSimulation(
DCMotor driveMotor,
DCMotor turnMotor,
@@ -60,112 +128,309 @@ public SwerveModuleSimulation(
DRIVE_GEAR_RATIO = driveGearRatio;
TURN_GEAR_RATIO = turnGearRatio;
DRIVE_FRICTION_VOLTAGE = driveFrictionVoltage;
- TURN_FRICTION_VOLTAGE = turnFrictionVoltage;
WHEELS_COEFFICIENT_OF_FRICTION = tireCoefficientOfFriction;
WHEEL_RADIUS_METERS = wheelsRadiusMeters;
this.turnMotorSim =
new BrushlessMotorSim(
- turnMotor, TURN_GEAR_RATIO, turnRotationalInertia, TURN_FRICTION_VOLTAGE);
+ SimulatedField.getInstance(),
+ turnMotor,
+ turnGearRatio,
+ KilogramSquareMeters.of(turnRotationalInertia),
+ Volts.of(turnFrictionVoltage));
this.cachedDriveEncoderUnGearedPositionsRad = new ConcurrentLinkedQueue<>();
- for (int i = 0; i < SIMULATION_SUB_TICKS_IN_1_PERIOD; i++)
+ for (int i = 0; i < SimulatedField.getSimulationSubTicksIn1Period(); i++)
cachedDriveEncoderUnGearedPositionsRad.offer(driveEncoderUnGearedPositionRad);
this.cachedTurnRelativeEncoderPositionsRad = new ConcurrentLinkedQueue<>();
- for (int i = 0; i < SIMULATION_SUB_TICKS_IN_1_PERIOD; i++)
+ for (int i = 0; i < SimulatedField.getSimulationSubTicksIn1Period(); i++)
cachedTurnRelativeEncoderPositionsRad.offer(turnRelativeEncoderPositionRad);
this.cachedTurnAbsolutePositions = new ConcurrentLinkedQueue<>();
- for (int i = 0; i < SIMULATION_SUB_TICKS_IN_1_PERIOD; i++)
- cachedTurnAbsolutePositions.offer(turnAbsoluteRotation);
+ for (int i = 0; i < SimulatedField.getSimulationSubTicksIn1Period(); i++)
+ cachedTurnAbsolutePositions.offer(turnAbsolutePosition);
this.turnRelativeEncoderPositionRad =
- turnAbsoluteRotation.getRadians() + turnRelativeEncoderOffSet;
- }
-
- public void requestDriveVoltageOut(double volts) {
- this.driveMotorRequestedVolts = volts;
- }
-
- public void requestTurnVoltageOut(double volts) {
- this.turnMotorAppliedVolts = volts;
- // this.turnMotorSim.setInputVoltage(MathUtil.applyDeadband(volts, turn_FRICTION_VOLTAGE,
- // 12));
+ turnAbsolutePosition.getRadians() + turnRelativeEncoderOffSet;
}
+ /**
+ *
+ *
+ *
Requests the Driving Motor to Run at a Specified Voltage Output.
+ *
+ *
Think of it as the setVoltage() of your physical driving motor.
+ *
+ *
This method sets the desired voltage output for the driving motor. The change will be
+ * applied in the next sub-tick of the simulation.
+ *
+ *
Note: The requested voltage may not always be fully applied if the current
+ * is too high. The current limit may reduce the motor's output, similar to real motors.
+ *
+ *
To check the actual voltage applied to the drivetrain, use {@link
+ * #getDriveMotorAppliedVolts()}.
+ *
+ * @param volts the voltage to be applied to the driving motor
+ */
public void requestDriveVoltageOut(Voltage volts) {
this.driveMotorRequestedVolts = volts.in(Volts);
}
+ /**
+ *
+ *
+ *
Requests the Steering Motor to Run at a Specified Voltage Output.
+ *
+ *
Think of it as the setVoltage() of your physical steering motor.
+ *
+ *
This method sets the desired voltage output for the steering motor. The change will be
+ * applied in the next sub-tick of the simulation.
+ *
+ *
Note: Similar to the drive motor, the requested voltage may not always be
+ * fully applied if the current exceeds the limit. The current limit will reduce the motor's
+ * output as needed, mimicking real motor behavior.
+ *
+ *
To check the actual voltage applied to the steering motor, use {@link
+ * #getTurnMotorAppliedVolts()}.
+ *
+ * @param volts the voltage to be applied to the steering motor
+ */
public void requestTurnVoltageOut(Voltage volts) {
- this.turnMotorAppliedVolts = volts.in(Volts);
- // this.turnMotorSim.setInputVoltage(MathUtil.applyDeadband(volts, turn_FRICTION_VOLTAGE,
- // 12));
+ this.turnMotorSim.setControl(volts);
}
+ /**
+ *
+ *
+ *
Obtains the Actual Output Voltage of the Drive Motor.
+ *
+ *
This method returns the actual voltage being applied to the drive motor. The actual applied
+ * voltage may differ from the value set by {@link #requestDriveVoltageOut(double)}.
+ *
+ *
If the motor's supply current is too high, the motor will automatically reduce its output
+ * voltage to protect the system.
+ *
+ * @return the actual output voltage of the drive motor, in volts
+ */
public double getDriveMotorAppliedVolts() {
return driveMotorAppliedVolts;
}
+ /**
+ *
+ *
+ *
Obtains the Actual Output Voltage of the Steering Motor.
+ *
+ *
This method returns the actual voltage being applied to the steering motor. It wraps around
+ * the {@link BrushlessMotorSim#getAppliedVolts()} method.
+ *
+ *
The actual applied voltage may differ from the value set by {@link
+ * #requestTurnVoltageOut(double)}. If the motor's supply current is too high, the motor will
+ * automatically reduce its output voltage to protect the system.
+ *
+ * @return the actual output voltage of the steering motor, in volts
+ */
public double getTurnMotorAppliedVolts() {
- return turnMotorAppliedVolts;
+ return turnMotorSim.getRotorVoltage().in(Volts);
}
+ /**
+ *
+ *
+ *
Obtains the Amount of Current Supplied to the Drive Motor.
+ *
+ *
Think of it as the getSupplyCurrent() of your physical drive motor.
+ *
+ * @return the current supplied to the drive motor, in amperes
+ */
public double getDriveMotorSupplyCurrentAmps() {
return driveMotorSupplyCurrentAmps;
}
+ /**
+ *
+ *
+ *
Obtains the Amount of Current Supplied to the Steer Motor.
+ *
+ *
Think of it as the getSupplyCurrent() of your physical steer motor.
+ *
+ *
This method wraps around {@link BrushlessMotorSim#getCurrentDrawAmps()}.
+ *
+ * @return the current supplied to the steer motor, in amperes
+ */
public double getTurnMotorSupplyCurrentAmps() {
- return turnMotorSupplyCurrentAmps;
+ return turnMotorSim.getSupplyCurrent().in(Amps);
}
+ /**
+ *
+ *
+ *
Obtains the Position of the Drive Encoder.
+ *
+ *
Think of it as the getPosition() of your physical driving motor.
+ *
+ *
This method is used to simulate your {@link SwerveDrivePoseEstimator}.
+ *
+ *
This value represents the un-geared position of the encoder, i.e., the amount of radians the
+ * drive motor's encoder has rotated.
+ *
+ *
To get the final wheel rotations, use {@link #getDriveEncoderFinalPositionRad()}.
+ *
+ * @return the position of the drive motor's encoder, in radians (un-geared)
+ */
public double getDriveEncoderUnGearedPositionRad() {
return driveEncoderUnGearedPositionRad;
}
+ /**
+ *
+ *
+ *
Obtains the Final Position of the Drive Encoder (Wheel Rotations).
+ *
+ *
This method is used to simulate the {@link SwerveDrivePoseEstimator} by providing the final
+ * position of the drive encoder in terms of wheel rotations.
+ *
+ *
The value is calculated by dividing the un-geared encoder position (obtained from {@link
+ * #getDriveEncoderUnGearedPositionRad()}) by the drive gear ratio.
+ *
+ * @return the final position of the drive encoder (wheel rotations), in radians
+ */
public double getDriveEncoderFinalPositionRad() {
return getDriveEncoderUnGearedPositionRad() / DRIVE_GEAR_RATIO;
}
+ /**
+ *
+ *
+ *
Obtains the Speed of the Drive Encoder (Un-Geared), in Radians per Second.
+ *
+ *
Think of it as the getVelocity() of your physical drive motor.
+ *
+ *
This method returns the current speed of the drive encoder in radians per second, without
+ * accounting for the drive gear ratio.
+ *
+ * @return the un-geared speed of the drive encoder, in radians per second
+ */
public double getDriveEncoderUnGearedSpeedRadPerSec() {
return driveEncoderUnGearedSpeedRadPerSec;
}
+ /**
+ *
+ *
+ *
Obtains the Final Speed of the Wheel, in Radians per Second.
+ *
+ * @return the final speed of the drive wheel, in radians per second
+ */
public double getDriveWheelFinalSpeedRadPerSec() {
return getDriveEncoderUnGearedSpeedRadPerSec() / DRIVE_GEAR_RATIO;
}
- /** geared */
+ /**
+ *
+ *
+ *
Obtains the Relative Position of the Steer Encoder, in Radians.
+ *
+ *
Think of it as the getPosition() of your physical steer motor.
+ *
+ * @return the relative encoder position of the steer motor, in radians
+ */
public double getTurnRelativeEncoderPositionRad() {
return turnRelativeEncoderPositionRad;
}
- /** geared */
+ /**
+ *
+ *
+ *
Obtains the Speed of the Steer Relative Encoder, in Radians per Second (Geared).
+ *
+ *
Think of it as the getVelocity() of your physical steer motor.
+ *
+ * @return the speed of the steer relative encoder, in radians per second (geared)
+ */
public double getTurnRelativeEncoderSpeedRadPerSec() {
return turnRelativeEncoderSpeedRadPerSec;
}
+ /**
+ *
+ *
+ *
Obtains the Absolute Facing of the Steer Mechanism.
+ *
+ *
Think of it as the getAbsolutePosition() of your CanCoder.
+ *
+ * @return the absolute facing of the steer mechanism, as a {@link Rotation2d}
+ */
public Rotation2d getTurnAbsolutePosition() {
- return turnAbsoluteRotation;
+ return turnAbsolutePosition;
}
+ /**
+ *
+ *
+ *
Obtains the Absolute Rotational Velocity of the Steer Mechanism.
+ *
+ *
Think of it as the getVelocity() of your CanCoder.
+ *
+ * @return the absolute angular velocity of the steer mechanism, in radians per second
+ */
public double getTurnAbsoluteEncoderSpeedRadPerSec() {
return turnAbsoluteEncoderSpeedRadPerSec;
}
+ /**
+ *
+ *
+ *
Obtains the Cached Readings of the Drive Encoder's Un-Geared Position.
+ *
+ *
The values of {@link #getDriveEncoderUnGearedPositionRad()} are cached at each sub-tick and
+ * can be retrieved using this method.
+ *
+ * @return an array of cached drive encoder un-geared positions, in radians
+ */
public double[] getCachedDriveEncoderUnGearedPositionsRad() {
return cachedDriveEncoderUnGearedPositionsRad.stream().mapToDouble(value -> value).toArray();
}
+ /**
+ *
+ *
+ *
Obtains the Cached Readings of the Drive Encoder's Final Position (Wheel Rotations).
+ *
+ *
The values of {@link #getDriveEncoderUnGearedPositionRad()} are cached at each sub-tick and
+ * are divided by the gear ratio to obtain the final wheel rotations.
+ *
+ * @return an array of cached drive encoder final positions (wheel rotations), in radians
+ */
public double[] getCachedDriveWheelFinalPositionsRad() {
return cachedDriveEncoderUnGearedPositionsRad.stream()
- .mapToDouble(value -> value / DRIVE_GEAR_RATIO)
+ .mapToDouble(value -> value * ModuleConstants.DRIVE_TO_METERS)
.toArray();
}
+ /**
+ *
+ *
+ *
Obtains the Cached Readings of the Steer Relative Encoder's Position.
+ *
+ *
The values of {@link #getTurnRelativeEncoderPositionRad()} are cached at each sub-tick and
+ * can be retrieved using this method.
+ *
+ * @return an array of cached steer relative encoder positions, in radians
+ */
public double[] getCachedTurnRelativeEncoderPositions() {
return cachedTurnRelativeEncoderPositionsRad.stream().mapToDouble(value -> value).toArray();
}
+ /**
+ *
+ *
+ *
Obtains the Cached Readings of the Steer Absolute Positions.
+ *
+ *
The values of {@link #getTurnAbsolutePosition()} are cached at each sub-tick and can be
+ * retrieved using this method.
+ *
+ * @return an array of cached absolute steer positions, as {@link Rotation2d} objects
+ */
public Rotation2d[] getCachedTurnAbsolutePositions() {
return cachedTurnAbsolutePositions.toArray(Rotation2d[]::new);
}
@@ -175,50 +440,90 @@ protected double getGrippingForceNewtons(double gravityForceOnModuleNewtons) {
}
/**
- * updates the simulation sub-tick for this module, updating its inner status (sensor readings)
- * and calculating a total force
*
- * @param moduleCurrentGroundVelocityWorldRelative
- * @return the propelling force that the module generates
+ *
+ *
Updates the Simulation for This Module.
+ *
+ *
This method is called once during every sub-tick of the simulation. It performs the
+ * following actions:
+ *
+ *
+ *
Updates the simulation of the steering mechanism.
+ *
Simulates the propelling force generated by the module.
+ *
Updates and caches the encoder readings for odometry simulation.
+ *
+ *
+ *
Note: Friction forces are not simulated in this method.
+ *
+ * @param moduleCurrentGroundVelocityWorldRelative the current ground velocity of the module,
+ * relative to the world
+ * @param robotFacing the absolute facing of the robot, relative to the world
+ * @param gravityForceOnModuleNewtons the gravitational force acting on this module, in newtons
+ * @return the propelling force generated by the module, as a {@link Vector2} object
*/
public Vector2 updateSimulationSubTickGetModuleForce(
Vector2 moduleCurrentGroundVelocityWorldRelative,
- Rotation2d robotRotation,
+ Rotation2d robotFacing,
double gravityForceOnModuleNewtons) {
- updateturnSimulation();
+ updateTurnSimulation();
/* the maximum gripping force that the wheel can generate */
final double grippingForceNewtons = getGrippingForceNewtons(gravityForceOnModuleNewtons);
- final Rotation2d moduleWorldRotation = this.turnAbsoluteRotation.plus(robotRotation);
+ final Rotation2d moduleWorldFacing = this.turnAbsolutePosition.plus(robotFacing);
final Vector2 propellingForce =
getPropellingForce(
- grippingForceNewtons, moduleWorldRotation, moduleCurrentGroundVelocityWorldRelative);
- updateEncoderTicks();
+ grippingForceNewtons, moduleWorldFacing, moduleCurrentGroundVelocityWorldRelative);
+ updateDriveEncoders();
return propellingForce;
}
- /** */
- private void updateturnSimulation() {
- turnMotorSim.update(SIMULATION_DT);
-
+ /**
+ *
+ *
+ *
updates the simulation for the steer mechanism.
+ *
+ *
Updates the simulation for the steer mechanism and cache the encoder readings.
+ *
+ *
The steer mechanism is modeled by a {@link BrushlessMotorSim}.
+ */
+ private void updateTurnSimulation() {
/* update the readings of the sensor */
- this.turnAbsoluteRotation = Rotation2d.fromRadians(turnMotorSim.getAngularPositionRad());
+ this.turnAbsolutePosition = new Rotation2d(turnMotorSim.getPosition());
this.turnRelativeEncoderPositionRad =
- turnMotorSim.getAngularPositionRad() + turnRelativeEncoderOffSet;
- this.turnAbsoluteEncoderSpeedRadPerSec = turnMotorSim.getAngularVelocityRadPerSec();
+ turnMotorSim.getPosition().in(Radians) + turnRelativeEncoderOffSet;
+ this.turnAbsoluteEncoderSpeedRadPerSec = turnMotorSim.getVelocity().in(RadiansPerSecond);
this.turnRelativeEncoderSpeedRadPerSec = turnAbsoluteEncoderSpeedRadPerSec * TURN_GEAR_RATIO;
/* cache sensor readings to queue for high-frequency odometry */
this.cachedTurnAbsolutePositions.poll();
- this.cachedTurnAbsolutePositions.offer(turnAbsoluteRotation);
+ this.cachedTurnAbsolutePositions.offer(turnAbsolutePosition);
this.cachedTurnRelativeEncoderPositionsRad.poll();
this.cachedTurnRelativeEncoderPositionsRad.offer(turnRelativeEncoderPositionRad);
}
+ /**
+ *
+ *
+ *
Calculates the amount of propelling force that the module generates.
+ *
+ *
The swerve module's drive motor will generate a propelling force.
+ *
+ *
For most of the time, that propelling force is directly applied to the drivetrain. And the
+ * drive wheel runs as fast as the ground velocity
+ *
+ *
However, if the propelling force exceeds the gripping, only the max gripping force is
+ * applied. The rest of the propelling force will cause the wheel to start skidding and make the
+ * odometry inaccurate.
+ *
+ * @param grippingForceNewtons the amount of gripping force that wheel can generate, in newtons
+ * @param moduleWorldFacing the current world facing of the module
+ * @param moduleCurrentGroundVelocity the current ground velocity of the module, world-reference
+ * @return a vector representing the propelling force that the module generates, world-reference
+ */
private Vector2 getPropellingForce(
double grippingForceNewtons,
- Rotation2d moduleWorldRotation,
+ Rotation2d moduleWorldFacing,
Vector2 moduleCurrentGroundVelocity) {
final double driveWheelTorque = getDriveWheelTorque(),
theoreticalMaxPropellingForceNewtons = driveWheelTorque / WHEEL_RADIUS_METERS;
@@ -233,39 +538,34 @@ private Vector2 getPropellingForce(
moduleCurrentGroundVelocity.getMagnitude()
* Math.cos(
moduleCurrentGroundVelocity.getAngleBetween(
- new Vector2(moduleWorldRotation.getRadians())));
-
- if (skidding) {
- /* if the chassis is skidding, part of the toque will cause the wheels to spin freely */
- final double torqueOnWheel = driveWheelTorque * 0.3;
- this.driveEncoderUnGearedSpeedRadPerSec +=
- torqueOnWheel / DRIVE_WHEEL_INERTIA * SIMULATION_DT * DRIVE_GEAR_RATIO;
- } else // if the chassis is tightly gripped on floor, the floor velocity is projected to the
- // wheel
+ new Vector2(moduleWorldFacing.getRadians())));
+
+ // if the chassis is tightly gripped on floor, the floor velocity is projected to the wheel
+ this.driveEncoderUnGearedSpeedRadPerSec =
+ floorVelocityProjectionOnWheelDirectionMPS / WHEEL_RADIUS_METERS * DRIVE_GEAR_RATIO;
+
+ // if the module is skidding
+ if (skidding)
this.driveEncoderUnGearedSpeedRadPerSec =
- floorVelocityProjectionOnWheelDirectionMPS / WHEEL_RADIUS_METERS * DRIVE_GEAR_RATIO;
+ 0.5 * driveEncoderUnGearedSpeedRadPerSec
+ + 0.5 * DRIVE_MOTOR.getSpeed(0, driveMotorAppliedVolts);
- return Vector2.create(propellingForceNewtons, moduleWorldRotation.getRadians());
+ return Vector2.create(propellingForceNewtons, moduleWorldFacing.getRadians());
}
+ /**
+ *
+ *
+ *
Calculates the amount of torque that the drive motor can generate on the wheel.
+ *
+ *
Before calculating the torque of the motor, the output voltage of the drive motor is
+ * constrained for the current limit through {@link
+ * BrushlessMotorSim#constrainOutputVoltage(DCMotor, double, double, double)}.
+ *
+ * @return the amount of torque on the wheel by the drive motor, in Newton * Meters
+ */
private double getDriveWheelTorque() {
- final double currentAtRequestedVolts =
- DRIVE_MOTOR.getCurrent(this.driveEncoderUnGearedSpeedRadPerSec, driveMotorRequestedVolts);
-
driveMotorAppliedVolts = driveMotorRequestedVolts;
- /* normally, motor controller starts cutting the supply voltage when the current exceed 150% the current limit */
- final boolean currentTooHigh = Math.abs(currentAtRequestedVolts) > 1.2 * DRIVE_CURRENT_LIMIT,
- driveMotorTryingToAccelerate = driveMotorRequestedVolts * driveMotorSupplyCurrentAmps > 0;
-
- if (currentTooHigh && driveMotorTryingToAccelerate) {
- /* activate current limit, cut down the applied voltage to match current limit */
- final double currentWithLimits = Math.copySign(DRIVE_CURRENT_LIMIT, currentAtRequestedVolts);
- driveMotorAppliedVolts =
- DRIVE_MOTOR.getVoltage(
- DRIVE_MOTOR.getTorque(currentWithLimits), this.driveEncoderUnGearedSpeedRadPerSec);
- }
-
- driveMotorAppliedVolts = MathUtil.clamp(driveMotorAppliedVolts, -12, 12);
/* calculate the actual supply current */
driveMotorSupplyCurrentAmps =
@@ -274,21 +574,25 @@ private double getDriveWheelTorque() {
MathUtil.applyDeadband(driveMotorAppliedVolts, DRIVE_FRICTION_VOLTAGE, 12));
/* calculate the torque generated, */
- final double torqueOnRotor = DRIVE_MOTOR.getTorque(driveMotorSupplyCurrentAmps);
- return torqueOnRotor * DRIVE_GEAR_RATIO;
+ final double torqueOnRotter = DRIVE_MOTOR.getTorque(driveMotorSupplyCurrentAmps);
+ return torqueOnRotter * DRIVE_GEAR_RATIO;
}
/**
* @return the current module state of this simulation module
*/
- protected SwerveModuleState getCurrentState() {
+ public SwerveModuleState getCurrentState() {
return new SwerveModuleState(
- getDriveWheelFinalSpeedRadPerSec() * WHEEL_RADIUS_METERS, turnAbsoluteRotation);
+ getDriveWheelFinalSpeedRadPerSec() * WHEEL_RADIUS_METERS, turnAbsolutePosition);
}
/**
- * gets the state of the module, if it is allowed to spin freely for a long time under the current
- * applied drive volts
+ *
+ *
+ *
Obtains the "free spin" state of the module
+ *
+ *
The "free spin" state of a simulated module refers to its state after spinning freely for a
+ * long time under the current input voltage
*
* @return the free spinning module state
*/
@@ -299,37 +603,88 @@ protected SwerveModuleState getFreeSpinState() {
driveMotorAppliedVolts)
/ DRIVE_GEAR_RATIO
* WHEEL_RADIUS_METERS,
- turnAbsoluteRotation);
+ turnAbsolutePosition);
}
- private void updateEncoderTicks() {
- this.driveEncoderUnGearedPositionRad += this.driveEncoderUnGearedSpeedRadPerSec * SIMULATION_DT;
+ /**
+ *
+ *
+ *
Cache the encoder values.
+ *
+ *
An internal method to cache the encoder values to their queues.
+ */
+ private void updateDriveEncoders() {
+ this.driveEncoderUnGearedPositionRad +=
+ this.driveEncoderUnGearedSpeedRadPerSec * SimulatedField.getSimulationDt();
this.cachedDriveEncoderUnGearedPositionsRad.poll();
this.cachedDriveEncoderUnGearedPositionsRad.offer(driveEncoderUnGearedPositionRad);
}
+ /**
+ *
+ *
+ *
Obtains the theoretical speed that the module can achieve.
+ *
+ * @return the theoretical maximum ground speed that the module can achieve, in m/s
+ */
public double getModuleTheoreticalSpeedMPS() {
return DRIVE_MOTOR.freeSpeedRadPerSec / DRIVE_GEAR_RATIO * WHEEL_RADIUS_METERS;
}
- public double getTheoreticalPropellingForcePerModule(double robotMass, int modulesCount) {
+ /**
+ *
+ *
+ *
Obtains the theoretical maximum propelling force of ONE module.
+ *
+ *
Calculates the maximum propelling force with respect to the gripping force and the drive
+ * motor's torque under its current limit.
+ *
+ * @param robotMassKg the mass of the robot, is kilograms
+ * @param modulesCount the amount of modules on the robot, assumed to be sharing the gravity force
+ * equally
+ * @return the maximum propelling force of EACH module
+ */
+ public double getTheoreticalPropellingForcePerModule(double robotMassKg, int modulesCount) {
final double
maxThrustNewtons =
DRIVE_MOTOR.getTorque(DRIVE_CURRENT_LIMIT) * DRIVE_GEAR_RATIO / WHEEL_RADIUS_METERS,
- maxGrippingNewtons = 9.8 * robotMass / modulesCount * WHEELS_COEFFICIENT_OF_FRICTION;
+ maxGrippingNewtons = 9.8 * robotMassKg / modulesCount * WHEELS_COEFFICIENT_OF_FRICTION;
return Math.min(maxThrustNewtons, maxGrippingNewtons);
}
- public double getModuleMaxAccelerationMPSsq(double robotMass, int modulesCount) {
- return getTheoreticalPropellingForcePerModule(robotMass, modulesCount)
+ /**
+ *
+ *
+ *
Obtains the theatrical linear acceleration that the robot can achieve.
+ *
+ *
Calculates the maximum linear acceleration of a robot, with respect to its mass and {@link
+ * #getTheoreticalPropellingForcePerModule(double, int)}.
+ *
+ * @param robotMassKg the mass of the robot, is kilograms
+ * @param modulesCount the amount of modules on the robot, assumed to be sharing the gravity force
+ * equally
+ */
+ public double getModuleMaxAccelerationMPSsq(double robotMassKg, int modulesCount) {
+ return getTheoreticalPropellingForcePerModule(robotMassKg, modulesCount)
* modulesCount
- / robotMass;
+ / robotMassKg;
}
- public enum DRIVE_WHEEL_TYPE {
- RUBBER,
- TIRE
+ /**
+ *
+ *
+ *
Stores the coefficient of friction of some common used wheels.
+ */
+ public enum WHEEL_GRIP {
+ RUBBER_WHEEL(1.25),
+ TIRE_WHEEL(1.2);
+
+ public final double cof;
+
+ WHEEL_GRIP(double cof) {
+ this.cof = cof;
+ }
}
/**
@@ -340,7 +695,7 @@ public static Supplier getModule(
DCMotor driveMotor,
DCMotor turnMotor,
double driveCurrentLimitAmps,
- DRIVE_WHEEL_TYPE driveWheelType,
+ WHEEL_GRIP driveWheelType,
double driveGearRatio) {
return () ->
new SwerveModuleSimulation(
@@ -351,10 +706,7 @@ public static Supplier getModule(
11.3142,
0.25,
0.05,
- switch (driveWheelType) {
- case RUBBER -> 1.55;
- case TIRE -> 1.45;
- },
+ driveWheelType.cof,
Units.inchesToMeters(2),
0.05);
}
diff --git a/src/main/java/frc/robot/extras/util/AllianceFlipper.java b/src/main/java/frc/robot/extras/util/AllianceFlipper.java
index a1fe9e4..92ffe9c 100644
--- a/src/main/java/frc/robot/extras/util/AllianceFlipper.java
+++ b/src/main/java/frc/robot/extras/util/AllianceFlipper.java
@@ -12,10 +12,9 @@
import edu.wpi.first.math.geometry.Translation3d;
import edu.wpi.first.wpilibj.DriverStation;
import edu.wpi.first.wpilibj.DriverStation.Alliance;
+import frc.robot.Constants.FieldConstants;
public class AllianceFlipper {
- private static final double FIELD_LENGTH = 16.58112;
-
public static boolean isBlue() {
return DriverStation.getAlliance().orElseGet(() -> Alliance.Blue).equals(Alliance.Blue);
}
@@ -29,7 +28,8 @@ public static boolean isRed() {
* @return Translation2d object with its x coordinate flipped over the y-axis
*/
public static final Translation2d flipTranslation(Translation2d translation) {
- return new Translation2d(FIELD_LENGTH - translation.getX(), translation.getY());
+ return new Translation2d(
+ FieldConstants.FIELD_LENGTH_METERS - translation.getX(), translation.getY());
}
/**
@@ -38,7 +38,9 @@ public static final Translation2d flipTranslation(Translation2d translation) {
*/
public static final Translation3d flipTranslation(Translation3d translation) {
return new Translation3d(
- FIELD_LENGTH - translation.getX(), translation.getY(), translation.getZ());
+ FieldConstants.FIELD_LENGTH_METERS - translation.getX(),
+ translation.getY(),
+ translation.getZ());
}
/**
diff --git a/src/main/java/frc/robot/extras/util/GeomUtil.java b/src/main/java/frc/robot/extras/util/GeomUtil.java
index 3b52c44..017c98e 100644
--- a/src/main/java/frc/robot/extras/util/GeomUtil.java
+++ b/src/main/java/frc/robot/extras/util/GeomUtil.java
@@ -50,7 +50,38 @@ public static ChassisSpeeds toWpilibChassisSpeeds(
dyn4jLinearVelocity.x, dyn4jLinearVelocity.y, angularVelocityRadPerSec);
}
+ /**
+ * Gets the x and y velocities of a ChassisSpeeds
+ *
+ * @param chassisSpeeds the ChassisSpeeds to retrieve velocities from
+ * @return a Translation2d containing the velocities in the x and y direction in meters per second
+ */
public static Translation2d getChassisSpeedsTranslationalComponent(ChassisSpeeds chassisSpeeds) {
return new Translation2d(chassisSpeeds.vxMetersPerSecond, chassisSpeeds.vyMetersPerSecond);
}
+
+ /**
+ * Checks if two translations are within a certain threshold in meters
+ *
+ * @param t1 Translation 1 as a Translation2d
+ * @param t2 Translation 2 as a Translation2d
+ * @param thresholdMeters the threshold between the two translations in meters
+ * @return true if the two translations are within thresholdMeters
+ */
+ public static boolean isTranslationWithinThreshold(
+ Translation2d t1, Translation2d t2, double thresholdMeters) {
+ return t1.getDistance(t2) <= thresholdMeters;
+ }
+
+ /**
+ * Checks if two rotations are within a certain threshold in degrees
+ *
+ * @param r1 Rotations 1 as a Rotation2d
+ * @param r2 Rotation 2 as a Rotation2d
+ * @param thresholdMeters the threshold between the two rotations in degrees
+ * @return true if the two rotations are within thresholdDegrees
+ */
+ public static boolean isRotationWithinThreshold(double r1, double r2, double thresholdDegrees) {
+ return Math.abs(r1 - r2) <= thresholdDegrees;
+ }
}
diff --git a/src/main/java/frc/robot/extras/util/JoystickUtil.java b/src/main/java/frc/robot/extras/util/JoystickUtil.java
new file mode 100644
index 0000000..1ffc4bb
--- /dev/null
+++ b/src/main/java/frc/robot/extras/util/JoystickUtil.java
@@ -0,0 +1,102 @@
+package frc.robot.extras.util;
+
+import frc.robot.Constants.JoystickConstants;
+import java.util.function.DoubleSupplier;
+
+public class JoystickUtil {
+
+ /**
+ * Deadbands a value to 0
+ *
+ * @param value Value to input
+ * @param deadband If the the input is within this value, the function will return 0.
+ * @return 0.0 if the value is within specified deadband range
+ */
+ private static double deadband(double value, double deadband) {
+ if (Math.abs(value) > deadband) {
+ if (value > 0.0) {
+ return (value - deadband) / (1.0 - deadband);
+ } else {
+ return (value + deadband) / (1.0 - deadband);
+ }
+ } else {
+ return 0.0;
+ }
+ }
+
+ /**
+ * Function to modify a singular joystick axis.
+ *
+ * @param supplierValue Supplies a double, usually by a joystick.
+ * @param exponent Exponent to raise the supplied value to.
+ * @return Returns the modified value.
+ */
+ public static double modifyAxis(DoubleSupplier supplierValue, int exponent) {
+ double value = supplierValue.getAsDouble();
+
+ // Deadband
+ value = deadband(value, JoystickConstants.DEADBAND_VALUE);
+
+ // Raise value to specified exponent
+ value = Math.copySign(Math.pow(value, exponent), value);
+
+ return value;
+ }
+
+ public static double modifyAxisCubed(DoubleSupplier supplierValue) {
+ double value = supplierValue.getAsDouble();
+
+ // Deadband
+ value = JoystickUtil.deadband(value, JoystickConstants.DEADBAND_VALUE);
+
+ // Cube the axis
+ value = Math.copySign(value * value * value, value);
+
+ return value;
+ }
+
+ /**
+ * Converts the two axis coordinates to polar to get both the angle and radius, so they can work
+ * in a double[] list.
+ *
+ * @param xJoystick The supplied input of the xJoystick.
+ * @param yJoystick The supplied input of the yJoystick.
+ * @param exponent The exponent to raise the supplied value to.
+ * @return The modified axises of both joysticks in polar form.
+ */
+ public static double[] modifyAxisPolar(
+ DoubleSupplier xJoystick, DoubleSupplier yJoystick, int exponent) {
+ double xInput = deadband(xJoystick.getAsDouble(), JoystickConstants.DEADBAND_VALUE);
+ double yInput = deadband(yJoystick.getAsDouble(), JoystickConstants.DEADBAND_VALUE);
+ if (Math.abs(xInput) > 0 && Math.abs(yInput) > 0) {
+ double theta = Math.atan(xInput / yInput);
+ double hypotenuse = Math.sqrt(xInput * xInput + yInput * yInput);
+ double raisedHypotenuse = Math.pow(hypotenuse, exponent);
+ xInput = Math.copySign(Math.sin(theta) * raisedHypotenuse, xInput);
+ yInput = Math.copySign(Math.cos(theta) * raisedHypotenuse, yInput);
+ return new double[] {xInput, yInput};
+ }
+ return new double[] {
+ Math.copySign(Math.pow(xInput, exponent), xInput),
+ Math.copySign(Math.pow(yInput, exponent), yInput)
+ };
+ }
+
+ public static double[] modifyAxisCubedPolar(DoubleSupplier xJoystick, DoubleSupplier yJoystick) {
+ double xInput =
+ JoystickUtil.deadband(xJoystick.getAsDouble(), JoystickConstants.DEADBAND_VALUE);
+ double yInput = deadband(yJoystick.getAsDouble(), JoystickConstants.DEADBAND_VALUE);
+ if (Math.abs(xInput) > 0 && Math.abs(yInput) > 0) {
+ double theta = Math.atan(xInput / yInput);
+ double hypotenuse = Math.sqrt(xInput * xInput + yInput * yInput);
+ double cubedHypotenuse = Math.pow(hypotenuse, 3);
+ xInput = Math.copySign(Math.sin(theta) * cubedHypotenuse, xInput);
+ yInput = Math.copySign(Math.cos(theta) * cubedHypotenuse, yInput);
+ return new double[] {xInput, yInput};
+ }
+ return new double[] {
+ Math.copySign(xInput * xInput * xInput, xInput),
+ Math.copySign(yInput * yInput * yInput, yInput)
+ };
+ }
+}
diff --git a/src/main/java/frc/robot/extras/util/MathUtil.java b/src/main/java/frc/robot/extras/util/MathUtil.java
index 65acfdf..f67401a 100644
--- a/src/main/java/frc/robot/extras/util/MathUtil.java
+++ b/src/main/java/frc/robot/extras/util/MathUtil.java
@@ -1,7 +1,6 @@
package frc.robot.extras.util;
import edu.wpi.first.math.geometry.Rotation3d;
-import frc.robot.BuildConstants;
import java.util.Random;
public class MathUtil {
@@ -9,7 +8,7 @@ public class MathUtil {
* random object that generates random variables the seed is the hash of GIT_SHA this way when you
* do log-replay even the generated random numbers are the same
*/
- private static final Random random = new Random(BuildConstants.GIT_SHA.hashCode());
+ private static final Random random = new Random((long) Math.random());
public static double linearInterpretationWithBounding(
double x1, double y1, double x2, double y2, double x) {
diff --git a/src/main/java/frc/robot/extras/util/ProceduralStructGenerator.java b/src/main/java/frc/robot/extras/util/ProceduralStructGenerator.java
new file mode 100644
index 0000000..7a47a72
--- /dev/null
+++ b/src/main/java/frc/robot/extras/util/ProceduralStructGenerator.java
@@ -0,0 +1,857 @@
+package frc.robot.extras.util;
+
+import edu.wpi.first.units.Measure;
+import edu.wpi.first.util.struct.Struct;
+import edu.wpi.first.util.struct.StructSerializable;
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.RecordComponent;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Optional;
+import java.util.OptionalInt;
+import java.util.Set;
+import java.util.function.Supplier;
+import java.util.stream.BaseStream;
+
+/** A utility class for procedurally generating {@link Struct}s from records and enums. */
+public final class ProceduralStructGenerator {
+ private ProceduralStructGenerator() {
+ throw new UnsupportedOperationException("This is a utility class!");
+ }
+
+ /**
+ * A functional interface representing a method that retrives a value from a {@link ByteBuffer}.
+ */
+ @FunctionalInterface
+ private interface Unpacker {
+ T unpack(ByteBuffer buffer);
+ }
+
+ /** A functional interface representing a method that packs a value into a {@link ByteBuffer}. */
+ @FunctionalInterface
+ private interface Packer {
+ ByteBuffer pack(ByteBuffer buffer, T value);
+
+ static Packer fromStruct(Struct struct) {
+ return (buffer, value) -> {
+ struct.pack(buffer, value);
+ return buffer;
+ };
+ }
+ }
+
+ private record PrimType(String name, int size, Unpacker unpacker, Packer packer) {}
+
+ /** A map of primitive types to their schema types. */
+ private static final HashMap, PrimType>> primitiveTypeMap = new HashMap<>();
+
+ private static void addPrimType(
+ Class boxedClass,
+ Class primitiveClass,
+ String name,
+ int size,
+ Unpacker unpacker,
+ Packer packer) {
+ PrimType primType = new PrimType<>(name, size, unpacker, packer);
+ primitiveTypeMap.put(boxedClass, primType);
+ primitiveTypeMap.put(primitiveClass, primType);
+ }
+
+ // Add primitive types to the map
+ static {
+ addPrimType(
+ Integer.class, int.class, "int32", Integer.BYTES, ByteBuffer::getInt, ByteBuffer::putInt);
+ addPrimType(
+ Double.class,
+ double.class,
+ "float64",
+ Double.BYTES,
+ ByteBuffer::getDouble,
+ ByteBuffer::putDouble);
+ addPrimType(
+ Float.class,
+ float.class,
+ "float32",
+ Float.BYTES,
+ ByteBuffer::getFloat,
+ ByteBuffer::putFloat);
+ addPrimType(
+ Boolean.class,
+ boolean.class,
+ "bool",
+ Byte.BYTES,
+ buffer -> buffer.get() != 0,
+ (buffer, value) -> buffer.put((byte) (value ? 1 : 0)));
+ addPrimType(
+ Character.class,
+ char.class,
+ "char",
+ Character.BYTES,
+ ByteBuffer::getChar,
+ ByteBuffer::putChar);
+ addPrimType(Byte.class, byte.class, "uint8", Byte.BYTES, ByteBuffer::get, ByteBuffer::put);
+ addPrimType(
+ Short.class, short.class, "int16", Short.BYTES, ByteBuffer::getShort, ByteBuffer::putShort);
+ addPrimType(
+ Long.class, long.class, "int64", Long.BYTES, ByteBuffer::getLong, ByteBuffer::putLong);
+ }
+
+ /**
+ * A map of types to their custom struct schemas.
+ *
+ *
This allows adding custom struct implementations for types that are not supported by
+ * default. Think of vendor-specific.
+ */
+ private static final HashMap, Struct>> customStructTypeMap = new HashMap<>();
+
+ /**
+ * Add a custom struct to the structifier.
+ *
+ * @param The type the struct is for.
+ * @param clazz The class of the type.
+ * @param struct The struct to add.
+ * @param override Whether to override an existing struct. An existing struct could mean the type
+ * already has a {@code struct} field and implemnts {@link StructSerializable} or that the
+ * type is already in the custom struct map.
+ */
+ public static void addCustomStruct(Class clazz, Struct struct, boolean override) {
+ if (override) {
+ customStructTypeMap.put(clazz, struct);
+ } else if (!StructSerializable.class.isAssignableFrom(clazz)) {
+ customStructTypeMap.putIfAbsent(clazz, struct);
+ }
+ }
+
+ /**
+ * Returns a {@link Struct} for the given {@link StructSerializable} marked class. Due to the
+ * non-contractual nature of the marker this can fail. If the {@code struct} field could not be
+ * accessed for any reason, an empty {@link Optional} is returned.
+ *
+ * @param The type of the class.
+ * @param clazz The class object to extract the struct from.
+ * @return An optional containing the struct if it could be extracted.
+ */
+ @SuppressWarnings("unchecked")
+ public static Optional> extractClassStruct(
+ Class extends T> clazz) {
+ try {
+ var possibleField = Optional.ofNullable(clazz.getDeclaredField("struct"));
+ return possibleField.flatMap(
+ field -> {
+ field.setAccessible(true);
+ if (Struct.class.isAssignableFrom(field.getType())) {
+ try {
+ return Optional.ofNullable((Struct) field.get(null));
+ } catch (IllegalAccessException e) {
+ return Optional.empty();
+ }
+ } else {
+ return Optional.empty();
+ }
+ });
+ } catch (NoSuchFieldException e) {
+ return Optional.empty();
+ }
+ }
+
+ /**
+ * Returns a {@link Struct} for the given class. This does not do compile time checking that the
+ * class is a {@link StructSerializable}. Whenever possible it is reccomended to use {@link
+ * #extractClassStruct(Class)}.
+ *
+ * @param clazz The class object to extract the struct from.
+ * @return An optional containing the struct if it could be extracted.
+ */
+ @SuppressWarnings("unchecked")
+ public static Optional> extractClassStructDynamic(Class> clazz) {
+ if (StructSerializable.class.isAssignableFrom(clazz)) {
+ return extractClassStruct((Class extends StructSerializable>) clazz).map(struct -> struct);
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.FIELD, ElementType.RECORD_COMPONENT})
+ @Documented
+ public @interface IgnoreStructField {}
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.FIELD, ElementType.RECORD_COMPONENT})
+ @Documented
+ public @interface FixedSizeArray {
+ int size();
+ }
+
+ private static OptionalInt arraySize(AnnotatedElement field) {
+ return Optional.ofNullable(field.getAnnotation(FixedSizeArray.class))
+ .map(FixedSizeArray::size)
+ .map(OptionalInt::of)
+ .orElse(OptionalInt.empty());
+ }
+
+ private static boolean shouldIgnore(AnnotatedElement field) {
+ return field.isAnnotationPresent(IgnoreStructField.class);
+ }
+
+ private record StructField(
+ String name,
+ String type,
+ int size,
+ boolean immutable,
+ Set> structsToLoad,
+ Unpacker> unpacker,
+ Packer> packer) {
+
+ public static StructField fromField(Field field) {
+ return StructField.fromNameAndClass(
+ field.getName(),
+ field.getType(),
+ arraySize(field),
+ Modifier.isFinal(field.getModifiers()));
+ }
+
+ public static StructField fromRecordComponent(RecordComponent component) {
+ return StructField.fromNameAndClass(
+ component.getName(), component.getType(), arraySize(component), true);
+ }
+
+ @SuppressWarnings("unchecked")
+ public static StructField fromNameAndClass(
+ String name, Class> clazz, OptionalInt arraySize, boolean isFinal) {
+ if (!isFixedSize(clazz, arraySize)) {
+ return null;
+ }
+ if (clazz.isArray() && arraySize.isPresent()) {
+ final Class> componentType = clazz.getComponentType();
+ final int size = arraySize.getAsInt();
+ final StructField componentField =
+ fromNameAndClass(
+ componentType.getSimpleName(), componentType, OptionalInt.empty(), false);
+ return new StructField(
+ name + "[" + size + "]",
+ componentField.type,
+ componentField.size * size,
+ isFinal,
+ componentField.structsToLoad,
+ buffer -> {
+ Object[] array = new Object[size];
+ for (int i = 0; i < size; i++) {
+ array[i] = componentField.unpacker.unpack(buffer);
+ }
+ return array;
+ },
+ (buffer, value) -> {
+ for (Object obj : (Object[]) value) {
+ ((Packer