diff --git a/build-lib.bat b/build-lib.bat
index d0bf11a..06629ed 100644
--- a/build-lib.bat
+++ b/build-lib.bat
@@ -6,6 +6,7 @@ cd lib
@REM Build the distribution zip (used for the driverstation client)
call gradlew.bat distZip
@REM Build the maven repo (used for the test-bot and for any vendor dependencies)
+rmdir /Q /S app\build\maven-repo\org\frc\raptors1711\raptors-claw
call gradlew.bat publish
cd ..
diff --git a/lib/app/build.gradle b/lib/app/build.gradle
index 8361394..fd0b5b4 100644
--- a/lib/app/build.gradle
+++ b/lib/app/build.gradle
@@ -85,6 +85,8 @@ dependencies {
// Used by other dependencies
implementation "edu.wpi.first.wpimath:wpimath-java:2023.1.1"
+ implementation "edu.wpi.first.hal:hal-java:2023.1.1"
+ implementation "edu.wpi.first.hal:hal-jni:2023.1.1"
// Add org.fusesource.jansi
// Used for console ANSI processing for driverstation-side RCT
diff --git a/lib/app/build/distributions/driverstation-rct-client.zip b/lib/app/build/distributions/driverstation-rct-client.zip
index 2a462df..59dbf7f 100644
Binary files a/lib/app/build/distributions/driverstation-rct-client.zip and b/lib/app/build/distributions/driverstation-rct-client.zip differ
diff --git a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0-sources.jar b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0-sources.jar
index d6b55c7..d9212a1 100644
Binary files a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0-sources.jar and b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0-sources.jar differ
diff --git a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0-sources.jar.md5 b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0-sources.jar.md5
index a3fc1d5..2fd27e2 100644
--- a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0-sources.jar.md5
+++ b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0-sources.jar.md5
@@ -1 +1 @@
-f7be92f5ac76d7f011f43a4fbab9352a
\ No newline at end of file
+2a98f4603c63168315cd902fdea8ed8c
\ No newline at end of file
diff --git a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0-sources.jar.sha1 b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0-sources.jar.sha1
index ea4dee1..91bdeed 100644
--- a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0-sources.jar.sha1
+++ b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0-sources.jar.sha1
@@ -1 +1 @@
-94c6057efb26087e59d62bc642d50770cae0f2f3
\ No newline at end of file
+45408af577b0daeb320a4061a63a1c8358d865ef
\ No newline at end of file
diff --git a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0-sources.jar.sha256 b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0-sources.jar.sha256
index d879608..cb57406 100644
--- a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0-sources.jar.sha256
+++ b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0-sources.jar.sha256
@@ -1 +1 @@
-cc0de2620d8b5d26842e4091d44d9848c9782fc261dbdc893eaecb36e666ff1c
\ No newline at end of file
+cf51b16c07af45d32ca8cf75785f093af125c18a334ce74c4154d450207f046c
\ No newline at end of file
diff --git a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0-sources.jar.sha512 b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0-sources.jar.sha512
index 30de1ed..d6afdba 100644
--- a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0-sources.jar.sha512
+++ b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0-sources.jar.sha512
@@ -1 +1 @@
-fe7f1e6c62a01bfd8d2d4ef4c7b814db7e818bb0ea0552dc80b6278d41765fc5955627bd1843083a43bf1b6db817afbd51a077ef496f82fd37bfb50ecb5f6e4e
\ No newline at end of file
+7a49dc68e04b31347589e9d450515505c6ad259c068d85b00536ac8369124278e500ea479201c7d3413d9121f729aeec87684e69225b1b2829544b443ef6ba49
\ No newline at end of file
diff --git a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.jar b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.jar
index 6b1039b..999dc31 100644
Binary files a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.jar and b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.jar differ
diff --git a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.jar.md5 b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.jar.md5
index 8895cbf..d550802 100644
--- a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.jar.md5
+++ b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.jar.md5
@@ -1 +1 @@
-c64437c4e3855dfdbbc48ce9634a60bb
\ No newline at end of file
+69c7395c4e981d1c9876168dfbad12e3
\ No newline at end of file
diff --git a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.jar.sha1 b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.jar.sha1
index 58eb966..85ad585 100644
--- a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.jar.sha1
+++ b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.jar.sha1
@@ -1 +1 @@
-bb5fa500ca0622333860118f2ad73d77621d4ad6
\ No newline at end of file
+fa571001cca7fc6b1b6a3a5cb904b877c00dccf3
\ No newline at end of file
diff --git a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.jar.sha256 b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.jar.sha256
index a76d8fd..683158f 100644
--- a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.jar.sha256
+++ b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.jar.sha256
@@ -1 +1 @@
-41050bf4db962f80a1e2456ad2def64d4851aac0173987f0cfec1b3a65c7aa3d
\ No newline at end of file
+5639e1fcd244f5a278d31d39521d48fa862b7fdd5c52c20a5fff82e9f694ab67
\ No newline at end of file
diff --git a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.jar.sha512 b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.jar.sha512
index 107bcb6..393dec9 100644
--- a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.jar.sha512
+++ b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.jar.sha512
@@ -1 +1 @@
-25bee8a2b2221c1544b9860de60442c8cf77efed71e73f6f168c05ec812308b9c78262debc086cdf77426e26544b10be4af823118e25fc820b0f5962f65c3db7
\ No newline at end of file
+cc5581befe8bad65b269aab28a848429ff746c62df122529eb02ef629d61803213c7c3139e9b2a091db3b3c5aec5aa5b35eaa0a98c3d7ee4c36cc3458cae1d21
\ No newline at end of file
diff --git a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.module b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.module
index 7d5ab99..c96f3a3 100644
--- a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.module
+++ b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.module
@@ -27,11 +27,11 @@
{
"name": "raptors-claw-0.jar",
"url": "raptors-claw-0.jar",
- "size": 113205,
- "sha512": "25bee8a2b2221c1544b9860de60442c8cf77efed71e73f6f168c05ec812308b9c78262debc086cdf77426e26544b10be4af823118e25fc820b0f5962f65c3db7",
- "sha256": "41050bf4db962f80a1e2456ad2def64d4851aac0173987f0cfec1b3a65c7aa3d",
- "sha1": "bb5fa500ca0622333860118f2ad73d77621d4ad6",
- "md5": "c64437c4e3855dfdbbc48ce9634a60bb"
+ "size": 143380,
+ "sha512": "cc5581befe8bad65b269aab28a848429ff746c62df122529eb02ef629d61803213c7c3139e9b2a091db3b3c5aec5aa5b35eaa0a98c3d7ee4c36cc3458cae1d21",
+ "sha256": "5639e1fcd244f5a278d31d39521d48fa862b7fdd5c52c20a5fff82e9f694ab67",
+ "sha1": "fa571001cca7fc6b1b6a3a5cb904b877c00dccf3",
+ "md5": "69c7395c4e981d1c9876168dfbad12e3"
}
]
},
@@ -87,6 +87,20 @@
"requires": "2023.1.1"
}
},
+ {
+ "group": "edu.wpi.first.hal",
+ "module": "hal-java",
+ "version": {
+ "requires": "2023.1.1"
+ }
+ },
+ {
+ "group": "edu.wpi.first.hal",
+ "module": "hal-jni",
+ "version": {
+ "requires": "2023.1.1"
+ }
+ },
{
"group": "org.fusesource.jansi",
"module": "jansi",
@@ -120,11 +134,11 @@
{
"name": "raptors-claw-0.jar",
"url": "raptors-claw-0.jar",
- "size": 113205,
- "sha512": "25bee8a2b2221c1544b9860de60442c8cf77efed71e73f6f168c05ec812308b9c78262debc086cdf77426e26544b10be4af823118e25fc820b0f5962f65c3db7",
- "sha256": "41050bf4db962f80a1e2456ad2def64d4851aac0173987f0cfec1b3a65c7aa3d",
- "sha1": "bb5fa500ca0622333860118f2ad73d77621d4ad6",
- "md5": "c64437c4e3855dfdbbc48ce9634a60bb"
+ "size": 143380,
+ "sha512": "cc5581befe8bad65b269aab28a848429ff746c62df122529eb02ef629d61803213c7c3139e9b2a091db3b3c5aec5aa5b35eaa0a98c3d7ee4c36cc3458cae1d21",
+ "sha256": "5639e1fcd244f5a278d31d39521d48fa862b7fdd5c52c20a5fff82e9f694ab67",
+ "sha1": "fa571001cca7fc6b1b6a3a5cb904b877c00dccf3",
+ "md5": "69c7395c4e981d1c9876168dfbad12e3"
}
]
},
@@ -140,11 +154,11 @@
{
"name": "raptors-claw-0-sources.jar",
"url": "raptors-claw-0-sources.jar",
- "size": 71904,
- "sha512": "fe7f1e6c62a01bfd8d2d4ef4c7b814db7e818bb0ea0552dc80b6278d41765fc5955627bd1843083a43bf1b6db817afbd51a077ef496f82fd37bfb50ecb5f6e4e",
- "sha256": "cc0de2620d8b5d26842e4091d44d9848c9782fc261dbdc893eaecb36e666ff1c",
- "sha1": "94c6057efb26087e59d62bc642d50770cae0f2f3",
- "md5": "f7be92f5ac76d7f011f43a4fbab9352a"
+ "size": 87408,
+ "sha512": "7a49dc68e04b31347589e9d450515505c6ad259c068d85b00536ac8369124278e500ea479201c7d3413d9121f729aeec87684e69225b1b2829544b443ef6ba49",
+ "sha256": "cf51b16c07af45d32ca8cf75785f093af125c18a334ce74c4154d450207f046c",
+ "sha1": "45408af577b0daeb320a4061a63a1c8358d865ef",
+ "md5": "2a98f4603c63168315cd902fdea8ed8c"
}
]
}
diff --git a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.module.md5 b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.module.md5
index 00cb944..ab5f13a 100644
--- a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.module.md5
+++ b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.module.md5
@@ -1 +1 @@
-b0fc064dd4e57caccd34e492db097cd4
\ No newline at end of file
+2b7c30da23792f8bb8e99cac0d2fcce8
\ No newline at end of file
diff --git a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.module.sha1 b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.module.sha1
index 5ce806b..77e058b 100644
--- a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.module.sha1
+++ b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.module.sha1
@@ -1 +1 @@
-da4aa36ea816d3804b06ebf152431494f655656a
\ No newline at end of file
+dbe5d06ba94a5718b942b99ad8b1775ca3ab2bab
\ No newline at end of file
diff --git a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.module.sha256 b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.module.sha256
index 26a0f10..b0fd3be 100644
--- a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.module.sha256
+++ b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.module.sha256
@@ -1 +1 @@
-09b794a92a2a4d97c16d99ae7071b4007290f898bed7f4781b0253fb5b4011b9
\ No newline at end of file
+4e145cf6795a3cbb0ca471c72fe20ef7bba3cf7ac7551095eded4b73f1cfa883
\ No newline at end of file
diff --git a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.module.sha512 b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.module.sha512
index 1619a77..892f21e 100644
--- a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.module.sha512
+++ b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.module.sha512
@@ -1 +1 @@
-0476cd79862afcdb30762d775b5d37753cd58fe3c35439e2198fa915b396284ff45ebe73121f7a0fc413d15fe6e7ef15e8967bb1ba2c67978c9bd66ef125344e
\ No newline at end of file
+99dd9f4b385f74ed29298a341963ca0ebf14e447454713f0cf7db679b18ed8e38665129b3a8181bcc03970429a5287a8a83fe3069303179894ee0f5098f5315a
\ No newline at end of file
diff --git a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.pom b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.pom
index f1a0d52..eb3f063 100644
--- a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.pom
+++ b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.pom
@@ -49,6 +49,18 @@
2023.1.1runtime
+
+ edu.wpi.first.hal
+ hal-java
+ 2023.1.1
+ runtime
+
+
+ edu.wpi.first.hal
+ hal-jni
+ 2023.1.1
+ runtime
+ org.fusesource.jansijansi
diff --git a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.pom.md5 b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.pom.md5
index ff6b772..91c8ee9 100644
--- a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.pom.md5
+++ b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.pom.md5
@@ -1 +1 @@
-a875c60bcc81513eb2da3f571a22fc25
\ No newline at end of file
+e02ff13651d2c72425dd2e1317c719fb
\ No newline at end of file
diff --git a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.pom.sha1 b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.pom.sha1
index 178fabd..9562a0e 100644
--- a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.pom.sha1
+++ b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.pom.sha1
@@ -1 +1 @@
-e53e55f8df7b668d00bc63f023be1fa87e3341c3
\ No newline at end of file
+e0aa31056f00965a658913cd0727081b875e0be8
\ No newline at end of file
diff --git a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.pom.sha256 b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.pom.sha256
index 7890ef8..32c13bc 100644
--- a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.pom.sha256
+++ b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.pom.sha256
@@ -1 +1 @@
-814fad6c4d5e1204b77f01926947061d9974cec316dd58847c537463719e7bb9
\ No newline at end of file
+ba87785d734ee69db697ddccabc4b796704d2d83808dfa7c66df5546d371c8e2
\ No newline at end of file
diff --git a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.pom.sha512 b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.pom.sha512
index 5266655..369e832 100644
--- a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.pom.sha512
+++ b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/0/raptors-claw-0.pom.sha512
@@ -1 +1 @@
-fb1c6d465f43f2a19a96e10689739116a2d9db345fc148eb21b3e98a9ad7da829911208386cd9c31ff3e4adbc1eb54bcf3eaae4fdf6bee7f0e2376ae5fcf8c3a
\ No newline at end of file
+b9fb86f462200fac15cebace6413dcf3887c768c4a3cd5d12065ea41ce4a59a794a026bd00b201d2e9ab747c1f1cff800cbdf9afc03b265fe7edd032d03fc4b9
\ No newline at end of file
diff --git a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/maven-metadata.xml b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/maven-metadata.xml
index 0f2c98e..876ccf9 100644
--- a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/maven-metadata.xml
+++ b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/maven-metadata.xml
@@ -8,6 +8,6 @@
0
- 20230223211415
+ 20230313202232
diff --git a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/maven-metadata.xml.md5 b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/maven-metadata.xml.md5
index dd61e5b..1e83158 100644
--- a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/maven-metadata.xml.md5
+++ b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/maven-metadata.xml.md5
@@ -1 +1 @@
-ddd5917af0b7d7cb8178add75c5eac89
\ No newline at end of file
+db1d12f0bc9179176520bd51c4e5d25e
\ No newline at end of file
diff --git a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/maven-metadata.xml.sha1 b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/maven-metadata.xml.sha1
index fb32640..705ad0d 100644
--- a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/maven-metadata.xml.sha1
+++ b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/maven-metadata.xml.sha1
@@ -1 +1 @@
-0bfb9526335c17b0cdc65c3192f8f22c8dd0077c
\ No newline at end of file
+1254df09615b3cd2a0b31493ecafc7cf4eb8c613
\ No newline at end of file
diff --git a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/maven-metadata.xml.sha256 b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/maven-metadata.xml.sha256
index 85ab47a..4145cfb 100644
--- a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/maven-metadata.xml.sha256
+++ b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/maven-metadata.xml.sha256
@@ -1 +1 @@
-d2a64a5cfac32dca1136e654e98f6d1cf7d8b80d5e7f1c4acd1f21a1802c7744
\ No newline at end of file
+a0550bca32e3a3204a52d8e2d91839ff3254ba7b2f5f801c23deed2cd8f68831
\ No newline at end of file
diff --git a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/maven-metadata.xml.sha512 b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/maven-metadata.xml.sha512
index f35b560..242fe1e 100644
--- a/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/maven-metadata.xml.sha512
+++ b/lib/app/build/maven-repo/org/frc/raptors1711/raptors-claw/maven-metadata.xml.sha512
@@ -1 +1 @@
-7dd20974a1222fbfd1ee6291cd9eb11a0bbe17e0406cd85eb756ddd9c6a4237a99f4d81e9616bfa1c5d7d01d6318c4a3908dc0c58aa84661968781881a09e3c1
\ No newline at end of file
+55e9af188bf935923a37bdf033f332be519dcf2e18b53ee1ffc189c2fef1100966f46df9b7c782119d832161a324f5c7a75903f5c3a9adb34d414e4fdbe3a91b
\ No newline at end of file
diff --git a/lib/app/src/main/java/claw/CLAWRobot.java b/lib/app/src/main/java/claw/CLAWRobot.java
index 183fb22..8da9376 100644
--- a/lib/app/src/main/java/claw/CLAWRobot.java
+++ b/lib/app/src/main/java/claw/CLAWRobot.java
@@ -8,45 +8,55 @@
import claw.logs.LogHandler;
import claw.rct.commands.CommandLineInterpreter;
import claw.rct.remote.RCTServer;
+import edu.wpi.first.wpilibj.Preferences;
import edu.wpi.first.wpilibj.TimedRobot;
import edu.wpi.first.wpilibj2.command.Command;
import edu.wpi.first.wpilibj2.command.CommandScheduler;
public class CLAWRobot {
+ // Runtime execution determined by preferences so that the user can control
+ // this through any NetorkTables client (so that if you turn the server off,
+ // you can still control this execution)
+ private static final String RUN_RCT_SERVER = "CLAW.RUN_RCT_SERVER", RCT_SERVER_PORT = "CLAW.RCT_SERVER_PORT";
+ private static final int DEFAULT_SERVER_PORT = 5800;
+
private static final CommandLineInterpreter EXTENSIBLE_COMMAND_INTERPRETER = new CommandLineInterpreter();
+ private static boolean hasStartedCompetition = false;
+
public static void startCompetition (TimedRobot robot, Runnable robotStartCompetition) {
+ // Do not call startCompetition more than once
+ if (hasStartedCompetition)
+ throw new RuntimeException("Cannot call startCompetition more than once");
+ hasStartedCompetition = true;
- initializeRuntime();
- robot.addPeriodic(CLAWRobot::robotPeriodic, TimedRobot.kDefaultPeriod);
-
- try {
- robotStartCompetition.run();
- } catch (Throwable exception) {
- handleFatalUncaughtException(exception);
- onRobotCodeFinish();
- throw exception;
+ // Start the RCT server if indicated by preferences to do so
+ Preferences.initBoolean(RUN_RCT_SERVER, true);
+ Preferences.initInt(RCT_SERVER_PORT, DEFAULT_SERVER_PORT);
+ if (Preferences.getBoolean(RUN_RCT_SERVER, true)) {
+ startThread(CLAWRobot::initializeRCTServer);
}
+ // Run until robot code finishes
+ runRobotCode(robot, robotStartCompetition);
+
}
-
- public static CommandLineInterpreter getExtensibleCommandInterpreter () {
- return EXTENSIBLE_COMMAND_INTERPRETER;
+
+ private static void startThread (Runnable thread) {
+ new Thread(thread).start();
}
-
+
private static final CLAWLogger
COMMANDS_LOG = CLAWLogger.getLogger("claw.commands"),
RUNTIME_LOG = CLAWLogger.getLogger("claw.runtime");
-
+
private static RCTServer server;
- private static boolean initialized = false;
- private static void initializeRuntime () {
-
- if (initialized) return;
- initialized = true;
-
+ /**
+ * Start the robot code and the CLAW robot code runtime necessary for robot code functioning
+ */
+ private static void runRobotCode (TimedRobot robot, Runnable robotStartCompetition) {
// Put a message into the console indicating that the CLAWRobot runtime has started
System.out.println("\n -- CLAW is running -- \n");
@@ -59,23 +69,34 @@ private static void initializeRuntime () {
CommandScheduler.getInstance().onCommandFinish(CLAWRobot::onCommandFinish);
CommandScheduler.getInstance().onCommandInterrupt(CLAWRobot::onCommandInterrupt);
- // Start RCT server thread
- new Thread(() -> {
- try {
- server = new RCTServer(5800, EXTENSIBLE_COMMAND_INTERPRETER);
- server.start();
- } catch (IOException e) {
- System.err.println("Failed to start RCT server.");
- e.printStackTrace();
- }
- }).start();
-
+ try {
+ // Add the periodic method for the CLAWRobot and call start competition within a try-catch
+ // loop to catch any exceptions
+ robot.addPeriodic(CLAWRobot::robotPeriodic, TimedRobot.kDefaultPeriod);
+ robotStartCompetition.run();
+ } catch (Throwable exception) {
+ // Catch any uncaught robot exceptions
+ handleFatalUncaughtException(exception);
+ throw exception;
+ }
}
+ /**
+ * Initialize the RCT server allowing for advanced debugging and control from the driver station
+ */
+ private static void initializeRCTServer () {
+ // Start RCT server
+ try {
+ server = new RCTServer(Preferences.getInt(RCT_SERVER_PORT, DEFAULT_SERVER_PORT), EXTENSIBLE_COMMAND_INTERPRETER);
+ server.start();
+ } catch (IOException e) {
+ System.err.println("Failed to start RCT server.");
+ e.printStackTrace();
+ }
+ }
- private static void onRobotCodeFinish () {
- if (server != null)
- LogHandler.getInstance().sendData(server);
+ public static CommandLineInterpreter getExtensibleCommandInterpreter () {
+ return EXTENSIBLE_COMMAND_INTERPRETER;
}
private static void robotPeriodic () {
@@ -101,7 +122,12 @@ private static void onCommandInterrupt (Command command) {
private static void handleUncaughtException (Thread thread, Throwable exception) {
// Print to the driver station
- System.err.println("Caught an uncaught exception: " + exception.getMessage());
+ System.err.println(
+ "Caught an exception in the robot code: " + exception.getMessage()+". Use the " +
+ "'errlog' command in the Robot Control Terminal to examine it."
+ );
+
+ RobotErrorLog.logThreadError(exception);
// Put to the logger
RUNTIME_LOG.err("Uncaught exception in a thread '"+thread.getName()+"':\n"+getStackTrace(exception));
@@ -110,6 +136,8 @@ private static void handleUncaughtException (Thread thread, Throwable exception)
private static void handleFatalUncaughtException (Throwable exception) {
// Put to the logger
RUNTIME_LOG.err("Fatal uncaught exception in robot code:\n"+getStackTrace(exception));
+
+ RobotErrorLog.logFatalError(exception);
}
private static String getStackTrace (Throwable e) {
diff --git a/lib/app/src/main/java/claw/LiveValues.java b/lib/app/src/main/java/claw/LiveValues.java
new file mode 100644
index 0000000..8bb36f2
--- /dev/null
+++ b/lib/app/src/main/java/claw/LiveValues.java
@@ -0,0 +1,135 @@
+package claw;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+
+import claw.rct.network.low.ConsoleManager;
+
+/**
+ * A class allowing for the live updating of fields to be displayed in the console.
+ */
+public class LiveValues {
+
+ /**
+ * An object lock to be synchronized on before making any modifications to the fields,
+ * values, etc.
+ */
+ private final Object fieldsLock = new Object();
+
+ private final ArrayList fields = new ArrayList<>();
+ private final ArrayList values = new ArrayList<>();
+ private final HashSet newFieldNames = new HashSet<>();
+ private final HashSet updatedFields = new HashSet<>();
+
+ /**
+ * Set a live field to a string value to be displayed in the console.
+ * @param fieldName The name of the field under which the value should be displayed.
+ * @param value The value to put to the console.
+ */
+ public void setField (String fieldName, String value) {
+ synchronized (fieldsLock) {
+ // Get the index of the field in the fields and values arrays
+ int fieldIndex = fields.indexOf(fieldName);
+
+ if (fieldIndex == -1) {
+
+ // Add a new field and value
+ newFieldNames.add(fieldName);
+ fields.add(fieldName);
+ values.add(value);
+
+ } else if (!values.get(fieldIndex).equals(value)) {
+
+ // Update the value only, but first check if it's the same as it was before
+ values.set(fieldIndex, value);
+ updatedFields.add(fieldName);
+
+ }
+ }
+ }
+
+ /**
+ * Set a live field to a double value to be displayed in the console.
+ * @param fieldName The name of the field under which the value should be displayed.
+ * @param value The value to put to the console.
+ */
+ public void setField (String fieldName, double value) {
+ setField(fieldName, Double.toString(value));
+ }
+
+ /**
+ * Set a live field to a boolean value to be displayed in the console.
+ * @param fieldName The name of the field under which the value should be displayed.
+ * @param value The value to put to the console.
+ */
+ public void setField (String fieldName, boolean value) {
+ setField(fieldName, Boolean.toString(value));
+ }
+
+ /**
+ * Set a live field to an integer value to be displayed in the console.
+ * @param fieldName The name of the field under which the value should be displayed.
+ * @param value The value to put to the console.
+ */
+ public void setField (String fieldName, int value) {
+ setField(fieldName, Integer.toString(value));
+ }
+
+ /**
+ * Update all the fields in the console to display the latest values.
+ * @param console
+ */
+ public void update (ConsoleManager console) {
+ synchronized (fieldsLock) {
+
+ // Do nothing if no fields have been changed
+ if (updatedFields.size() == 0 && newFieldNames.size() == 0) {
+ return;
+ }
+
+ // Move up to the top of the preexisting lines
+ int preexistingLines = fields.size() - newFieldNames.size();
+ console.moveUp(preexistingLines);
+
+ // Iterate through all existing fields
+ for (int i = 0; i < fields.size(); i ++) {
+ String fieldName = fields.get(i);
+ String value = values.get(i);
+
+ // Check for no-change vs. updated vs. new field
+ if (newFieldNames.contains(fieldName)) {
+
+ // New field
+ printField(console, fieldName, value);
+
+ } else if (updatedFields.contains(fieldName)) {
+
+ // Updated field
+ console.clearLine();
+ printField(console, fieldName, value);
+
+ } else {
+
+ // No change to field
+ console.moveUp(-1);
+
+ }
+
+ }
+
+ // Clear updated fields and new fields
+ newFieldNames.clear();
+ updatedFields.clear();
+
+ }
+ }
+
+ /**
+ * Print a single field to the console
+ */
+ private static void printField (ConsoleManager console, String fieldName, String value) {
+ String space = " ".repeat(Math.max(0, 18 - fieldName.length()));
+ console.println(fieldName + " : " + space + value);
+ }
+
+}
\ No newline at end of file
diff --git a/lib/app/src/main/java/claw/RobotErrorLog.java b/lib/app/src/main/java/claw/RobotErrorLog.java
new file mode 100644
index 0000000..e72b144
--- /dev/null
+++ b/lib/app/src/main/java/claw/RobotErrorLog.java
@@ -0,0 +1,114 @@
+package claw;
+
+import java.io.PrintWriter;
+import java.io.Serializable;
+import java.io.StringWriter;
+import java.sql.Date;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+
+import claw.rct.commands.CommandProcessor;
+import claw.rct.commands.CommandReader;
+import claw.rct.commands.CommandProcessor.BadCallException;
+import claw.rct.network.low.ConsoleManager;
+
+public class RobotErrorLog {
+
+ private static record LoggableError (long timestamp, String text, ErrorType type) implements Serializable { }
+
+ public static final CommandProcessor ERROR_LOG_COMMAND_PROCESSOR = new CommandProcessor(
+ "errlog",
+ "errlog",
+ "Use 'errlog' to view logged robot errors and warnings through the RobotErrorLog class.",
+ RobotErrorLog::errorLogCommand
+ );
+
+ private static final Setting> ERROR_LOG_SETTING = new Setting<>("CLAW.ERROR_LOG", () -> new ArrayList<>());
+ private static final Object ERROR_LOG_LOCK = new Object();
+
+ private static void errorLogCommand (ConsoleManager console, CommandReader reader) throws BadCallException {
+ // Copy an array of errors from the error log setting
+ LoggableError[] errors;
+ synchronized (ERROR_LOG_LOCK) {
+ errors = ERROR_LOG_SETTING.get().toArray(new LoggableError[0]);
+ }
+
+ if (errors.length > 0) {
+ console.println("There are "+errors.length+" logged error(s).");
+
+ for (LoggableError error : errors) {
+ console.println(" -- " + error.type.name + " -- ");
+ console.println(new SimpleDateFormat("MM/dd/yyyy hh:mm:ss a").format(new Date(error.timestamp)));
+
+ String text = ConsoleManager.formatMessage(error.text);
+
+ switch (error.type) {
+ case FATAL_ERROR:
+ case THREAD_ERROR:
+ console.printlnErr(text);
+ break;
+ case WARNING:
+ console.printlnSys(text);
+ break;
+ }
+
+ console.println("\n");
+
+ }
+
+ } else {
+ console.println("There are no logged errors.");
+ }
+ }
+
+ private static void logError (LoggableError error) {
+ synchronized (ERROR_LOG_LOCK) {
+ ERROR_LOG_SETTING.get().add(error);
+ ERROR_LOG_SETTING.save();
+ }
+ }
+
+ /**
+ * Log a fatal error (an error which kills the robot code). This method for logging an error
+ * should only be used internally in CLAW.
+ * @param exception The exception to log.
+ */
+ public static void logFatalError (Throwable exception) {
+ logError(new LoggableError(System.currentTimeMillis(), getStackTrace(exception), ErrorType.FATAL_ERROR));
+ }
+
+ /**
+ * Log an error which kills some thread of the robot code. This method for logging an error
+ * should only be used internally in CLAW.
+ * @param exception The exception to log.
+ */
+ public static void logThreadError (Throwable exception) {
+ logError(new LoggableError(System.currentTimeMillis(), getStackTrace(exception), ErrorType.THREAD_ERROR));
+ }
+
+ /**
+ * Log a warning with a given description.
+ * @param description The warning's description.
+ */
+ public static void logWarning (String description) {
+ logError(new LoggableError(System.currentTimeMillis(), description, ErrorType.WARNING));
+ }
+
+ private static String getStackTrace (Throwable e) {
+ StringWriter stringWriter = new StringWriter();
+ e.printStackTrace(new PrintWriter(stringWriter));
+ return stringWriter.toString();
+ }
+
+ private enum ErrorType {
+ FATAL_ERROR ("Fatal Error"),
+ THREAD_ERROR ("Thread Error"),
+ WARNING ("Warning");
+
+ private final String name;
+ private ErrorType (String name) {
+ this.name = name;
+ }
+ }
+
+}
diff --git a/lib/app/src/main/java/claw/hardware/AbsoluteEncoderDevice.java b/lib/app/src/main/java/claw/hardware/AbsoluteEncoderDevice.java
deleted file mode 100644
index af932d2..0000000
--- a/lib/app/src/main/java/claw/hardware/AbsoluteEncoderDevice.java
+++ /dev/null
@@ -1,68 +0,0 @@
-package claw.hardware;
-
-import claw.Setting;
-import edu.wpi.first.math.geometry.Rotation2d;
-
-/**
- * A device wrapper for an absolute encoder, allowing for reading from the encoder and configuring its
- * offset from zero.
- */
-public class AbsoluteEncoderDevice extends Device {
-
- private final Setting offsetSetting;
- private final EncoderReader encoderReader;
-
- /**
- * Create a new {@link AbsoluteEncoderDevice}.
- * @param deviceName The unique identifier for the device.
- * @param initializer The initializer for the device.
- * @param finalizer The finalizer for the device.
- * @param offsetSetting A {@link Setting} field which can be used to store the encoder's absolute offset.
- * @param encoderReader A function which can read a {@link Rotation2d} from a provided encoder device.
- * @see Device#Device(String, DeviceInitializer, DeviceFinalizer)
- */
- public AbsoluteEncoderDevice (
- String deviceName,
- DeviceInitializer initializer,
- DeviceFinalizer finalizer,
- Setting offsetSetting,
- EncoderReader encoderReader
- ) {
- super(deviceName, initializer, finalizer);
- this.offsetSetting = offsetSetting;
- this.encoderReader = encoderReader;
- }
-
- /**
- * An functional interface which reads a rotation from an encoder.
- */
- @FunctionalInterface
- public static interface EncoderReader {
- /**
- * Read a rotation from a given encoder device.
- * @param encoderDevice The encoder which the rotation should be read from.
- * @return The {@link Rotation2d} representing the encoder's current reading.
- */
- public Rotation2d getRotation (T encoderDevice);
- }
-
- /**
- * Get the absolute rotation of the encoder after accounting for the configured offset.
- * @return A {@link Rotation2d} describing the state of the encoder.
- */
- public Rotation2d getRotationMeasurement () {
- // Reading = Supplier + Offset
- return Rotation2d.fromDegrees(encoderReader.getRotation(get()).getDegrees() + offsetSetting.get());
- }
-
- /**
- * Configures the saved offset so that the reading of the absolute encoder using {@link #getRotationMeasurement()} will,
- * at this position, be equal to the provided {@code targetRotation}.
- * @param targetRotation What this encoder should be reading at the current position.
- */
- public void configureOffset (Rotation2d targetRotation) {
- // Offset = Reading - Supplier
- offsetSetting.set(targetRotation.getDegrees() - encoderReader.getRotation(get()).getDegrees());
- }
-
-}
diff --git a/lib/app/src/main/java/claw/hardware/DIOReadCommand.java b/lib/app/src/main/java/claw/hardware/DIOReadCommand.java
new file mode 100644
index 0000000..f1e103d
--- /dev/null
+++ b/lib/app/src/main/java/claw/hardware/DIOReadCommand.java
@@ -0,0 +1,78 @@
+package claw.hardware;
+
+import java.util.Optional;
+
+import claw.LiveValues;
+import claw.rct.commands.CommandProcessor;
+import claw.rct.commands.CommandReader;
+import claw.rct.commands.CommandProcessor.BadCallException;
+import claw.rct.network.low.ConsoleManager;
+import edu.wpi.first.hal.util.AllocationException;
+import edu.wpi.first.wpilibj.DigitalInput;
+
+public class DIOReadCommand {
+
+ private static final int DIO_PORTS_TOTAL = 10;
+
+ public static final CommandProcessor DIO_RAW_COMMAND_PROCESSOR = new CommandProcessor(
+ "dioraw",
+ "dioraw",
+ "Use 'dioraw' to enumerate DIO ports and list their current values. This may be helpful to distinguish between different " +
+ "devices if cables are poorly labeled on the roboRIO, or to quickly tell if a device is functioning properly.",
+ DIOReadCommand::dioRawCommandFunction
+ );
+
+ private static void dioRawCommandFunction (ConsoleManager console, CommandReader reader) throws BadCallException {
+ reader.allowNone();
+
+ DIOPort[] ports = new DIOPort[DIO_PORTS_TOTAL];
+ for (int i = 0; i < ports.length; i ++) {
+ ports[i] = new DIOPort(i);
+ }
+
+ LiveValues values = new LiveValues();
+
+ while (!console.hasInputReady()) {
+
+ for (int i = 0; i < ports.length; i ++) {
+ values.setField("DIO["+i+"]", ports[i].toString());
+ }
+
+ values.update(console);
+
+ }
+
+ for (int i = 0; i < ports.length; i ++) {
+ ports[i].free();
+ }
+
+ }
+
+ private static class DIOPort {
+
+ private Optional digitalInput = Optional.empty();
+
+ private DIOPort (int port) {
+ try {
+ digitalInput = Optional.of(new DigitalInput(port));
+ } catch (AllocationException e) {
+ // Ignore AllocationExceptions
+ }
+ }
+
+ @Override
+ public String toString () {
+ if (digitalInput.isEmpty()) {
+ return "Port unavailable (already allocated)";
+ } else {
+ return Boolean.toString(digitalInput.get().get());
+ }
+ }
+
+ public void free () {
+ digitalInput.ifPresent(DigitalInput::close);
+ }
+
+ }
+
+}
diff --git a/lib/app/src/main/java/claw/hardware/Device.java b/lib/app/src/main/java/claw/hardware/Device.java
deleted file mode 100644
index 6e07087..0000000
--- a/lib/app/src/main/java/claw/hardware/Device.java
+++ /dev/null
@@ -1,252 +0,0 @@
-package claw.hardware;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-
-import claw.Setting;
-import claw.logs.CLAWLogger;
-
-/**
- * A thread-safe wrapper around any hardware components which are connected to the roboRIO on a certain port or with a given ID.
- * The core functionality of the {@code Device} wrapper is initializing a device given an ID. IDs will be read from CLAW settings,
- * so they can be set via the Robot Control Terminal.
- *
- * Examples of good components to use with this device wrapper could be resources connected to the roboRIO via CAN, DIO, or PWM (e.g.
- * motor controllers, limit switches, CANCoders, or anything else which has some sort of integer ID).
- */
-public class Device {
-
- private static final CLAWLogger LOG = CLAWLogger.getLogger("claw.devices");
-
- /**
- * A map of unique device names onto their set IDs, which is saved to the roboRIO so that the IDs are persistent.
- */
- private static final Setting> SAVED_DEVICE_NAMES_TO_IDS = new Setting<>("claw.devices", HashMap::new);
-
- /**
- * A map of existing device names onto devices.
- */
- private static final HashMap> NAMES_TO_DEVICES = new HashMap<>();
-
- private final String deviceName;
- private final DeviceInitializer initializer;
- private final DeviceFinalizer finalizer;
-
- /**
- * The currently instantiated device resource (like a motor controller or digital input, for example).
- */
- private Optional device = Optional.empty();
-
- /**
- * A lock for {@code device} so that it isn't used in more than one thread at a time.
- */
- private final Object deviceLock = new Object();
-
- /**
- * Create a new device wrapper with a given initializer, finalizer, and name.
- * @param deviceName A unique, user-friendly string which can be used to identify this device.
- * No two {@link Device}s should be initialized with the same {@code deviceName} throughout the runtime
- * of a program. Also, it should be noted that if this name changes for a particular device, you will need
- * to reset its ID through the robot control terminal. It is common to use the format {@code "PORTTYPE.DEVTYPE.SUBSYSTEM.IDENTIFIER"}, where
- * PORTTYPE is the ID system to use (e.g. CAN, DIO, PWM), DEVTYPE is the type of device used (e.g. motor controller, limit switch),
- * SUBSYSTEM is the name of the subsystem the device belongs to, and IDENTIFIER is name which will distinguish the
- * device from other similar devices in the subsystem (based on location or purpose).
- * A sample {@code deviceName} could be {@code "CAN.ENCODER.SWERVE.FRONT_LEFT"}.
- * @param initializer A {@link DeviceInitializer} which accepts an integer device ID and returns the underlying
- * device object (e.g. a motor controller or digital input). It may be useful to also apply some basic settings to
- * the device in this initializer, such as setting a motor controller's default brake mode to coast, or resetting
- * a relative encoder.
- * @param finalizer A {@link DeviceFinalizer} which finalizes a device before and puts it in a safe state before
- * the resource is released (e.g. stopping a motor controller). It may be possible that the finalizer will perform no
- * operations on the device.
- */
- public Device (String deviceName, DeviceInitializer initializer, DeviceFinalizer finalizer) {
-
- // Handle the NAMES_TO_DEVICES set
- synchronized (NAMES_TO_DEVICES) {
- // Throw an exception if a device already has the provided name
- if (NAMES_TO_DEVICES.containsKey(deviceName))
- throw new IllegalArgumentException("A device has already been instantiated with the name '"+deviceName+"'");
-
- // Add the device name to the set
- NAMES_TO_DEVICES.put(deviceName, this);
- }
-
- // Set instance fields
- this.deviceName = deviceName;
- this.initializer = initializer;
- this.finalizer = finalizer;
- }
-
- /**
- * Get all instantiated devices' names. This may or may not differ from the
- * set of device names associated with a saved ID.
- * @return The {@code Set} of all instantiated {@link Device}s' names.
- * @see #getAllSavedDeviceIDs()
- */
- @SuppressWarnings("unchecked")
- public static Set getAllDeviceNames () {
- synchronized (NAMES_TO_DEVICES) {
- return ((HashMap>)NAMES_TO_DEVICES.clone()).keySet();
- }
- }
-
- /**
- * Get a map of device names onto device IDs representing what is currently saved to the roboRIO. The
- * device names may or may not differ from the set of names belonging to devices which have actually
- * been instantiated.
- * @return A {@code Map} read from the roboRIO, mapping device names onto
- * saved device IDs.
- * @see #getAllDeviceNames()
- */
- @SuppressWarnings("unchecked")
- public static Map getAllSavedDeviceIDs () {
- synchronized (SAVED_DEVICE_NAMES_TO_IDS) {
- return (Map)SAVED_DEVICE_NAMES_TO_IDS.get().clone();
- }
- }
-
- /**
- * Clear all device names and IDs saved to the roboRIO.
- * @return {@code true} if the save was successful, {@code false} otherwise.
- */
- public static boolean clearAllSavedIDs () {
- synchronized (SAVED_DEVICE_NAMES_TO_IDS) {
- SAVED_DEVICE_NAMES_TO_IDS.get().clear();
- return SAVED_DEVICE_NAMES_TO_IDS.save();
- }
- }
-
- /**
- * Attempt to save a new integer ID to a particular device. This will be saved to the roboRIO such that the
- * device ID will be persistent between program instances (i.e. if you restart your robot code or turn the robot off,
- * the device ID will still be saved with the given value). Notably, the change in ID will not take effect on any existing
- * {@link Device} instances. In order for devices IDs to be refreshed according to their saved values, use {@link #reinitialize()}.
- * If {@code id} is {@code Optional.empty()}, then the device's saved ID will be cleared.
- * @param deviceName The device's unique name.
- * @param id An {@link Optional} containing The new device ID. If the optional is empty, the device's saved ID will be cleared.
- * @return {@code true} if the ID was successfully saved, {@code false} otherwise.
- */
- public static boolean saveDeviceID (String deviceName, Optional id) {
- synchronized (SAVED_DEVICE_NAMES_TO_IDS) {
-
- if (id.isPresent()) {
- // If an ID was supplied, save the new value
- SAVED_DEVICE_NAMES_TO_IDS.get().put(deviceName, id.get());
- } else {
- // If no ID was supplied, clear the entry
- SAVED_DEVICE_NAMES_TO_IDS.get().remove(deviceName);
- }
-
- return SAVED_DEVICE_NAMES_TO_IDS.save();
- }
- }
-
- /**
- * Get the saved ID for a particular device's name.
- * @param deviceName The device name to retrieve the ID for.
- * @return The device's saved ID, if it could be found.
- */
- public static Optional getDeviceId (String deviceName) {
- synchronized (SAVED_DEVICE_NAMES_TO_IDS) {
- // Retrieve the boxed Integer ID from the settings, and convert to an optional
- return Optional.ofNullable(SAVED_DEVICE_NAMES_TO_IDS.get().get(deviceName));
- }
- }
-
- /**
- * Call {@link #reinitialize()} on all existing devices. Note that this could cause damage
- * to the robot if called while devices are being operated. Ensure the robot is disabled before
- * calling this method.
- */
- public static void reinitializeAllDevices () {
- synchronized (NAMES_TO_DEVICES) {
- for (Device> device : NAMES_TO_DEVICES.values()) {
- device.reinitialize();
- }
- }
- }
-
- /**
- * Initialize a new device according to the ID in settings
- */
- private T initializeDevice () {
- Optional id = getDeviceId(deviceName);
-
- // If the ID was not found in the settings, log a warning
- if (id.isEmpty())
- LOG.out("Warning: No device ID was found for device name '"+deviceName+"', defaulting to 0");
-
- // Return the device initialized with the given ID
- return initializer.initializeDevice(id.orElse(0));
- }
-
- /**
- * Retrieve the underlying resource belonging to this {@link Device}. If the device resource has already been initialized according
- * to the ID saved to the CLAW settings, then it will not be reinitialized. To reinitialize the device, use {@link #reinitialize()}.
- * @return The underlying resource (e.g. a motor controller or digital input).
- */
- public T get () {
- synchronized (deviceLock) {
- // If the device has not yet been initialized, initialize it
- if (device.isEmpty())
- device = Optional.of(initializeDevice());
-
- // Return the new device
- return device.get();
- }
- }
-
- /**
- * Reinitialize the device resource according to the ID saved to the CLAW settings. This should only be called
- * when the device isn't actively being operated.
- */
- public void reinitialize () {
- synchronized (deviceLock) {
- // Finalize the device if it exists
- if (device.isPresent())
- finalizer.finalizeDevice(device.get());
-
- // Initialize the new device
- device = Optional.of(initializeDevice());
- }
- }
-
- /**
- * Get the unique name associated with this device.
- * @return The device's name.
- */
- public String getName () {
- return deviceName;
- }
-
- /**
- * A functional interface which handles initializing a particular device given an integer ID.
- * Generally, this is something like a device connected to the roboRIO via CAN, DIO, or PWM.
- */
- @FunctionalInterface
- public static interface DeviceInitializer {
- /**
- * Retrieve and initialize a device of type {@code T} given an integer ID.
- * @param id The ID of the device (potentially a CAN ID, DIO port, etc.)
- * @return The device.
- */
- public T initializeDevice (int id);
- }
-
- /**
- * A functional interface which handles finalizing a device (i.e. putting it into a safe state
- * and releasing any underlying resources).
- */
- @FunctionalInterface
- public static interface DeviceFinalizer {
- /**
- * Finalize the device such that it is in a safe state and any associated resources are released.
- * @param device The device to finalize.
- */
- public void finalizeDevice (T device);
- }
-
-}
diff --git a/lib/app/src/main/java/claw/hardware/LimitSwitchDevice.java b/lib/app/src/main/java/claw/hardware/LimitSwitchDevice.java
index a68529c..bb2a14f 100644
--- a/lib/app/src/main/java/claw/hardware/LimitSwitchDevice.java
+++ b/lib/app/src/main/java/claw/hardware/LimitSwitchDevice.java
@@ -3,19 +3,19 @@
import edu.wpi.first.wpilibj.DigitalInput;
/**
- * An implementation of {@link Device} for limit switches connected via DIO.
+ * A wrapper around {@link DigitalInput} for reading a value from a limit switch.
*/
-public class LimitSwitchDevice extends Device {
+public class LimitSwitchDevice implements AutoCloseable {
+ private final DigitalInput limitSwitchInput;
private final NormalState normalState;
/**
* Create a new {@link LinkSwitchDevice}.
- * @param deviceName The unique device name.
* @param normalState The {@link NormalState} of the limit switch's circuit.
*/
- public LimitSwitchDevice (String deviceName, NormalState normalState) {
- super(deviceName, DigitalInput::new, DigitalInput::close);
+ public LimitSwitchDevice (DigitalInput limitSwitchInput, NormalState normalState) {
+ this.limitSwitchInput = limitSwitchInput;
this.normalState = normalState;
}
@@ -24,7 +24,7 @@ public LimitSwitchDevice (String deviceName, NormalState normalState) {
* @return {@code true} if the limit switch is pressed.
*/
public boolean isPressed () {
- return (normalState == NormalState.NORMALLY_OPEN) == get().get();
+ return (normalState == NormalState.NORMALLY_OPEN) != limitSwitchInput.get();
}
/**
@@ -33,17 +33,22 @@ public boolean isPressed () {
public enum NormalState {
/**
* For a normally open limit switch, if the limit switch is not pressed, the circuit will be open,
- * and so a low signal will be received through DIO. Because of this, if the DIO is unplugged,
- * the input to the roboRIO will indicate that the limit switch is not pressed.
+ * and so a high signal (5V) will be received through DIO. Because of this, if the DIO is unplugged,
+ * the input to the roboRIO will indicate that the limit switch is pressed.
*/
NORMALLY_OPEN,
/**
* For a normally closed limit switch, if the limit switch is not pressed, the circuit will be closed,
- * and so a high signal will be received through DIO. Because of this, if the DIO is unplugged,
- * the input to the roboRIO will indicate that the limit switch is pressed.
+ * and so a low signal (0V) will be received through DIO. Because of this, if the DIO is unplugged,
+ * the input to the roboRIO will indicate that the limit switch is not pressed.
*/
NORMALLY_CLOSED,
}
+ @Override
+ public void close () {
+ limitSwitchInput.close();
+ }
+
}
diff --git a/lib/app/src/main/java/claw/hardware/can/CANMessageID.java b/lib/app/src/main/java/claw/hardware/can/CANMessageID.java
new file mode 100644
index 0000000..f10c08e
--- /dev/null
+++ b/lib/app/src/main/java/claw/hardware/can/CANMessageID.java
@@ -0,0 +1,267 @@
+package claw.hardware.can;
+
+/**
+ * Represents the arbitration ID of a message received over CAN. See the FRC CAN Device Specifications for details on formatting:
+ * https://docs.wpilib.org/en/stable/docs/software/can-devices/can-addressing.html
+ */
+public record CANMessageID (byte deviceNum, byte apiIndex, byte apiClass, ManufacturerCode manufacturer, DeviceType deviceType) {
+
+ /**
+ * Parse a {@link CANMessageID} from an integer message ID.
+ * @param fullArbitrationId The full message ID / arbitration ID.
+ * @return The parsed {@code CANMessageID}.
+ */
+ public static CANMessageID fromMessageId (int fullArbitrationId) {
+
+ // Break apart the arbitration ID into its components:
+ int[] bitSliceSizes = new int[] {
+ 6, // Device number
+ 4, // API Index
+ 6, // API Class
+ 8, // Manufacturer Code
+ 5, // Device Type
+ };
+
+ // Get the binary slices of the message ID corresponding with each useful segment (device number, api index, etc.)
+ int[] bitSlices = getBinarySlices(fullArbitrationId, bitSliceSizes);
+
+ // Get the byte fields
+ byte deviceNum = (byte)bitSlices[0];
+ byte apiIndex = (byte)bitSlices[1];
+ byte apiClass = (byte)bitSlices[2];
+
+ // Parse a ManufacturerCode from bitSlices[3]
+ ManufacturerCode manufacturer = getEnumFromId(
+ ManufacturerCode.class,
+ bitSlices[3],
+ ManufacturerCode.UNKNOWN
+ );
+
+ // Parse a DeviceType from bitSlices[4]
+ DeviceType deviceType = getEnumFromId(
+ DeviceType.class,
+ bitSlices[4],
+ DeviceType.UNKNOWN
+ );
+
+ return new CANMessageID(deviceNum, apiIndex, apiClass, manufacturer, deviceType);
+
+ }
+
+ /**
+ * Get an array of binary slices from a value.
+ * @param value The value to take binary slices from
+ * @param sliceSizes The size of each slice. {@code slice[0]} will be taken from the far right of the value.
+ * @return An array of binary slices corresponding with the given {@code sliceSizes} array
+ */
+ private static int[] getBinarySlices (int value, int[] sliceSizes) {
+ int[] bitSlices = new int[sliceSizes.length];
+
+ int startIndex = 0;
+ for (int i = 0; i < bitSlices.length; i ++) {
+ bitSlices[i] = getBinarySliceFrom(value, startIndex, sliceSizes[i]);
+ startIndex += sliceSizes[i];
+ }
+
+ return bitSlices;
+ }
+
+ /**
+ * Gets a binary slice from a given value.
+ * @param value The value to retrieve the binary slice from.
+ * @param startIndex The index of the first bit to retrieve, starting at the 1's place (index 0).
+ * @param length The number of bits to retrieve from the given {@code value}.
+ * @return The section of bits retrieved from the {@code value}, shifted so the 1's place
+ * contains the rightmost bit in the slice.
+ */
+ private static int getBinarySliceFrom (int value, int startIndex, int length) {
+ // This creates an int bitMask which, when viewed in binary representation,
+ // looks like 1111110000, where the parameter "length" describes the number of
+ // 1 bits, and the parameter "startIndex" describes the number of 0 bits
+ int bitMask = ~(-1 << length) << startIndex;
+
+ // The bitMask can be ANDed with "value" to get only the bits we want from it.
+ // We shift the bits back to the right to undo the "startIndex"
+ int maskedNum = (bitMask & value) >>> startIndex;
+
+ return maskedNum;
+ }
+
+ /**
+ * Internal, helper interface. Useful for finding enum options that correspond with some given integer ID
+ */
+ private interface EnumWithID {
+ public int getMinId ();
+ public int getMaxId ();
+ }
+
+ /**
+ * Helps to find an enum option which corresponds with some given integer ID
+ */
+ private static T getEnumFromId (Class enumClass, int id, T defaultOption) {
+ T[] enumOptions = enumClass.getEnumConstants();
+ for (int i = 0; i < enumOptions.length; i ++) {
+ if (id >= enumOptions[i].getMinId() && id <= enumOptions[i].getMaxId()) {
+ return enumOptions[i];
+ }
+ }
+
+ return defaultOption;
+ }
+
+ /**
+ * A named device type according to the FRC CAN Device Specifications.
+ */
+ public static enum DeviceType implements EnumWithID {
+ BROADCAST_MESSAGES (0),
+ ROBOT_CONTROLLER (1),
+ MOTOR_CONTROLLER (2),
+ RELAY_CONTROLLER (3),
+ GYRO_SENSOR (4),
+ ACCELEROMETER (5),
+ ULTRASONIC_SENSOR (6),
+ GEAR_TOOTH_SENSOR (7),
+ POWER_DISTRIBUTION_MODULE (8),
+ PNEUMATICS_CONTROLLER (9),
+ MISCELLANEOUS (10),
+ IO_BREAKOUT (11),
+ RESERVED (12, 30),
+ FIRMWARE_UPDATE (31),
+ UNKNOWN (-1);
+
+ @Override
+ public int getMinId () {
+ return minId;
+ }
+
+ @Override
+ public int getMaxId () {
+ return maxId;
+ }
+
+ private final int minId, maxId;
+ private DeviceType (int num) {
+ minId = num;
+ maxId = num;
+ }
+
+ private DeviceType (int minId, int maxId) {
+ this.minId = minId;
+ this.maxId = maxId;
+ }
+ }
+
+ /**
+ * A manufacturer according to the FRC CAN Device Specifications.
+ */
+ public static enum ManufacturerCode implements EnumWithID {
+ BROADCAST (
+ "[Broadcast]",
+ 0
+ ),
+
+ NI (
+ "National Instruments",
+ 1
+ ),
+
+ LUMINARY_MICRO (
+ "Luminary Micro",
+ 2
+ ),
+
+ DEKA (
+ "DEKA Research and Development",
+ 3
+ ),
+
+ CTR_ELECTRONICS (
+ "Cross the Road Electronics",
+ 4
+ ),
+
+ REV_ROBOTICS (
+ "REV Robotics",
+ 5
+ ),
+
+ GRAPPLE (
+ "Grapple",
+ 6
+ ),
+
+ MIND_SENSORS (
+ "MindSensors",
+ 7
+ ),
+
+ TEAM_USE (
+ "[Team Use]",
+ 8
+ ),
+
+ KAUAI_LABS (
+ "Kauai Labs",
+ 9
+ ),
+
+ COPPERFORGE (
+ "Copperforge",
+ 10
+ ),
+
+ PLAYING_WITH_FUSION (
+ "Playing With Fusion",
+ 11
+ ),
+
+ STUDICA (
+ "Studica",
+ 12
+ ),
+
+ THE_THRIFTY_BOT (
+ "The Thrifty Bot",
+ 13
+ ),
+
+ RESERVED (
+ "[Reserved]",
+ 14, 255
+ ),
+
+ UNKNOWN (
+ "[Unknown]",
+ -1
+ );
+
+ @Override
+ public int getMinId () {
+ return minId;
+ }
+
+ @Override
+ public int getMaxId () {
+ return maxId;
+ }
+
+ private final int minId, maxId;
+
+ /**
+ * A more readable name for the device type than the enum name.
+ */
+ public final String friendlyName;
+ private ManufacturerCode (String name, int num) {
+ this.friendlyName = name;
+ minId = num;
+ maxId = num;
+ }
+
+ private ManufacturerCode (String name, int minId, int maxId) {
+ this.friendlyName = name;
+ this.minId = minId;
+ this.maxId = maxId;
+ }
+ }
+
+}
diff --git a/lib/app/src/main/java/claw/hardware/can/CANScanner.java b/lib/app/src/main/java/claw/hardware/can/CANScanner.java
new file mode 100644
index 0000000..fd72ba9
--- /dev/null
+++ b/lib/app/src/main/java/claw/hardware/can/CANScanner.java
@@ -0,0 +1,215 @@
+package claw.hardware.can;
+
+import java.nio.ByteBuffer;
+
+import java.nio.ByteOrder;
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+
+import claw.LiveValues;
+import claw.hardware.can.CANMessageID.DeviceType;
+import claw.hardware.can.CANMessageID.ManufacturerCode;
+import claw.rct.commands.CommandProcessor;
+import claw.rct.commands.CommandReader;
+import claw.rct.commands.CommandProcessor.BadCallException;
+import claw.rct.network.low.ConsoleManager;
+import edu.wpi.first.hal.can.CANJNI;
+import edu.wpi.first.hal.can.CANMessageNotFoundException;
+import edu.wpi.first.hal.can.CANStatus;
+import edu.wpi.first.math.filter.Debouncer;
+import edu.wpi.first.math.filter.LinearFilter;
+import edu.wpi.first.math.filter.Debouncer.DebounceType;
+
+/**
+ * A utility class which can detect devices on, send messages to, and read messages from the CAN bus.
+ */
+public class CANScanner {
+
+ /**
+ * A message read from the CAN bus.
+ */
+ public static record CANMessage (CANMessageID messageID, byte[] messageData, long timestamp) { }
+
+ /**
+ * A trace for a device connected to the roboRIO via CAN.
+ */
+ public static record CANDeviceTrace (DeviceType deviceType, ManufacturerCode manufacturer, int deviceNum) { }
+
+ /**
+ * A command processor for the {@code canscan} command.
+ */
+ public static final CommandProcessor CAN_SCAN_COMMAND_PROCESSOR = new CommandProcessor(
+ "canscan",
+ "canscan [status | devices]",
+ "Use 'canscan status' to get the status of the CAN bus (bus utilization and presence of errors). " +
+ "'canscan devices' will scan to detect devices on the CAN bus. It reads manufacturers, device types, " +
+ "and device numbers (IDs).",
+ CANScanner::canScanCommand
+ );
+
+ private static double roundTo (double value, int precision) {
+ return Math.round(value * precision) / precision;
+ }
+
+ private static String padToSize (String str, int size) {
+ int totalPadding = Math.max(size - str.length(), 0);
+
+ int leftPadding = totalPadding / 2;
+ int rightPadding = totalPadding - leftPadding;
+
+ return " ".repeat(leftPadding) + str + " ".repeat(rightPadding);
+ }
+
+ private static void canScanCommand (ConsoleManager console, CommandReader reader) throws BadCallException {
+ String scanType = reader.readArgOneOf("scan type", "Expected a scan type of 'status' or 'devices'.", "status", "devices");
+ reader.noMoreArgs();
+ reader.allowNoOptions();
+ reader.allowNoFlags();
+
+ LiveValues values = new LiveValues();
+
+ if (scanType.equals("status")) {
+
+ LinearFilter canUtilizationFilter = LinearFilter.movingAverage(35);
+ Debouncer receiveErrorDebouncer = new Debouncer(0.1, DebounceType.kFalling);
+ Debouncer transmitErrorDebouncer = new Debouncer(0.1, DebounceType.kFalling);
+
+ while (!console.hasInputReady()) {
+
+ CANStatus canStatusReading = getCANStatus();
+ double percentBusUtilization = canUtilizationFilter.calculate(canStatusReading.percentBusUtilization) * 100;
+
+ boolean hasReceiveError = receiveErrorDebouncer.calculate(canStatusReading.receiveErrorCount > 0);
+ boolean hasTransmitError = transmitErrorDebouncer.calculate(canStatusReading.transmitErrorCount > 0);
+
+ values.setField("Bus Utilization", roundTo(percentBusUtilization, 1000) + "%");
+ values.setField("Receive Error", hasReceiveError ? "Present" : "None");
+ values.setField("Transmit Error", hasTransmitError ? "Present" : "None");
+
+ values.update(console);
+ }
+
+ } else if (scanType.equals("devices")) {
+
+ // TODO: Send an "enumerate" CAN frame, write a wrapper around the FRC_Net_Comm_Mux functions,
+ // filter out bad data, remove System.out printing, and clean up CANMessageIDs
+
+ console.printlnSys("Scanning...");
+ console.flush();
+
+ console.printlnSys(
+ padToSize("Manufacturer", 35) +
+ padToSize("Device Type", 35) +
+ padToSize("Device Number", 20)
+ );
+
+ Set devices = scanCANDevices(600);
+ for (CANDeviceTrace device : devices) {
+ console.println(
+ padToSize(device.manufacturer.friendlyName, 35) +
+ padToSize(device.deviceType+"", 35) +
+ padToSize(device.deviceNum+"", 20)
+ );
+ }
+
+ }
+
+ }
+
+ /**
+ * Get the status {@link CANStatus} of the CAN bus.
+ * @return The {@code CANStatus} describing the state of the CAN bus.
+ */
+ public static CANStatus getCANStatus () {
+ CANStatus status = new CANStatus();
+ CANJNI.getCANStatus(status);
+ return status;
+ }
+
+ /**
+ * Read a single message from the CAN bus (from any device).
+ * @return A new message received from the CAN bus.
+ */
+ public static Optional readMessage () {
+ return readMessage(0, 0);
+ }
+
+ /**
+ * Read a single message from the CAN bus. See the FRC CAN Device Specifications for
+ * the specifications on the message ID.
+ * https://docs.wpilib.org/en/stable/docs/software/can-devices/can-addressing.html
+ *
+ * Note that there are generally better options through WPILib for reading messages
+ * from the CAN bus. This would only be useful if you need to read a broad range of
+ * messages from varying IDs for some reason.
+ * @param messageID The arbitration ID of the messages to read. See the specifications.
+ * @param messageIDMask A bit mask to apply to messages IDs received from the CAN bus.
+ * The received message ID, after applying a bitwise {@code &} with the given mask, will be checked
+ * against the {@code messageID} argument to see if it should be intercepted.
+ * @return The CAN message intercepted from the bus, if one could be read.
+ */
+ public static Optional readMessage (int messageID, int messageIDMask) {
+
+ // Create a message ID buffer for distinguishing which messages to intercept
+ // This buffer will also be filled with the ID of the retrieved message once done
+ ByteBuffer messageIDBuffer = ByteBuffer.allocateDirect(4);
+ messageIDBuffer.order(ByteOrder.LITTLE_ENDIAN);
+ messageIDBuffer.clear();
+ messageIDBuffer.asIntBuffer().put(messageID, 0);
+
+ // Create a buffer to store the timestamp of the received message. This will be filled
+ // after receiving the message
+ ByteBuffer timestampBuffer = ByteBuffer.allocate(4);
+
+ byte[] messageContent;
+
+ try {
+ // Receive a message from the CAN bus and fill the messageIDBuffer and timestampBuffer
+ messageContent = CANJNI.FRCNetCommCANSessionMuxReceiveMessage(
+ messageIDBuffer.asIntBuffer(),
+ messageIDMask,
+ timestampBuffer
+ );
+ } catch (CANMessageNotFoundException e) {
+ // If no message was found, return the empty optional
+ return Optional.empty();
+ }
+
+ // TODO: Use timestamp ByteBuffer to get message timestamp
+ return Optional.of(new CANMessage(CANMessageID.fromMessageId(messageIDBuffer.asIntBuffer().get()), messageContent, 0));
+
+ }
+
+ public static Set scanCANDevices (int messagesToScan) {
+
+ // Create a set of CAN device traces to return
+ HashSet devices = new HashSet<>();
+
+ // TODO: Send the enumerate message or something else to encourage devices to identify themselves
+
+ // Scan through the given number of messages to find all device traces
+ for (int i = 0; i < messagesToScan; i ++) {
+ readMessage().ifPresent(msg -> {
+
+ // TODO: Filter out devices with non-device manufacturers or device types
+
+ // Add the device trace to the set
+ CANDeviceTrace deviceTrace = new CANDeviceTrace(
+ msg.messageID().deviceType(),
+ msg.messageID().manufacturer(),
+ msg.messageID().deviceNum()
+ );
+
+ devices.add(deviceTrace);
+
+ });
+ }
+
+ return devices;
+
+ }
+
+ private CANScanner () { }
+
+}
diff --git a/lib/app/src/main/java/claw/logs/LogHandler.java b/lib/app/src/main/java/claw/logs/LogHandler.java
index d514f11..f09dca6 100644
--- a/lib/app/src/main/java/claw/logs/LogHandler.java
+++ b/lib/app/src/main/java/claw/logs/LogHandler.java
@@ -6,8 +6,8 @@
import java.util.List;
import java.util.Set;
-import claw.rct.network.low.Waiter;
-import claw.rct.network.low.Waiter.NoValueReceivedException;
+import claw.rct.network.low.concurrency.Waiter;
+import claw.rct.network.low.concurrency.Waiter.NoValueReceivedException;
import claw.rct.network.messages.LogDataMessage;
import claw.rct.network.messages.LogDataMessage.LogData;
import claw.rct.remote.RCTServer;
diff --git a/lib/app/src/main/java/claw/math/DualDebouncer.java b/lib/app/src/main/java/claw/math/DualDebouncer.java
new file mode 100644
index 0000000..58a9800
--- /dev/null
+++ b/lib/app/src/main/java/claw/math/DualDebouncer.java
@@ -0,0 +1,99 @@
+package claw.math;
+
+import edu.wpi.first.math.filter.Debouncer;
+
+/**
+ * An extension of the WPILib {@link Debouncer} allowing for a different rising edge and falling edge debounce times.
+ * This {@code DualDebouncer} also allows for greater control over the internal state of the debouncer. For this reason,
+ * it may be useful to use a {@code DualDebouncer} even if different rising edge and falling edge debounce times are not
+ * required.
+ */
+public class DualDebouncer extends Debouncer {
+
+ private final double fallingEdgeTime, risingEdgeTime;
+
+ /**
+ * The current state of the debouncer (the "baseline", because the internalDebouncer is set up to
+ * only debounce the edge which would change this baselineState)
+ */
+ private boolean baselineState;
+
+ /**
+ * The internal debouncer, which is reset whenever the baseline changes (i.e. there will be a different
+ * internalDebouncer for falling edge vs. rising edge debouncing)
+ */
+ private Debouncer internalDebouncer;
+
+ /**
+ * Create a new {@link DualDebouncer} with separate falling edge and rising edge debounce times.
+ * @param baselineState The initial state of this debouncer.
+ * @param fallingEdgeTime The number of seconds the input must be changed to {@code false} when the
+ * baseline is {@code true} in order for the baseline to change.
+ * @param risingEdgeTime The number of seconds the input must be changed to {@code true} when the
+ * baseline is {@code false} in order for the baseline to change.
+ */
+ public DualDebouncer (boolean baselineState, double fallingEdgeTime, double risingEdgeTime) {
+ // This class only extends Debouncer so that it can be used in place
+ // of a WPILib Debouncer. Otherwise, it actually doesn't use inheritance
+ // to control the debounce filter at all.
+ super(0);
+
+ this.fallingEdgeTime = fallingEdgeTime;
+ this.risingEdgeTime = risingEdgeTime;
+
+ // Reset to the given baselineState
+ resetToBaseline(baselineState);
+ }
+
+ /**
+ * Create a new {@link DualDebouncer} with a given debounceTime to use for both the rising and falling edges.
+ * @param baselineState The initial state of this debouncer.
+ * @param debounceTime The number of seconds the input must be different than the baseline for before the
+ * baseline changes.
+ */
+ public DualDebouncer (boolean baselineState, double debounceTime) {
+ this(baselineState, debounceTime, debounceTime);
+ }
+
+ /**
+ * Reset the timer of the debouncer without changing the current baseline state.
+ */
+ public void resetTimer () {
+ resetToBaseline(baselineState);
+ }
+
+ /**
+ * Reset the timer of the debouncer and change the baseline state.
+ * @param newBaseline The new baseline state of the debouncer.
+ */
+ public void resetToBaseline (boolean newBaseline) {
+ // Reset the baseline state
+ baselineState = newBaseline;
+
+ // Set the internalDebouncer for the new baseline
+ internalDebouncer = newBaseline
+ // If the newBaseline is true, we debounce the falling edge
+ ? new Debouncer(fallingEdgeTime, DebounceType.kFalling)
+
+ // If the newBaseline is false, we debounce the rising edge
+ : new Debouncer(risingEdgeTime, DebounceType.kRising);
+ }
+
+ @Override
+ public boolean calculate (boolean input) {
+
+ // Again, we actually don't use any details from the superclass implementation of the debouncer.
+ // All debounce control is internal. We only extend the WPILib Debouncer so this DualDebouncer
+ // can be used in any place the WPILib Debouncer can be used
+
+ // If the internalDebouncer indicates that the baseline state must change,
+ // reset to the new baseline
+ if (internalDebouncer.calculate(input) != baselineState) {
+ resetToBaseline(!baselineState);
+ }
+
+ return baselineState;
+
+ }
+
+}
diff --git a/lib/app/src/main/java/claw/math/LinearInterpolator.java b/lib/app/src/main/java/claw/math/LinearInterpolator.java
new file mode 100644
index 0000000..39a4715
--- /dev/null
+++ b/lib/app/src/main/java/claw/math/LinearInterpolator.java
@@ -0,0 +1,115 @@
+package claw.math;
+
+import java.util.Arrays;
+
+/**
+ * A linear interpolator {@link Transform} which can take in x-coordinates and provide a corresponding y-coordinate
+ * according to a provided dataset.
+ */
+public class LinearInterpolator implements Transform {
+
+ /**
+ * A record representing a point on the x-y plane, which can be used to perform interpolation calculations.
+ */
+ public static record Point (double x, double y) { }
+
+ private final Point[] points;
+
+ /**
+ * Create a linear interpolator from the given points to interpolate between.
+ * @param points A varargs list of points to interpolate beween.
+ */
+ public LinearInterpolator (Point... points) {
+ // Create an array of the points, sorted by x value
+ Point[] sortedPoints = points.clone();
+ Arrays.sort(sortedPoints, (Point a, Point b) ->
+ a.x < b.x ? -1 : (a.x > b.x ? 1 : 0)
+ );
+
+ this.points = sortedPoints;
+ }
+
+ /**
+ * Create a linear interpolator from the given x and y coordinates representing points to interpolate between.
+ * @param xyPairs Pairs of {@code double} coordinates (x, y), (x, y), (x, y)...
+ * Because the given {@code xyPairs} represents pairs of x and y coordinates, an odd number of {@code xyPairs}
+ * values is invalid. For example, the input {@code new LinearInterpolator(1, 2, 3, 4, 5, 6)} would represent
+ * the points (1, 2), (3, 4) and (5, 6).
+ */
+ public LinearInterpolator (double... xyPairs) {
+ this(xyPairsToPoints(xyPairs));
+ }
+
+ private static Point[] xyPairsToPoints (double... xyPairs) {
+ // Only allow an even number of coordinates (otherwise, we'd be missing a y coordinate for the last point)
+ if (xyPairs.length % 2 != 0) {
+ throw new IllegalArgumentException("Missing a y coordinate for the last point (number of coordinates given must be even)");
+ }
+
+ // Create an array of points from the xyPairs list
+ Point[] points = new Point[xyPairs.length / 2];
+ for (int i = 0; i < points.length; i ++) {
+ points[i] = new Point(xyPairs[2*i], xyPairs[2*i + 1]);
+ }
+
+ return points;
+ }
+
+ /**
+ * Perform a linear interpolation on two points {@code a} and {@code b} in order to estimate a reasonable
+ * y-coordinate corresponding with the input x-coordinate {@code x}.
+ * @param x The input x-coordinate.
+ * @param a The first of two points which form the basis of the linear interpolation.
+ * @param b The second of two points which form the basis of the linear interpolation.
+ * @return An output y-coordinate corresponding to the input {@code x}.
+ */
+ public static double interpolate (double x, Point a, Point b) {
+ // If a and b have exactly the same x value, just get the mean of their y values
+ if (a.x == b.x) return (a.y + b.y) / 2;
+
+ // Calculate p. If p=0, (x,y) should be on point a, if p=1, (x,y) should be on point b
+ double p = (x - a.x) / (b.x - a.x);
+
+ // Given p, perform the linear interpolation between points a and b
+ return p * (b.y - a.y) + a.y;
+ }
+
+ /**
+ * Get a corresponding y coordinate for the given x coordinate by performing a linear interpolation betwen
+ * the points provided to this {@link LinearInterpolator}.
+ * @param x The x input coordinate.
+ * @return The corresponding y output coordinate.
+ */
+ @Override
+ public double apply (double x) {
+
+ // Return a constant value if there are exactly 0 or 1 given points, as all later calculations
+ // depends on there being at least two points
+ if (points.length == 0) return 0;
+ if (points.length == 1) return points[0].y;
+ final int lastIndex = points.length - 1;
+
+ // If x is further to the left than the first point, we interpolate between the first two points
+ if (x < points[0].x) return interpolate(x, points[0], points[1]);
+
+ // Find the first point which is further to the right than the given x
+ for (int i = 0; i <= lastIndex; i ++) {
+
+ // Return the y coordinate of this point if it exactly matches the given x coordinate
+ if (points[i].x == x) return points[i].y;
+
+ // Because points[i] is the first point further to the right than x, points[i-1] must
+ // also be the last point further to the left than x, so we interpolate between the two
+ if (points[i].x > x) {
+ return interpolate(x, points[i-1], points[i]);
+ }
+
+ }
+
+ // Because no point was further to the right than x, x is beyond the last point and we must
+ // interpolate between the last two points
+ return interpolate(x, points[lastIndex - 1], points[lastIndex]);
+
+ }
+
+}
diff --git a/lib/app/src/main/java/claw/math/State.java b/lib/app/src/main/java/claw/math/State.java
new file mode 100644
index 0000000..6b8e425
--- /dev/null
+++ b/lib/app/src/main/java/claw/math/State.java
@@ -0,0 +1,15 @@
+package claw.math;
+
+/**
+ * An interface for the internal state of some {@link StateMachine}.
+ */
+public interface State, I> {
+
+ /**
+ * Get the next {@link State} given the {@link StateMachine} input {@code input}.
+ * @param input The input event or data to some state machine.
+ * @return The state the input leads to.
+ */
+ public S getNewState (I input);
+
+}
\ No newline at end of file
diff --git a/lib/app/src/main/java/claw/math/StateMachine.java b/lib/app/src/main/java/claw/math/StateMachine.java
new file mode 100644
index 0000000..263fedf
--- /dev/null
+++ b/lib/app/src/main/java/claw/math/StateMachine.java
@@ -0,0 +1,36 @@
+package claw.math;
+
+/**
+ * A generic state machine which allows for transitioning and storing {@link State}.
+ */
+public class StateMachine , I> {
+
+ private S currentState;
+
+ /**
+ * Create a new {@link StateMachine} with some initial state.
+ * @param initialState The initial {@link State} of this state machine.
+ */
+ public StateMachine (S initialState) {
+ currentState = initialState;
+ }
+
+ /**
+ * Transition the state of this {@link StateMachine} according to a given {@code input}.
+ * @param input The input which will be used to get a new state.
+ * @return This state machine, so that several transition inputs can easily be chained together.
+ */
+ public StateMachine transition (I input) {
+ currentState = currentState.getNewState(input);
+ return this;
+ }
+
+ /**
+ * Get the internal {@link State} of this {@link StateMachine}.
+ * @return The internal state.
+ */
+ public S getState () {
+ return currentState;
+ }
+
+}
diff --git a/lib/app/src/main/java/claw/math/Transform.java b/lib/app/src/main/java/claw/math/Transform.java
index abfa820..65e4329 100644
--- a/lib/app/src/main/java/claw/math/Transform.java
+++ b/lib/app/src/main/java/claw/math/Transform.java
@@ -30,6 +30,17 @@ public static Transform clamp (double low, double high) {
return input -> Math.min(Math.max(input, low), high);
}
+ /**
+ * Gets a linear {@link Transform} which applies the equation {@code y = mx + b}
+ * to the parameter for the given {@code m} and {@code b}.
+ * @param m The slope of the line represented by this linear transform.
+ * @param b The y-intercept of the line represented by this linear transform.
+ * @return The linear transform.
+ */
+ public static Transform linear (double m, double b) {
+ return x -> m*x + b;
+ }
+
/**
* Turns a transform into an even function by reflecting all x values less than zero across the y-axis.
* @param transform The base {@link Transform} to turn into an even function.
diff --git a/lib/app/src/main/java/claw/math/Vector.java b/lib/app/src/main/java/claw/math/Vector.java
index 4e176f6..f1a8e37 100644
--- a/lib/app/src/main/java/claw/math/Vector.java
+++ b/lib/app/src/main/java/claw/math/Vector.java
@@ -4,21 +4,20 @@
import edu.wpi.first.math.Nat;
import edu.wpi.first.math.Num;
+import edu.wpi.first.math.numbers.N1;
+import edu.wpi.first.math.numbers.N2;
+import edu.wpi.first.math.numbers.N3;
+import edu.wpi.first.math.numbers.N4;
/**
- * A type-safe vector class which can be used for calculations.
+ * A vector class which can be used for calculations.
*/
public class Vector {
private Optional magnitude = Optional.empty();
+ private Optional angle = Optional.empty();
private final Nat dimensionality;
-
- /**
- * An array of components backed by the {@link Vector} (meaning these components should never be modified).
- * For example, for a two-dimensional vector {@code Vector}, this array would
- * have a length of two and would contain the x and y components of the vector as {@code [x, y]}.
- */
- public final double[] components;
+ private final double[] components;
/**
* Gets a {@link Vector} from a list of components and the dimensionality of the vector.
@@ -30,10 +29,116 @@ public Vector (Nat dim, double... components) {
if (dim.getNum() != components.length)
throw new IllegalArgumentException("The number of components given for this Vector does not match the dimensionality of the Vector");
dimensionality = dim;
-
this.components = components.clone();
}
+ /**
+ * Create a new one-dimensional vector {@code } from the given x component.
+ * @param x The x component of the vector.
+ * @return The vector {@code }.
+ */
+ public static Vector from (double x) {
+ return new Vector<>(Nat.N1(), x);
+ }
+
+ /**
+ * Create a new two-dimensional vector {@code } from the given components.
+ * @param x The x component of the vector.
+ * @param y The y component of the vector.
+ * @return The vector {@code }.
+ */
+ public static Vector from (double x, double y) {
+ return new Vector<>(Nat.N2(), x, y);
+ }
+
+ /**
+ * Create a new three-dimensional vector {@code } from the given components.
+ * @param x The x component of the vector.
+ * @param y The y component of the vector.
+ * @param z The z component of the vector.
+ * @return The vector {@code }.
+ */
+ public static Vector from (double x, double y, double z) {
+ return new Vector<>(Nat.N3(), x, y, z);
+ }
+
+ /**
+ * Create a new four-dimensional vector {@code } from the given components.
+ * @param x The x component of the vector.
+ * @param y The y component of the vector.
+ * @param z The z component of the vector.
+ * @param w The w component of the vector.
+ * @return The vector {@code }.
+ */
+ public static Vector from (double x, double y, double z, double w) {
+ return new Vector<>(Nat.N4(), x, y, z, w);
+ }
+
+ /**
+ * Get the dimensionality of this vector (i.e. the number of components it has), as an integer.
+ * @return The dimensionality of this vector.
+ */
+ public int getDimensionality () {
+ return dimensionality.getNum();
+ }
+
+ /**
+ * Get the {@code i+1}th component of this vector. For example,
+ * {@code getComponent(2)} would return the third component.
+ * @param i The index of the component to retrieve.
+ * @return The {@code i+1}th component of this vector.
+ * @throws IllegalArgumentException If the given component index {@code i} is invalid for this vector
+ * (i.e. no {@code i+1}th component exists for this vector).
+ */
+ public double getComponent (int i) throws IllegalArgumentException {
+ if (i < 0) {
+ throw new IllegalArgumentException("Cannot retrieve a component with index less than zero");
+ } else if (i >= getDimensionality()) {
+ throw new IllegalArgumentException(
+ "Cannot retrieve component with index "+i+" from a " +
+ getDimensionality()+"-dimensional vector"
+ );
+ } else {
+ return components[i];
+ }
+ }
+
+ /**
+ * Get the x (1st) component of this vector.
+ * If this vector's dimensionality is less than one, an exception will be thrown.
+ * @return The x component of this vector.
+ */
+ public double getX () {
+ return getComponent(0);
+ }
+
+ /**
+ * Get the y (2nd) component of this vector.
+ * If this vector's dimensionality is less than two, an exception will be thrown.
+ * @return The y component of this vector.
+ */
+ public double getY () {
+ return getComponent(1);
+ }
+
+ /**
+ * Get the z (3rd) component of this vector.
+ * If this vector's dimensionality is less than three, an exception will be thrown.
+ * @return The z component of this vector.
+ */
+ public double getZ () {
+ return getComponent(2);
+ }
+
+ /**
+ * Get the w (4th) component of this vector.
+ * If this vector's dimensionality is less than four, an exception will be thrown.
+ * @return The w component of this vector.
+ */
+ public double getW () {
+ return getComponent(3);
+ }
+
/**
* Gets the magnitude of the vector.
* @return The magnitude.
@@ -74,17 +179,16 @@ public Vector scale (double k) {
}
/**
- * Applies a {@link Transform} to the magnitude of this vector.
- * @param transform The {@code Transform} to apply to this vector's magnitude.
- * @return This vector scaled such that the magnitude of the resulting vector is equal to the result
- * of the transform applied to this vector's magnitude.
+ * Returns this vector scaled so that the new magnitude is equal to the given magnitude. If this vector's magnitude
+ * is zero, a zero vector will be returned.
+ * @param magnitude The magnitude of the vector after scaling.
+ * @return This vector, scale such that the magnitude equals the provided magnitude.
*/
- public Vector applyScale (Transform transform) {
+ public Vector scaleToMagnitude (double magnitude) {
if (getMagnitude() == 0)
return this.scale(0);
-
- double newMagnitude = transform.apply(getMagnitude());
- return this.scale(newMagnitude / getMagnitude());
+ else
+ return this.scale(magnitude / getMagnitude());
}
/**
@@ -133,7 +237,7 @@ public double dotProduct (Vector other) {
* @return The sum of the two vectors.
*/
public Vector add (Vector other) {
- return this.apply(other, (a, b) -> a * b);
+ return this.apply(other, (a, b) -> a + b);
}
/**
@@ -145,4 +249,46 @@ public Vector subtract (Vector other) {
return this.apply(other, (a, b) -> a - b);
}
+ /**
+ * Returns the angle formed between a two-dimensional vector and the x-axis, in radians. This angle
+ * increases counterclockwise. For example, a vector facing in the +y direction will return {@code pi/2}.
+ * @param vector The two-dimensional vector to retrieve the direction angle of.
+ * @return The angle of the vector, in radians.
+ */
+ public static double getAngle (Vector vector) {
+ if (vector.angle.isEmpty()) {
+
+ // Set vector.angle as a cache
+
+ if (vector.getMagnitude() == 0) {
+
+ // Return 0 if the magnitude is 0 to prevent dividing by zero (or yielding a nonsensical answer)
+ vector.angle = Optional.of(0.);
+
+ } else {
+
+ // Get the X component of the vector if it were on the unit circle so we can use arccos
+ double unitX = vector.components[0] / vector.getMagnitude();
+
+ // Get the angle if the vector were above the x-axis
+ double angle = Math.acos(unitX);
+
+ // Use the angle directly if the vector is above the x-axis, or modify it to flip across the x-axis otherwise
+ vector.angle = Optional.of(vector.components[1] >= 0 ? (angle) : (2*Math.PI - angle));
+
+ }
+
+ }
+
+ return vector.angle.get();
+ }
+
+ @Override
+ public String toString () {
+ String[] componentsStrings = new String[components.length];
+ for (int i = 0; i < components.length; i ++)
+ componentsStrings[i] = Double.toString(components[i]);
+ return "<" + String.join(", ", componentsStrings) + ">";
+ }
+
}
diff --git a/lib/app/src/main/java/claw/math/InputTransform.java b/lib/app/src/main/java/claw/math/input/InputTransform.java
similarity index 63%
rename from lib/app/src/main/java/claw/math/InputTransform.java
rename to lib/app/src/main/java/claw/math/input/InputTransform.java
index 61c79b9..de6973b 100644
--- a/lib/app/src/main/java/claw/math/InputTransform.java
+++ b/lib/app/src/main/java/claw/math/input/InputTransform.java
@@ -1,6 +1,12 @@
-package claw.math;
+package claw.math.input;
-public class InputTransform {
+import claw.math.Transform;
+
+/**
+ * A class which helps to process input from a scalar or vector input on a controller (trigger or joystick),
+ * handling a deadband zone and applying a curve to refine control.
+ */
+public class InputTransform implements Transform {
private static final Transform INPUT_CLAMP = Transform.clamp(-1, 1);
@@ -20,7 +26,7 @@ public class InputTransform {
public static final Transform SQUARE_CURVE = Transform.toOdd(x -> x*x);
/**
- * Creates {@link Transform} representing a deadband's application to some input value.
+ * Creates a {@link Transform} representing a deadband's application to some input value.
* @param deadband A deadband value on the interval [0, 1).
* @return The deadband {@code Transform}.
*/
@@ -38,27 +44,36 @@ public static final Transform makeDeadband (double deadband) {
}));
}
+ private final Transform innerTransform;
+
/**
- * Create a {@link Transform} which can be applied to any input (but designed for handling human input
- * from an axis on a controller). The transform applies a deadband
+ * Create an {@link InputTransform} which can be applied to any input (but designed for handling human input
+ * from one (scalar) or two (vector) axes on a controller). The transform applies a deadband
* so that any input with a magnitude less than the deadband value will be ignored.
* A given input map is then applied to the output from this deadband, and finally
* the output is clamped to the range [-1, 1].
*
* @param inputMap Any {@code Transform}. It is recommended but not required that this transform map -1 to -1,
- * 0 to 0, and 1 to 1. See {@link Transform#toOdd(Transform)} for a easy way to convert an existing input curve which
+ * 0 to 0, and 1 to 1. See {@link Transform#toOdd(Transform)} for a easy way to convert an existing transform which
* works for positive numbers into one which will also properly handle negative numbers. If you're not sure where
* to start, try out {@link InputTransform#THREE_HALVES_CURVE}.
* @param deadbandValue Any input with a magnitude of less than this deadband value will be mapped to zero.
- * @return The final {@code Transform} which can be applied directly to input from a controller axis.
*/
- public static Transform getInputTransform (Transform inputMap, double deadbandValue) {
- return
+ public InputTransform (Transform inputMap, double deadbandValue) {
+ innerTransform =
makeDeadband(deadbandValue)
.then(inputMap)
.then(INPUT_CLAMP);
}
- private InputTransform () { }
+ /**
+ * Applies this {@link InputTransform} to the given input. The input is assumed to be on the range [-1, 1], but
+ * this transform will still clamp input to that range in case it isn't already. The input transform
+ * is best used when applying input directly to a value taken from a joystick axis on a controller.
+ */
+ @Override
+ public double apply (double input) {
+ return innerTransform.apply(input);
+ }
}
diff --git a/lib/app/src/main/java/claw/math/input/RaptorsXboxController.java b/lib/app/src/main/java/claw/math/input/RaptorsXboxController.java
new file mode 100644
index 0000000..ee088d6
--- /dev/null
+++ b/lib/app/src/main/java/claw/math/input/RaptorsXboxController.java
@@ -0,0 +1,200 @@
+package claw.math.input;
+
+import java.util.Optional;
+
+import claw.math.Transform;
+import claw.math.Vector;
+import edu.wpi.first.math.geometry.Rotation2d;
+import edu.wpi.first.math.numbers.N2;
+import edu.wpi.first.wpilibj.XboxController;
+import edu.wpi.first.wpilibj2.command.InstantCommand;
+import edu.wpi.first.wpilibj2.command.SequentialCommandGroup;
+import edu.wpi.first.wpilibj2.command.WaitCommand;
+
+/**
+ * An extension of the {@link XboxController} providing better control over the D-pad, {@link InputTransform}
+ * applications to joysticks, and other quality-of-life improvements.
+ */
+public class RaptorsXboxController extends XboxController {
+
+ private final Transform leftStickTransform, rightStickTransform;
+
+ /**
+ * Create a new {@link RaptorsXboxController}.
+ * @param port The port index of this Xbox controller on the Driver Station.
+ */
+ public RaptorsXboxController (int port) {
+ this(port, Transform.clamp(-1, 1));
+ }
+
+ /**
+ * Create a new {@link RaptorsXboxController} with a {@link Transform} to apply to both joysticks when retrieving
+ * their positions as vectors.
+ * @param port The port index of this Xbox controller on the Driver Station.
+ * @param stickVectorTransform The transform to apply to both joystick vectors' magnitudes. This is recommended
+ * to be an {@link InputTransform}.
+ */
+ public RaptorsXboxController (int port, Transform stickVectorTransform) {
+ this(port, stickVectorTransform, stickVectorTransform);
+ }
+
+ /**
+ * Create a new {@link RaptorsXboxController} with {@link Transform}s to apply to the left and right joysticks when retrieving
+ * their positions as vectors. The given {@code Transform}s are recommended to be {@link InputTransform}s.
+ * @param port The port index of this Xbox controller on the Driver Station.
+ * @param leftStickVectorTransform The transform to apply to the left joystick's position vector (magnitude).
+ * @param rightStickVectorTransform The transform to apply to the right joystick's position vector (magnitude).
+ */
+ public RaptorsXboxController (int port, Transform leftStickVectorTransform, Transform rightStickVectorTransform) {
+ super(port);
+ leftStickTransform = leftStickVectorTransform;
+ rightStickTransform = rightStickVectorTransform;
+ }
+
+ private Vector getStickVector (double x, double y, Transform transform) {
+ // Invert y because the input from the driver station is also inverted
+ Vector rawVector = Vector.from(x, -y);
+
+ // Re-scale the vector to a new magnitude
+ double rawMagnitude = rawVector.getMagnitude();
+ return rawVector.scaleToMagnitude(transform.apply(rawMagnitude));
+ }
+
+ /**
+ * Retrieves the left joystick's position as a {@link Vector}. The left joystick transform
+ * given through the constructor method will be applied to the vector's magnitude,
+ * if one was provided. Also, the vector will be normalized so that up on the joystick corresponds
+ * with a positive y value.
+ * @return A {@code Vector} representing the left joystick's position.
+ */
+ public Vector getLeftStickAsVector () {
+ return getStickVector(getLeftX(), getLeftY(), leftStickTransform);
+ }
+
+ /**
+ * Retrieves the right joystick's position as a {@link Vector}. The right joystick transform
+ * given through the constructor method will be applied to the vector's magnitude,
+ * if one was provided. Also, the vector will be normalized so that up on the joystick corresponds
+ * with a positive y value.
+ * @return A {@code Vector} representing the right joystick's position.
+ */
+ public Vector getRightStickAsVector () {
+ return getStickVector(getRightX(), getRightY(), rightStickTransform);
+ }
+
+ /**
+ * Set the controller's rumble.
+ * @param leftRumble A value on the interval [0, 1] representing the rumble to apply to the left side of the controller.
+ * @param rightRumble A value on the interval [0, 1] representing the rumble to apply to the right side of the controller.
+ */
+ public void setRumble (double leftRumble, double rightRumble) {
+ setRumble(RumbleType.kLeftRumble, leftRumble);
+ setRumble(RumbleType.kRightRumble, rightRumble);
+ }
+
+ /**
+ * Set the controller's rumble.
+ * @param rumbleValue A value on the interval [0, 1] representing the rumble to apply to the controller.
+ */
+ public void setRumble (double rumbleValue) {
+ setRumble(rumbleValue, rumbleValue);
+ }
+
+ /**
+ * Stop all controller rumble.
+ */
+ public void stopRumble () {
+ setRumble(0);
+ }
+
+ /**
+ * Set the controller to rumble for a given duration.
+ * @param durationSecs The duration of the rumble, in seconds.
+ * @param leftRumble A value on the interval [0, 1] representing the rumble to apply to the left side of the controller.
+ * @param rightRumble A value on the interval [0, 1] representing the rumble to apply to the right side of the controller.
+ */
+ public void setTimedRumble (double durationSecs, double leftRumble, double rightRumble) {
+ new SequentialCommandGroup(
+ new InstantCommand(() -> this.setRumble(leftRumble, rightRumble)),
+ new WaitCommand(durationSecs),
+ new InstantCommand(this::stopRumble)
+ ).schedule();
+ }
+
+ /**
+ * Set the controller to rumble for a given duration.
+ * @param durationSecs The duration of the rumble, in seconds.
+ * @param rumbleValue A value on the interval [0, 1] representing the rumble to apply to the controller.
+ */
+ public void setTimedRumble (double durationSecs, double rumbleValue) {
+ setTimedRumble(durationSecs, rumbleValue, rumbleValue);
+ }
+
+ /**
+ * Get the {@link DPadDirection} currently being pressed (if one is being pressed).
+ * @return An {@link Optional} containing the pressed {@code DPadDirection}.
+ */
+ public Optional getDPad () {
+
+ // Get the DPad direction as an integer
+ int directionInt = getPOV();
+
+ // Search through the DPadDirection enum to find one which corresponds with the current directionInt
+ for (DPadDirection direction : DPadDirection.values()) {
+ if (directionInt == direction.direction) return Optional.of(direction);
+ }
+
+ // If none matched, then return the empty optional
+ return Optional.empty();
+
+ }
+
+ /**
+ * Check whether a given {@link DPadDirection} is actively being pressed.
+ * @param direction The {@code DPadDirection} to check against.
+ * @return Whether the given direction is held down on the D-pad.
+ */
+ public boolean isDPadPressed (DPadDirection direction) {
+ Optional currentDirection = getDPad();
+ return currentDirection.isPresent() && currentDirection.get().equals(direction);
+ }
+
+ /**
+ * An enum representing the buttons or orientations of the D-pad.
+ * @see RaptorsXboxController#getDPad()
+ */
+ public enum DPadDirection {
+
+ TOP (0),
+ TOP_RIGHT (45),
+ RIGHT (90),
+ BOTTOM_RIGHT (135),
+ BOTTOM (180),
+ BOTTOM_LEFT (225),
+ LEFT (270),
+ TOP_LEFT (315);
+
+ /**
+ * Direction in degrees, starting with 0 for {@link DPadDirection#TOP} and progressing clockwise.
+ */
+ public final int direction;
+
+ /**
+ * The {@link Rotation2d} of this direction, as if it were placed on the unit circle.
+ * This conforms to the standard WPILib understanding of rotation, as zero rotation corresponds
+ * with {@link DPadDirection#RIGHT}, and the rotation progresses counterclockwise.
+ */
+ public final Rotation2d rotation;
+
+ private DPadDirection (int direction) {
+ this.direction = direction;
+
+ // Convert the direction degrees into rotation degrees on the unit circle
+ int unitCircleDegrees = 90 - direction;
+ if (unitCircleDegrees < 0) unitCircleDegrees += 360;
+ rotation = Rotation2d.fromDegrees(unitCircleDegrees);
+ }
+
+ }
+
+}
diff --git a/lib/app/src/main/java/claw/rct/local/LocalCommandInterpreter.java b/lib/app/src/main/java/claw/rct/local/LocalCommandInterpreter.java
index 410c571..fc4c09b 100644
--- a/lib/app/src/main/java/claw/rct/local/LocalCommandInterpreter.java
+++ b/lib/app/src/main/java/claw/rct/local/LocalCommandInterpreter.java
@@ -66,10 +66,6 @@ private void addCommands () {
"Displays the current status of the connection to the remote (the roboRIO), automatically updating over time. Press enter to stop.",
this::commsCommand);
- addCommand("ip", "ip [static | dynamic]",
- "Change the IP address to use for the roboRIO to be either static (use for connection via radio) or dynamic (ethernet or usb).",
- this::ipAddressCommand);
-
addCommand("ssh", "ssh [user]",
"Launches an Secure Socket Shell for the roboRIO, using either the user 'lvuser' or 'admin'.",
this::sshCommand);
@@ -77,6 +73,10 @@ private void addCommands () {
addCommand("log", "log",
"Print data with CLAWLoggers to the terminal when it is received from the robot.",
this::logCommand);
+
+ addCommand("config", "config [team number] [remote port]",
+ "Configure the connection to the RCT server, setting the team number and server port.",
+ this::configCommand);
}
/**
@@ -111,30 +111,6 @@ private void addCommand (String command, String usage, String helpDescription, C
// Command methods:
- private void ipAddressCommand (ConsoleManager console, CommandReader reader) throws BadCallException {
- reader.allowNoOptions();
- reader.allowNoFlags();
-
- String addressType = reader.readArgOneOf(
- "address type",
- "The given IP address type must be either 'static' or 'dynamic'.",
- "static", "dynamic");
-
- if (addressType.equals("static")) {
- system.setUseStaticRoborioAddress(true);
- } else {
- system.setUseStaticRoborioAddress(false);
- }
-
- try {
- console.println("Attempting to connect to " + system.getRoborioHost() + "...");
- system.establishNewConnection();
- console.println("Successfully connected.");
- } catch (IOException e) {
- console.printlnErr("Failed to establish a new connection.");
- }
- }
-
private void clearCommand (ConsoleManager console, CommandReader reader) throws BadCallException {
reader.allowNone();
console.clear();
@@ -145,6 +121,25 @@ private void exitCommand (ConsoleManager console, CommandReader reader) throws B
System.exit(0);
}
+ private void configCommand (ConsoleManager console, CommandReader reader) throws BadCallException {
+ int teamNum = reader.readArgInt("team number");
+ int port = reader.readArgInt("RCT server port");
+ reader.noMoreArgs();
+ reader.allowNoOptions();
+ reader.allowNoFlags();
+
+ system.setServerPort(port);
+ system.setTeamNum(teamNum);
+
+ try {
+ system.establishNewConnection();
+ console.printlnSys("Successfully connected to the RCT server at " + system.getRoborioHost().orElse("[connection failed]"));
+ } catch (IOException e) {
+ console.printlnErr("Failed to connect to the RCT server.");
+ }
+
+ }
+
private void helpCommand (ConsoleManager console, CommandReader reader) throws BadCallException {
reader.allowNoOptions();
reader.allowNoFlags();
@@ -257,12 +252,16 @@ private void sshCommand (ConsoleManager console, CommandReader reader) throws Ba
reader.noMoreArgs();
// Get host for ssh and generate command
- String host = system.getRoborioHost();
+ Optional host = system.getRoborioHost();
+ if (host.isEmpty()) {
+ console.printlnErr("There is no connection to the roboRIO.");
+ return;
+ }
String[] puttyCommand = new String[] {
"putty",
"-ssh",
- user+"@"+host,
+ user+"@"+host.get(),
};
// Attempt to run the command to start PuTTY
diff --git a/lib/app/src/main/java/claw/rct/local/LocalSystem.java b/lib/app/src/main/java/claw/rct/local/LocalSystem.java
index 5841400..325d864 100644
--- a/lib/app/src/main/java/claw/rct/local/LocalSystem.java
+++ b/lib/app/src/main/java/claw/rct/local/LocalSystem.java
@@ -10,8 +10,8 @@
import claw.rct.network.low.DriverStationSocketHandler;
import claw.rct.network.low.InstructionMessage;
import claw.rct.network.low.ResponseMessage;
-import claw.rct.network.low.Waiter;
-import claw.rct.network.low.Waiter.NoValueReceivedException;
+import claw.rct.network.low.concurrency.Waiter;
+import claw.rct.network.low.concurrency.Waiter.NoValueReceivedException;
import claw.rct.network.messages.CommandsListingMessage;
import claw.rct.network.messages.ConnectionCheckMessage;
import claw.rct.network.messages.ConnectionResponseMessage;
@@ -45,14 +45,12 @@ public class LocalSystem implements ResponseMessageHandler {
private final LogDataStorage logDataStorage;
// Socket handling
- private final int teamNum, remotePort;
+ private int teamNum, remotePort;
private DriverStationSocketHandler socket = null;
private final Thread requireNewConnectionThread = new Thread(this::requireNewConnectionThreadRunnable);
private boolean requireNewConnection = false;
- private boolean useStaticRoboRIOAddress;
-
private Optional remoteHelpMessages = Optional.empty();
private final Object remoteHelpMessagesLock = new Object();
@@ -69,28 +67,28 @@ public class LocalSystem implements ResponseMessageHandler {
* @param console
*/
public LocalSystem (
- int teamNum,
- boolean useStaticAddress,
- int remotePort,
- LogDataStorage logDataStorage,
- ConsoleManager console) {
+ int teamNum,
+ int remotePort,
+ LogDataStorage logDataStorage,
+ ConsoleManager console
+ ) {
// Instantiating final fields
this.teamNum = teamNum;
- this.useStaticRoboRIOAddress = useStaticAddress;
this.remotePort = remotePort;
this.logDataStorage = logDataStorage;
this.console = console;
interpreter = new LocalCommandInterpreter(this, logDataStorage);
- // Start the requireNewConnectionThread, which will attempt to establish a new connection
- // whenever it is interrupted
- requireNewConnectionThread.start();
-
// Attempt to establish socket connection
try {
establishNewConnection();
} catch (IOException e) { }
+
+ // Start the requireNewConnectionThread, which will attempt to establish a new connection
+ // whenever it is interrupted
+ requireNewConnectionThread.start();
+
}
/**
@@ -108,12 +106,15 @@ public synchronized void establishNewConnection () throws IOException {
try {
socket = new DriverStationSocketHandler(
teamNum,
- useStaticRoboRIOAddress,
remotePort,
this::receiveMessage,
this::handleSocketReceiverException
);
+
lastConnectionException = null;
+
+ updateConnectionStatus(ConnectionStatus.OK);
+
} catch (IOException exception) {
// If there's an IOException (one that's different from the previous exception),
@@ -130,19 +131,20 @@ public synchronized void establishNewConnection () throws IOException {
}
}
- public void setUseStaticRoborioAddress (boolean useStaticAddress) {
- useStaticRoboRIOAddress = useStaticAddress;
+ public Optional getRoborioHost () {
+ DriverStationSocketHandler s = socket;
+
+ if (s != null) {
+ return Optional.of(s.getRoborioHost());
+ } else return Optional.empty();
}
- public String getRoborioHost () {
- return DriverStationSocketHandler.getRoborioHost(useStaticRoboRIOAddress, teamNum) + ":" + remotePort;
+ public void setTeamNum (int newTeamNum) {
+ teamNum = newTeamNum;
}
- /**
- * Gets the team number passed in through the constructor.
- */
- public int getTeamNum () {
- return teamNum;
+ public void setServerPort (int newPort) {
+ remotePort = newPort;
}
/**
@@ -150,6 +152,7 @@ public int getTeamNum () {
* @return The {@link ConnectionStatus} describing the current connection to the server.
*/
public ConnectionStatus checkServerConnection () {
+
// Attempt to send a connection check message to remote
try {
DriverStationSocketHandler s = throwIfNullSocket();
@@ -305,6 +308,7 @@ private void requireNewConnectionThreadRunnable () {
while (true) {
// If a new connection needs to be established, try to do that
checkServerConnection(); // This will update the requireNewConnection flag
+
if (requireNewConnection) {
try {
// Try to establish a new connection
diff --git a/lib/app/src/main/java/claw/rct/local/RemoteProcessHandler.java b/lib/app/src/main/java/claw/rct/local/RemoteProcessHandler.java
index f150aeb..5c1fdbe 100644
--- a/lib/app/src/main/java/claw/rct/local/RemoteProcessHandler.java
+++ b/lib/app/src/main/java/claw/rct/local/RemoteProcessHandler.java
@@ -5,9 +5,9 @@
import claw.rct.network.low.ConsoleManager;
import claw.rct.network.low.InstructionMessage;
-import claw.rct.network.low.KeepaliveWatcher;
-import claw.rct.network.low.Waiter;
-import claw.rct.network.low.Waiter.NoValueReceivedException;
+import claw.rct.network.low.concurrency.KeepaliveWatcher;
+import claw.rct.network.low.concurrency.Waiter;
+import claw.rct.network.low.concurrency.Waiter.NoValueReceivedException;
import claw.rct.network.messages.commands.CommandInputMessage;
import claw.rct.network.messages.commands.CommandOutputMessage;
import claw.rct.network.messages.commands.ProcessKeepaliveLocal;
diff --git a/lib/app/src/main/java/claw/rct/local/RobotControlTerminal.java b/lib/app/src/main/java/claw/rct/local/RobotControlTerminal.java
index 1a98001..f60f72e 100644
--- a/lib/app/src/main/java/claw/rct/local/RobotControlTerminal.java
+++ b/lib/app/src/main/java/claw/rct/local/RobotControlTerminal.java
@@ -7,7 +7,6 @@
import claw.rct.local.LocalSystem.ConnectionStatus;
import claw.rct.local.console.LocalConsoleManager;
import claw.rct.network.low.ConsoleManager;
-import claw.rct.network.low.DriverStationSocketHandler;
/**
* Represents the main robot control terminal program which starts the driverstation side of the server
@@ -33,18 +32,10 @@ public RobotControlTerminal () throws IOException {
*/
public void start () {
- // Check whether to use the static or dynamic address for the roboRIO
- boolean useStaticAddress = getUseStaticAddress();
- console.clear();
-
- // Display of helpful message with roboRIO host url
- String host = DriverStationSocketHandler.getRoborioHost(useStaticAddress, TEAM_NUMBER) + ":" + REMOTE_PORT;
- console.printlnSys("Attempting to connect to server at " + host + "...");
-
// LocalSystem setup
+ console.printlnSys("Attempting to connect to roboRIO server...");
LocalSystem system = new LocalSystem(
TEAM_NUMBER,
- useStaticAddress,
REMOTE_PORT,
new LogDataStorage(),
console
@@ -83,31 +74,6 @@ public void start () {
}
- private boolean getUseStaticAddress () {
- console.printlnSys("Use the static (radio) or dynamic (ethernet/usb) IP address for the roboRIO? ");
- console.printlnSys("s - Static: " + DriverStationSocketHandler.getRoborioHost(true, TEAM_NUMBER));
- console.printlnSys("d - Dynamic: " + DriverStationSocketHandler.getRoborioHost(false, TEAM_NUMBER));
-
- String input = null;
- boolean useStaticAddress = true;
-
- console.printSys("(s | d): ");
-
- while (input == null) {
- input = console.readInputLine().trim().toLowerCase();
- if (input.equals("s")) {
- useStaticAddress = true;
- } else if (input.equals("d")) {
- useStaticAddress = false;
- } else {
- input = null;
- console.printSys("Use 's' for static or 'd' for dynamic. ");
- }
- }
-
- return useStaticAddress;
- }
-
private void processCommand (LocalSystem system, String line) {
try {
system.processCommand(line);
diff --git a/lib/app/src/main/java/claw/rct/network/low/DriverStationSocketHandler.java b/lib/app/src/main/java/claw/rct/network/low/DriverStationSocketHandler.java
index 1f06d72..6da18a9 100644
--- a/lib/app/src/main/java/claw/rct/network/low/DriverStationSocketHandler.java
+++ b/lib/app/src/main/java/claw/rct/network/low/DriverStationSocketHandler.java
@@ -1,6 +1,7 @@
package claw.rct.network.low;
import java.io.IOException;
+import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.function.Consumer;
@@ -24,31 +25,50 @@ public class DriverStationSocketHandler {
* @throws IOException If the socket throws an i/o exception.
*/
public DriverStationSocketHandler (
- int teamNum,
- boolean useStaticAddress,
- int port,
- Consumer responseReader,
- Consumer excHandler)
- throws IOException {
+ int teamNum,
+ int port,
+ Consumer responseReader,
+ Consumer excHandler
+ ) throws IOException {
- Socket socket = null;
+ // Get a socket handler, trying both static and dynamic addresses
+ SocketHandler handler;
try {
- socket = new Socket(getRoborioHost(useStaticAddress, teamNum), port);
- socketHandler = new SocketHandler(socket, this::receiveMessage, this::handleReceiverIOException);
+ handler = getSocketHandler(teamNum, false, port);
} catch (IOException e) {
- if (socket != null) socket.close();
- throw e;
+ handler = getSocketHandler(teamNum, true, port);
}
+
+ this.socketHandler = handler;
+
this.responseReader = responseReader;
this.excHandler = excHandler;
}
+ private SocketHandler getSocketHandler (int teamNum, boolean useStaticAddress, int port) throws IOException {
+ Socket socket = null;
+ try {
+ socket = new Socket();
+ socket.connect(new InetSocketAddress(getRoborioHost(useStaticAddress, teamNum), port), 500);
+ return new SocketHandler(socket, this::receiveMessage, this::handleReceiverIOException);
+ } catch (IOException e) {
+ try {
+ if (socket != null) socket.close();
+ } catch (IOException ex) { }
+ throw e;
+ }
+ }
+
+ public String getRoborioHost () {
+ return socketHandler.getHostname();
+ }
+
/**
* Gets the roboRIO host URL.
* @param teamNum The team number associated with the roboRIO.
* @return The hostname for connecting to the rorboRIO
*/
- public static String getRoborioHost (boolean useStaticAddress, int teamNum) {
+ private static String getRoborioHost (boolean useStaticAddress, int teamNum) {
if (useStaticAddress) {
int first = teamNum / 100;
int second = teamNum % 100;
@@ -94,7 +114,7 @@ private void handleReceiverIOException (IOException ioException) {
* @throws IOException If the socket failed to send the message.
*/
public void sendInstructionMessage (InstructionMessage message) throws IOException {
- socketHandler.sendMessage(message);
+ socketHandler.sendMessage(message, 3000);
}
/**
diff --git a/lib/app/src/main/java/claw/rct/network/low/RobotSocketHandler.java b/lib/app/src/main/java/claw/rct/network/low/RobotSocketHandler.java
index afe1950..f71d9da 100644
--- a/lib/app/src/main/java/claw/rct/network/low/RobotSocketHandler.java
+++ b/lib/app/src/main/java/claw/rct/network/low/RobotSocketHandler.java
@@ -84,7 +84,7 @@ private void handleReceiverIOException (IOException ioException) {
public void sendResponseMessage (ResponseMessage responseMessage) throws IOException {
if (clientSocketHandler == null)
throw new IOException("No socket to send the response message through");
- clientSocketHandler.sendMessage(responseMessage);
+ clientSocketHandler.sendMessage(responseMessage, 4000);
}
/**
diff --git a/lib/app/src/main/java/claw/rct/network/low/SocketHandler.java b/lib/app/src/main/java/claw/rct/network/low/SocketHandler.java
index 12fa9e9..634bb59 100644
--- a/lib/app/src/main/java/claw/rct/network/low/SocketHandler.java
+++ b/lib/app/src/main/java/claw/rct/network/low/SocketHandler.java
@@ -5,6 +5,9 @@
import java.net.Socket;
import java.util.function.Consumer;
+import claw.rct.network.low.concurrency.Waiter;
+import claw.rct.network.low.concurrency.Waiter.NoValueReceivedException;
+
/**
* A simple wrapper around {@code Socket} specialized for sending and receiving serializable {@link Message} objects.
*/
@@ -33,13 +36,47 @@ public SocketHandler (
beginReceivingMessages();
}
+ public String getHostname () {
+ return socket.getInetAddress().getHostAddress();
+ }
+
/**
* Sends a message through the socket.
* @param sendMessage The {@link Message} to send.
* @throws IOException If the socket failed to send the {@code Message}.
*/
- public void sendMessage (Message sendMessage) throws IOException {
+ public void sendMessage (Message sendMessage, int millisTimeout) throws IOException {
+
+ // Create a timeout waiter
+ Waiter