Merge "Allow using shortened channel names in foxglove_websocket"
diff --git a/aos/containers/BUILD b/aos/containers/BUILD
index 9fdc93f..ee1bf98 100644
--- a/aos/containers/BUILD
+++ b/aos/containers/BUILD
@@ -64,6 +64,30 @@
)
cc_library(
+ name = "error_list",
+ hdrs = [
+ "error_list.h",
+ ],
+ deps = [
+ ":sized_array",
+ "//aos:flatbuffers",
+ ],
+)
+
+cc_test(
+ name = "error_list_test",
+ srcs = [
+ "error_list_test.cc",
+ ],
+ target_compatible_with = ["@platforms//os:linux"],
+ deps = [
+ ":error_list",
+ "//aos:json_to_flatbuffer_fbs",
+ "//aos/testing:googletest",
+ ],
+)
+
+cc_library(
name = "resizeable_buffer",
hdrs = [
"resizeable_buffer.h",
diff --git a/aos/containers/error_list.h b/aos/containers/error_list.h
new file mode 100644
index 0000000..2fbd39e
--- /dev/null
+++ b/aos/containers/error_list.h
@@ -0,0 +1,130 @@
+#ifndef AOS_CONTAINERS_ERROR_LIST_H_
+#define AOS_CONTAINERS_ERROR_LIST_H_
+
+#include <iostream>
+
+#include "aos/containers/sized_array.h"
+#include "flatbuffers/flatbuffers.h"
+
+namespace aos {
+
+// A de-duplicated sorted array based on SizedArray
+// For keeping a list of errors that a subsystem has thrown
+// to publish them in a Status message.
+// It is designed to use flatbuffer enums, and use the reserved fields MAX and
+// MIN to automatically determine how much capacity it needs to have.
+template <typename T>
+class ErrorList {
+ private:
+ using array = SizedArray<T, static_cast<size_t>(T::MAX) -
+ static_cast<size_t>(T::MIN) + 1>;
+ array array_;
+
+ public:
+ using value_type = typename array::value_type;
+ using size_type = typename array::size_type;
+ using difference_type = typename array::difference_type;
+ using reference = typename array::reference;
+ using const_reference = typename array::const_reference;
+ using pointer = typename array::pointer;
+ using const_pointer = typename array::const_pointer;
+ using iterator = typename array::iterator;
+ using const_iterator = typename array::const_iterator;
+ using reverse_iterator = typename array::reverse_iterator;
+ using const_reverse_iterator = typename array::const_reverse_iterator;
+
+ constexpr ErrorList() = default;
+ ErrorList(const ErrorList &) = default;
+ ErrorList(ErrorList &&) = default;
+ ErrorList(const flatbuffers::Vector<T> &array) : array_() {
+ for (auto it = array.begin(); it < array.end(); it++) {
+ array_.push_back(*it);
+ }
+ std::sort(array_.begin(), array_.end());
+ };
+
+ ErrorList &operator=(const ErrorList &) = default;
+ ErrorList &operator=(ErrorList &&) = default;
+
+ bool operator==(const ErrorList &other) const {
+ if (other.size() != size()) {
+ return false;
+ }
+ for (size_t i = 0; i < size(); ++i) {
+ if (other[i] != (*this)[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+ bool operator!=(const ErrorList &other) const { return !(*this == other); }
+
+ reference at(size_t i) { return array_.at(i); }
+ const_reference at(size_t i) const { return array_.at(i); }
+
+ reference operator[](size_t i) { return array_[i]; }
+ const_reference operator[](size_t i) const { return array_[i]; }
+
+ reference front() { return array_.front(); }
+ const_reference front() const { return array_.front(); }
+
+ reference back() { return array_.back(); }
+ const_reference back() const { return array_.back(); }
+
+ T *data() { return array_.data(); }
+ const T *data() const { return array_.data(); }
+
+ iterator begin() { return array_.begin(); }
+ const_iterator begin() const { return array_.begin(); }
+ const_iterator cbegin() const { return array_.cbegin(); }
+
+ iterator end() { return array_.end(); }
+ const_iterator end() const { return array_.end(); }
+ const_iterator cend() const { return array_.cend(); }
+
+ reverse_iterator rbegin() { return array_.rbegin(); }
+ const_reverse_iterator rbegin() const { return array_.rbegin(); }
+ const_reverse_iterator crbegin() const { return array_.crbegin(); }
+
+ reverse_iterator rend() { return array_.rend(); }
+ const_reverse_iterator rend() const { return array_.rend(); }
+ const_reverse_iterator crend() const { return array_.crend(); }
+
+ bool empty() const { return array_.empty(); }
+ bool full() const { return array_.full(); }
+
+ size_t size() const { return array_.size(); }
+ constexpr size_t max_size() const { return array_.max_size(); }
+
+ void Clear(const T t) {
+ iterator index = std::find(array_.begin(), array_.end(), t);
+ if (index != array_.end()) {
+ array_.erase(index);
+ }
+ }
+
+ void Set(const T t) {
+ iterator position = std::lower_bound(array_.begin(), array_.end(), t);
+
+ // if it found something, and that something is the same, just leave it
+ if (position != array_.end() && *position == t) {
+ return;
+ }
+
+ // key doesn't already exist
+ array_.insert(position, t);
+ }
+
+ bool Has(const T t) {
+ return std::binary_search(array_.begin(), array_.end(), t);
+ }
+
+ flatbuffers::Offset<flatbuffers::Vector<T>> ToFlatbuffer(
+ flatbuffers::FlatBufferBuilder *fbb) const {
+ return fbb->CreateVector(array_.data(), array_.size());
+ }
+}; // namespace aos
+
+} // namespace aos
+
+#endif // AOS_CONTAINERS_ERROR_LIST_H_
diff --git a/aos/containers/error_list_test.cc b/aos/containers/error_list_test.cc
new file mode 100644
index 0000000..3ce23c4
--- /dev/null
+++ b/aos/containers/error_list_test.cc
@@ -0,0 +1,115 @@
+#include "aos/containers/error_list.h"
+
+#include "aos/json_to_flatbuffer_generated.h"
+#include "gtest/gtest.h"
+
+namespace aos {
+namespace testing {
+
+enum class TestEnum : int8_t {
+ FOO = 0,
+ BAR = 1,
+ BAZ = 2,
+ VWEEP = 3,
+ MIN = FOO,
+ MAX = VWEEP
+};
+
+// Tests that setting works and allows no duplicates
+TEST(ErrorListTest, NoDuplicates) {
+ ErrorList<TestEnum> a;
+ EXPECT_EQ(a.size(), 0);
+ a.Set(TestEnum::BAZ);
+ EXPECT_EQ(a.at(0), TestEnum::BAZ);
+ EXPECT_EQ(a.size(), 1);
+ a.Set(TestEnum::BAZ);
+ EXPECT_EQ(a.at(0), TestEnum::BAZ);
+ EXPECT_EQ(a.size(), 1);
+ a.Set(TestEnum::VWEEP);
+ EXPECT_EQ(a.at(0), TestEnum::BAZ);
+ EXPECT_EQ(a.at(1), TestEnum::VWEEP);
+ EXPECT_EQ(a.size(), 2);
+ a.Set(TestEnum::FOO);
+ EXPECT_EQ(a.at(0), TestEnum::FOO);
+ EXPECT_EQ(a.at(1), TestEnum::BAZ);
+ EXPECT_EQ(a.at(2), TestEnum::VWEEP);
+ EXPECT_EQ(a.size(), 3);
+}
+
+// Tests that clearing works
+TEST(ErrorListTest, Clearing) {
+ ErrorList<TestEnum> a;
+ a.Set(TestEnum::FOO);
+ a.Set(TestEnum::BAZ);
+ a.Set(TestEnum::VWEEP);
+ EXPECT_EQ(a.at(0), TestEnum::FOO);
+ EXPECT_EQ(a.at(1), TestEnum::BAZ);
+ EXPECT_EQ(a.at(2), TestEnum::VWEEP);
+ EXPECT_EQ(a.size(), 3);
+
+ a.Clear(TestEnum::BAR);
+ EXPECT_EQ(a.at(0), TestEnum::FOO);
+ EXPECT_EQ(a.at(1), TestEnum::BAZ);
+ EXPECT_EQ(a.at(2), TestEnum::VWEEP);
+ EXPECT_EQ(a.size(), 3);
+
+ a.Clear(TestEnum::BAZ);
+ EXPECT_EQ(a.at(0), TestEnum::FOO);
+ EXPECT_EQ(a.at(1), TestEnum::VWEEP);
+ EXPECT_EQ(a.size(), 2);
+}
+
+// Tests that checking for a value works
+TEST(ErrorListTest, Has) {
+ ErrorList<TestEnum> a;
+ a.Set(TestEnum::FOO);
+ a.Set(TestEnum::BAZ);
+ a.Set(TestEnum::VWEEP);
+ EXPECT_EQ(a.at(0), TestEnum::FOO);
+ EXPECT_EQ(a.at(1), TestEnum::BAZ);
+ EXPECT_EQ(a.at(2), TestEnum::VWEEP);
+ EXPECT_EQ(a.size(), 3);
+
+ EXPECT_TRUE(a.Has(TestEnum::FOO));
+ EXPECT_TRUE(a.Has(TestEnum::VWEEP));
+ EXPECT_TRUE(a.Has(TestEnum::BAZ));
+ EXPECT_FALSE(a.Has(TestEnum::BAR));
+}
+
+// Tests serializing and deserializing to/from flatbuffers.
+TEST(ErrorListTest, Flatbuffers) {
+ ErrorList<BaseType> a;
+ a.Set(BaseType::Bool);
+ a.Set(BaseType::Float);
+ a.Set(BaseType::Short);
+ EXPECT_TRUE(a.Has(BaseType::Bool));
+ EXPECT_TRUE(a.Has(BaseType::Short));
+ EXPECT_TRUE(a.Has(BaseType::Float));
+ EXPECT_EQ(a.at(0), BaseType::Bool);
+ EXPECT_EQ(a.at(1), BaseType::Short);
+ EXPECT_EQ(a.at(2), BaseType::Float);
+ EXPECT_EQ(a.size(), 3);
+
+ flatbuffers::FlatBufferBuilder fbb(1024);
+ flatbuffers::Offset<flatbuffers::Vector<BaseType>> vector =
+ a.ToFlatbuffer(&fbb);
+
+ ConfigurationBuilder builder(fbb);
+ builder.add_vector_foo_enum(vector);
+
+ fbb.Finish(builder.Finish());
+ const Configuration *config =
+ flatbuffers::GetRoot<Configuration>(fbb.GetBufferPointer());
+
+ ErrorList<BaseType> b(*config->vector_foo_enum());
+ EXPECT_TRUE(b.Has(BaseType::Bool));
+ EXPECT_TRUE(b.Has(BaseType::Short));
+ EXPECT_TRUE(b.Has(BaseType::Float));
+ EXPECT_EQ(b.at(0), BaseType::Bool);
+ EXPECT_EQ(b.at(1), BaseType::Short);
+ EXPECT_EQ(b.at(2), BaseType::Float);
+ EXPECT_EQ(b.size(), 3);
+}
+
+} // namespace testing
+} // namespace aos
diff --git a/aos/containers/sized_array_test.cc b/aos/containers/sized_array_test.cc
index ae732bc..d055f40 100644
--- a/aos/containers/sized_array_test.cc
+++ b/aos/containers/sized_array_test.cc
@@ -175,5 +175,70 @@
EXPECT_DEATH(a.emplace_back(5), "Aborted at");
}
+// Tests inserting at various positions in the array.
+TEST(SizedArrayTest, Inserting) {
+ SizedArray<int, 5> a;
+ a.insert(a.begin(), 2);
+ EXPECT_EQ(a.at(0), 2);
+ EXPECT_EQ(a.size(), 1);
+
+ a.emplace_back(3);
+ EXPECT_EQ(a.at(0), 2);
+ EXPECT_EQ(a.at(1), 3);
+ EXPECT_EQ(a.size(), 2);
+
+ a.insert(a.begin(), 0);
+ EXPECT_EQ(a.at(0), 0);
+ EXPECT_EQ(a.at(1), 2);
+ EXPECT_EQ(a.at(2), 3);
+ EXPECT_EQ(a.size(), 3);
+
+ a.insert(a.begin() + 1, 1);
+ EXPECT_EQ(a.at(0), 0);
+ EXPECT_EQ(a.at(1), 1);
+ EXPECT_EQ(a.at(2), 2);
+ EXPECT_EQ(a.at(3), 3);
+ EXPECT_EQ(a.size(), 4);
+
+ a.insert(a.begin() + 1, 0);
+ EXPECT_EQ(a.at(0), 0);
+ EXPECT_EQ(a.at(1), 0);
+ EXPECT_EQ(a.at(2), 1);
+ EXPECT_EQ(a.at(3), 2);
+ EXPECT_EQ(a.at(4), 3);
+ EXPECT_EQ(a.size(), 5);
+}
+
+// Tests erasing things from the array
+TEST(SizedArrayTest, Erasing) {
+ SizedArray<int, 5> a;
+ a.push_back(8);
+ a.push_back(9);
+ a.push_back(7);
+ a.push_back(1);
+ a.push_back(5);
+ EXPECT_EQ(a.at(0), 8);
+ EXPECT_EQ(a.at(1), 9);
+ EXPECT_EQ(a.at(2), 7);
+ EXPECT_EQ(a.at(3), 1);
+ EXPECT_EQ(a.at(4), 5);
+ EXPECT_EQ(a.size(), 5);
+
+ a.erase(a.begin() + 1, a.begin() + 3);
+ EXPECT_EQ(a.at(0), 8);
+ EXPECT_EQ(a.at(1), 1);
+ EXPECT_EQ(a.at(2), 5);
+ EXPECT_EQ(a.size(), 3);
+
+ a.erase(a.begin());
+ EXPECT_EQ(a.at(0), 1);
+ EXPECT_EQ(a.at(1), 5);
+ EXPECT_EQ(a.size(), 2);
+
+ a.erase(a.end() - 1);
+ EXPECT_EQ(a.at(0), 1);
+ EXPECT_EQ(a.size(), 1);
+}
+
} // namespace testing
} // namespace aos
diff --git a/frc971/rockpi/build_rootfs.sh b/frc971/rockpi/build_rootfs.sh
index bb8b84a..78ae6ac 100755
--- a/frc971/rockpi/build_rootfs.sh
+++ b/frc971/rockpi/build_rootfs.sh
@@ -168,13 +168,13 @@
cat << __EOF__ | sudo tee "${PARTITION}/boot/sdcard_extlinux.conf"
label Linux ${KERNEL_VERSION}
kernel /vmlinuz-${KERNEL_VERSION}
- append earlycon=uart8250,mmio32,0xff1a0000 earlyprintk console=ttyS2,1500000n8 root=/dev/mmcblk0p2 ro rootfstype=ext4 rootwait
+ append earlycon=uart8250,mmio32,0xff1a0000 earlyprintk console=ttyS2,1500000n8 root=/dev/mmcblk0p2 ro rootfstype=ext4 rootflags=data=journal rootwait
fdtdir /dtbs/${KERNEL_VERSION}/
__EOF__
cat << __EOF__ | sudo tee "${PARTITION}/boot/emmc_extlinux.conf"
label Linux ${KERNEL_VERSION}
kernel /vmlinuz-${KERNEL_VERSION}
- append earlycon=uart8250,mmio32,0xff1a0000 earlyprintk console=ttyS2,1500000n8 root=/dev/mmcblk1p2 ro rootfstype=ext4 rootwait
+ append earlycon=uart8250,mmio32,0xff1a0000 earlyprintk console=ttyS2,1500000n8 root=/dev/mmcblk1p2 ro rootfstype=ext4 rootflags=data=journal rootwait
fdtdir /dtbs/${KERNEL_VERSION}/
__EOF__
diff --git a/frc971/rockpi/contents/etc/fstab b/frc971/rockpi/contents/etc/fstab
index c148c2d..0074a98 100644
--- a/frc971/rockpi/contents/etc/fstab
+++ b/frc971/rockpi/contents/etc/fstab
@@ -1,2 +1,2 @@
-/dev/mmcblk0p2 / auto errors=remount-ro 0 0
+/dev/mmcblk0p2 / ext4 rw,relatime,sync,dirsync,nodelalloc,errors=remount-ro 0 0
tmpfs /dev/shm tmpfs rw,nosuid,nodev,size=90% 0 0
diff --git a/frc971/vision/calibration_accumulator.h b/frc971/vision/calibration_accumulator.h
index 4b59406..132bb4b 100644
--- a/frc971/vision/calibration_accumulator.h
+++ b/frc971/vision/calibration_accumulator.h
@@ -89,7 +89,8 @@
std::vector<std::vector<cv::Point2f>> charuco_corners) {
auto builder = annotations_sender_.MakeBuilder();
builder.CheckOk(builder.Send(
- BuildAnnotations(eof, charuco_corners, 2.0, builder.fbb())));
+ BuildAnnotations(builder.fbb(), eof, charuco_corners,
+ std::vector<double>{0.0, 1.0, 0.0, 1.0}, 2.0)));
}
private:
diff --git a/frc971/vision/charuco_lib.cc b/frc971/vision/charuco_lib.cc
index 2f9e81a..ac708f6 100644
--- a/frc971/vision/charuco_lib.cc
+++ b/frc971/vision/charuco_lib.cc
@@ -456,35 +456,15 @@
}
flatbuffers::Offset<foxglove::ImageAnnotations> BuildAnnotations(
+ flatbuffers::FlatBufferBuilder *fbb,
const aos::monotonic_clock::time_point monotonic_now,
- const std::vector<std::vector<cv::Point2f>> &corners, double thickness,
- flatbuffers::FlatBufferBuilder *fbb) {
+ const std::vector<std::vector<cv::Point2f>> &corners,
+ const std::vector<double> rgba_color, const double thickness,
+ const foxglove::PointsAnnotationType line_type) {
std::vector<flatbuffers::Offset<foxglove::PointsAnnotation>> rectangles;
- const struct timespec now_t = aos::time::to_timespec(monotonic_now);
- foxglove::Time time{static_cast<uint32_t>(now_t.tv_sec),
- static_cast<uint32_t>(now_t.tv_nsec)};
- // Draw the points in pink
- const flatbuffers::Offset<foxglove::Color> color_offset =
- foxglove::CreateColor(*fbb, 1.0, 0.75, 0.8, 1.0);
for (const std::vector<cv::Point2f> &rectangle : corners) {
- std::vector<flatbuffers::Offset<foxglove::Point2>> points_offsets;
- for (const cv::Point2f &point : rectangle) {
- points_offsets.push_back(foxglove::CreatePoint2(*fbb, point.x, point.y));
- }
- const flatbuffers::Offset<
- flatbuffers::Vector<flatbuffers::Offset<foxglove::Point2>>>
- points_offset = fbb->CreateVector(points_offsets);
- std::vector<flatbuffers::Offset<foxglove::Color>> color_offsets(
- points_offsets.size(), color_offset);
- auto colors_offset = fbb->CreateVector(color_offsets);
- foxglove::PointsAnnotation::Builder points_builder(*fbb);
- points_builder.add_timestamp(&time);
- points_builder.add_type(foxglove::PointsAnnotationType::POINTS);
- points_builder.add_points(points_offset);
- points_builder.add_outline_color(color_offset);
- points_builder.add_outline_colors(colors_offset);
- points_builder.add_thickness(thickness);
- rectangles.push_back(points_builder.Finish());
+ rectangles.push_back(BuildPointsAnnotation(
+ fbb, monotonic_now, rectangle, rgba_color, thickness, line_type));
}
const auto rectangles_offset = fbb->CreateVector(rectangles);
@@ -493,6 +473,39 @@
return annotation_builder.Finish();
}
+flatbuffers::Offset<foxglove::PointsAnnotation> BuildPointsAnnotation(
+ flatbuffers::FlatBufferBuilder *fbb,
+ const aos::monotonic_clock::time_point monotonic_now,
+ const std::vector<cv::Point2f> &corners,
+ const std::vector<double> rgba_color, const double thickness,
+ const foxglove::PointsAnnotationType line_type) {
+ const struct timespec now_t = aos::time::to_timespec(monotonic_now);
+ foxglove::Time time{static_cast<uint32_t>(now_t.tv_sec),
+ static_cast<uint32_t>(now_t.tv_nsec)};
+ const flatbuffers::Offset<foxglove::Color> color_offset =
+ foxglove::CreateColor(*fbb, rgba_color[0], rgba_color[1], rgba_color[2],
+ rgba_color[3]);
+ std::vector<flatbuffers::Offset<foxglove::Point2>> points_offsets;
+ for (const cv::Point2f &point : corners) {
+ points_offsets.push_back(foxglove::CreatePoint2(*fbb, point.x, point.y));
+ }
+ const flatbuffers::Offset<
+ flatbuffers::Vector<flatbuffers::Offset<foxglove::Point2>>>
+ points_offset = fbb->CreateVector(points_offsets);
+ std::vector<flatbuffers::Offset<foxglove::Color>> color_offsets(
+ points_offsets.size(), color_offset);
+ auto colors_offset = fbb->CreateVector(color_offsets);
+ foxglove::PointsAnnotation::Builder points_builder(*fbb);
+ points_builder.add_timestamp(&time);
+ points_builder.add_type(line_type);
+ points_builder.add_points(points_offset);
+ points_builder.add_outline_color(color_offset);
+ points_builder.add_outline_colors(colors_offset);
+ points_builder.add_thickness(thickness);
+
+ return points_builder.Finish();
+}
+
TargetType TargetTypeFromString(std::string_view str) {
if (str == "aruco") {
return TargetType::kAruco;
diff --git a/frc971/vision/charuco_lib.h b/frc971/vision/charuco_lib.h
index f1846b5..2c35a0b 100644
--- a/frc971/vision/charuco_lib.h
+++ b/frc971/vision/charuco_lib.h
@@ -174,9 +174,25 @@
// Puts the provided charuco corners into a foxglove ImageAnnotation type for
// visualization purposes.
flatbuffers::Offset<foxglove::ImageAnnotations> BuildAnnotations(
+ flatbuffers::FlatBufferBuilder *fbb,
const aos::monotonic_clock::time_point monotonic_now,
- const std::vector<std::vector<cv::Point2f>> &corners, double thickness,
- flatbuffers::FlatBufferBuilder *fbb);
+ const std::vector<std::vector<cv::Point2f>> &corners,
+ const std::vector<double> rgba_color = std::vector<double>{0.0, 1.0, 0.0,
+ 1.0},
+ const double thickness = 5,
+ const foxglove::PointsAnnotationType line_type =
+ foxglove::PointsAnnotationType::POINTS);
+
+// Creates a PointsAnnotation to build up ImageAnnotations with different types
+flatbuffers::Offset<foxglove::PointsAnnotation> BuildPointsAnnotation(
+ flatbuffers::FlatBufferBuilder *fbb,
+ const aos::monotonic_clock::time_point monotonic_now,
+ const std::vector<cv::Point2f> &corners,
+ const std::vector<double> rgba_color = std::vector<double>{0.0, 1.0, 0.0,
+ 1.0},
+ const double thickness = 5,
+ const foxglove::PointsAnnotationType line_type =
+ foxglove::PointsAnnotationType::POINTS);
} // namespace vision
} // namespace frc971
diff --git a/scouting/BUILD b/scouting/BUILD
index 1d6ac5d..c98c741 100644
--- a/scouting/BUILD
+++ b/scouting/BUILD
@@ -1,5 +1,5 @@
-load("@aspect_rules_cypress//cypress:defs.bzl", "cypress_module_test")
load("//tools/build_rules:apache.bzl", "apache_wrapper")
+load("//tools/build_rules:js.bzl", "cypress_test")
sh_binary(
name = "scouting",
@@ -16,23 +16,13 @@
],
)
-cypress_module_test(
+cypress_test(
name = "scouting_test",
- args = [
- "run",
- "--config-file=cypress.config.js",
- "--browser=../../chrome_linux/chrome",
- ],
- browsers = ["@chrome_linux//:all"],
- copy_data_to_bin = False,
- cypress = "//:node_modules/cypress",
data = [
- "cypress.config.js",
"scouting_test.cy.js",
"//scouting/testing:scouting_test_servers",
- "@xvfb_amd64//:wrapped_bin/Xvfb",
],
- runner = "cypress_runner.js",
+ runner = "scouting_test_runner.js",
)
apache_wrapper(
diff --git a/scouting/README.md b/scouting/README.md
index 6e31b5b..7e1a58a 100644
--- a/scouting/README.md
+++ b/scouting/README.md
@@ -124,3 +124,8 @@
where `<year>` is the year of the event and `<event_code>` is the short code
for the event. A list of event codes is available
[here](http://frclinks.com/#eventcodes).
+
+
+Debugging Cypress tests
+--------------------------------------------------------------------------------
+See the [dedicated section](../tools/js#debugging-cypress-tests) for this.
diff --git a/scouting/deploy/BUILD b/scouting/deploy/BUILD
index ed4b9cd..2bf4b4e 100644
--- a/scouting/deploy/BUILD
+++ b/scouting/deploy/BUILD
@@ -17,6 +17,15 @@
include_runfiles = True,
package_dir = "opt/frc971/scouting_server",
strip_prefix = ".",
+ # The "include_runfiles" attribute creates a runfiles tree as seen from
+ # within the workspace directory. But what we really want is the runfiles
+ # tree as seen from the root of the runfiles tree (i.e. one directory up).
+ # So we work around it by manually adding some symlinks that let us pretend
+ # that we're at the root of the runfiles tree.
+ symlinks = {
+ "opt/frc971/scouting_server/org_frc971": ".",
+ "opt/frc971/scouting_server/bazel_tools": "external/bazel_tools",
+ },
)
pkg_tar(
diff --git a/scouting/deploy/scouting.service b/scouting/deploy/scouting.service
index b22a57d..5aa64b0 100644
--- a/scouting/deploy/scouting.service
+++ b/scouting/deploy/scouting.service
@@ -7,6 +7,7 @@
Group=www-data
Type=simple
WorkingDirectory=/opt/frc971/scouting_server
+Environment=RUNFILES_DIR=/opt/frc971/scouting_server
ExecStart=/opt/frc971/scouting_server/scouting/scouting \
-port 8080 \
-db_config /var/frc971/scouting/db_config.json \
diff --git a/scouting/scouting_test.cy.js b/scouting/scouting_test.cy.js
index e27fb54..a2dfa83 100644
--- a/scouting/scouting_test.cy.js
+++ b/scouting/scouting_test.cy.js
@@ -115,6 +115,8 @@
cy.get('#comp_level').should('have.value', '3: sf');
});
+ //TODO(FILIP): Rewrite tests for the new scouting interface.
+ /*
it('should: error on unknown match.', () => {
switchToTab('Data Entry');
headerShouldBe('Team Selection');
@@ -167,6 +169,7 @@
}
}
});
+
it('should: review and submit correct data.', () => {
switchToTab('Data Entry');
@@ -234,25 +237,7 @@
headerShouldBe('Success');
});
- it('should: load all images successfully.', () => {
- switchToTab('Data Entry');
-
- // Get to the Auto display with the field pictures.
- headerShouldBe('Team Selection');
- clickButton('Next');
- headerShouldBe('Auto');
-
- // We expect 2 fully loaded images for each of the orientations.
- // 2 images for the original orientation and 2 images for the flipped orientation.
- for (let i = 0; i < 2; i++) {
- cy.get('img').should(($imgs) => {
- for (const $img of $imgs) {
- expect($img.naturalWidth).to.be.greaterThan(0);
- }
- });
- clickButton('Flip');
- }
- });
+ */
it('should: submit note scouting for multiple teams', () => {
// Navigate to Notes Page.
diff --git a/scouting/cypress_runner.js b/scouting/scouting_test_runner.js
similarity index 82%
rename from scouting/cypress_runner.js
rename to scouting/scouting_test_runner.js
index 6c63adb..3106ee7 100644
--- a/scouting/cypress_runner.js
+++ b/scouting/scouting_test_runner.js
@@ -57,17 +57,21 @@
// Wait for the server to be ready, run the tests, then shut down the server.
(async () => {
+ // Parse command line options.
+ let runOptions = await cypress.cli.parseRunArguments(process.argv.slice(2));
+
await serverStartup;
- const result = await cypress.run({
- headless: true,
- config: {
- baseUrl: 'http://localhost:8000',
- screenshotsFolder:
- process.env.TEST_UNDECLARED_OUTPUTS_DIR + '/screenshots',
- video: false,
- videosFolder: process.env.TEST_UNDECLARED_OUTPUTS_DIR + '/videos',
- },
- });
+ const result = await cypress.run(
+ Object.assign(runOptions, {
+ config: {
+ baseUrl: 'http://localhost:8000',
+ screenshotsFolder:
+ process.env.TEST_UNDECLARED_OUTPUTS_DIR + '/screenshots',
+ video: false,
+ videosFolder: process.env.TEST_UNDECLARED_OUTPUTS_DIR + '/videos',
+ },
+ })
+ );
await servers.kill();
await serverShutdown;
diff --git a/scouting/www/entry/BUILD b/scouting/www/entry/BUILD
index 37b7fe6..07c1d79 100644
--- a/scouting/www/entry/BUILD
+++ b/scouting/www/entry/BUILD
@@ -11,9 +11,7 @@
deps = [
":node_modules/@angular/forms",
"//scouting/webserver/requests/messages:error_response_ts_fbs",
- "//scouting/webserver/requests/messages:submit_data_scouting_response_ts_fbs",
- "//scouting/webserver/requests/messages:submit_data_scouting_ts_fbs",
- "//scouting/www/counter_button:_lib",
+ "//scouting/webserver/requests/messages:submit_actions_ts_fbs",
"@com_github_google_flatbuffers//ts:flatbuffers_ts",
],
)
diff --git a/scouting/www/entry/entry.component.ts b/scouting/www/entry/entry.component.ts
index 6209669..9aae0d1 100644
--- a/scouting/www/entry/entry.component.ts
+++ b/scouting/www/entry/entry.component.ts
@@ -11,17 +11,25 @@
import {Builder, ByteBuffer} from 'flatbuffers';
import {ErrorResponse} from '../../webserver/requests/messages/error_response_generated';
import {
- ClimbLevel,
- SubmitDataScouting,
-} from '../../webserver/requests/messages/submit_data_scouting_generated';
-import {SubmitDataScoutingResponse} from '../../webserver/requests/messages/submit_data_scouting_response_generated';
+ ObjectType,
+ ScoreLevel,
+ SubmitActions,
+ StartMatchAction,
+ PickupObjectAction,
+ PlaceObjectAction,
+ RobotDeathAction,
+ EndMatchAction,
+ ActionType,
+ Action,
+} from '../../webserver/requests/messages/submit_actions_generated';
type Section =
| 'Team Selection'
- | 'Auto'
- | 'TeleOp'
- | 'Climb'
- | 'Other'
+ | 'Init'
+ | 'Pickup'
+ | 'Place'
+ | 'Endgame'
+ | 'Dead'
| 'Review and Submit'
| 'Success';
@@ -38,22 +46,42 @@
f: 'Finals',
};
-const IMAGES_ARRAY = [
- {
- id: 'field_quadrants_image',
- original_image:
- '/sha256/cbb99a057a2504e80af526dae7a0a04121aed84c56a6f4889e9576fe1c20c61e/pictures/field/quadrants.jpeg',
- reversed_image:
- '/sha256/ee4d24cf6b850158aa64e2b301c31411cb28f88a247a8916abb97214bb251eb5/pictures/field/reversed_quadrants.jpeg',
- },
- {
- id: 'field_balls_image',
- original_image:
- '/sha256/e095cc8a75d804b0e2070e0a941fab37154176756d4c1a775e53cc48c3a732b9/pictures/field/balls.jpeg',
- reversed_image:
- '/sha256/fe4a4605c03598611c583d4dcdf28e06a056a17302ae91f5c527568966d95f3a/pictures/field/reversed_balls.jpeg',
- },
-];
+type ActionT =
+ | {
+ type: 'startMatchAction';
+ timestamp?: number;
+ position: number;
+ }
+ | {
+ type: 'pickupObjectAction';
+ timestamp?: number;
+ objectType: ObjectType;
+ auto?: boolean;
+ }
+ | {
+ type: 'placeObjectAction';
+ timestamp?: number;
+ objectType?: ObjectType;
+ scoreLevel: ScoreLevel;
+ auto?: boolean;
+ }
+ | {
+ type: 'robotDeathAction';
+ timestamp?: number;
+ robotOn: boolean;
+ }
+ | {
+ type: 'endMatchAction';
+ docked: boolean;
+ engaged: boolean;
+ timestamp?: number;
+ }
+ | {
+ // This is not a action that is submitted,
+ // It is used for undoing purposes.
+ type: 'endAutoPhase';
+ timestamp?: number;
+ };
@Component({
selector: 'app-entry',
@@ -63,9 +91,10 @@
export class EntryComponent {
// Re-export the type here so that we can use it in the `[value]` attribute
// of radio buttons.
- readonly ClimbLevel = ClimbLevel;
readonly COMP_LEVELS = COMP_LEVELS;
readonly COMP_LEVEL_LABELS = COMP_LEVEL_LABELS;
+ readonly ObjectType = ObjectType;
+ readonly ScoreLevel = ScoreLevel;
section: Section = 'Team Selection';
@Output() switchTabsEvent = new EventEmitter<string>();
@@ -73,118 +102,169 @@
@Input() teamNumber: number = 1;
@Input() setNumber: number = 1;
@Input() compLevel: CompLevel = 'qm';
- autoUpperShotsMade: number = 0;
- autoLowerShotsMade: number = 0;
- autoShotsMissed: number = 0;
- teleUpperShotsMade: number = 0;
- teleLowerShotsMade: number = 0;
- teleShotsMissed: number = 0;
- defensePlayedOnScore: number = 0;
- defensePlayedScore: number = 0;
- level: ClimbLevel = ClimbLevel.NoAttempt;
- ball1: boolean = false;
- ball2: boolean = false;
- ball3: boolean = false;
- ball4: boolean = false;
- ball5: boolean = false;
- quadrant: number = 1;
+
+ actionList: ActionT[] = [];
errorMessage: string = '';
- noShow: boolean = false;
- neverMoved: boolean = false;
- batteryDied: boolean = false;
- mechanicallyBroke: boolean = false;
- lostComs: boolean = false;
- comment: string = '';
+ autoPhase: boolean = true;
+ lastObject: ObjectType = null;
+
+ matchStartTimestamp: number = 0;
+
+ addAction(action: ActionT): void {
+ action.timestamp = Math.floor(Date.now() / 1000);
+ if (action.type == 'startMatchAction') {
+ // Unix nanosecond timestamp.
+ this.matchStartTimestamp = Date.now() * 1e6;
+ action.timestamp = 0;
+ } else {
+ // Unix nanosecond timestamp relative to match start.
+ action.timestamp = Date.now() * 1e6 - this.matchStartTimestamp;
+ }
+
+ if (
+ action.type == 'pickupObjectAction' ||
+ action.type == 'placeObjectAction'
+ ) {
+ action.auto = this.autoPhase;
+ if (action.type == 'pickupObjectAction') {
+ this.lastObject = action.objectType;
+ } else if (action.type == 'placeObjectAction') {
+ action.objectType = this.lastObject;
+ }
+ }
+ this.actionList.push(action);
+ }
+
+ undoLastAction() {
+ if (this.actionList.length > 0) {
+ let lastAction = this.actionList.pop();
+ switch (lastAction?.type) {
+ case 'endAutoPhase':
+ this.autoPhase = true;
+ case 'pickupObjectAction':
+ this.section = 'Pickup';
+ break;
+ case 'placeObjectAction':
+ this.section = 'Place';
+ break;
+ case 'endMatchAction':
+ this.section = 'Pickup';
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ changeSectionTo(target: Section) {
+ this.section = target;
+ }
@ViewChild('header') header: ElementRef;
- nextSection() {
- if (this.section === 'Team Selection') {
- this.section = 'Auto';
- } else if (this.section === 'Auto') {
- this.section = 'TeleOp';
- } else if (this.section === 'TeleOp') {
- this.section = 'Climb';
- } else if (this.section === 'Climb') {
- this.section = 'Other';
- } else if (this.section === 'Other') {
- this.section = 'Review and Submit';
- } else if (this.section === 'Review and Submit') {
- this.submitDataScouting();
- return;
- } else if (this.section === 'Success') {
- this.switchTabsEvent.emit('MatchList');
- return;
- }
- // Scroll back to the top so that we can be sure the user sees the
- // entire next screen. Otherwise it's easy to overlook input fields.
- this.scrollToTop();
- }
-
- prevSection() {
- if (this.section === 'Auto') {
- this.section = 'Team Selection';
- } else if (this.section === 'TeleOp') {
- this.section = 'Auto';
- } else if (this.section === 'Climb') {
- this.section = 'TeleOp';
- } else if (this.section === 'Other') {
- this.section = 'Climb';
- } else if (this.section === 'Review and Submit') {
- this.section = 'Other';
- }
- // Scroll back to the top so that we can be sure the user sees the
- // entire previous screen. Otherwise it's easy to overlook input
- // fields.
- this.scrollToTop();
- }
-
- flipImages() {
- for (let obj of IMAGES_ARRAY) {
- let img = document.getElementById(obj.id) as HTMLImageElement;
- img.src = img.src.endsWith(obj.original_image)
- ? obj.reversed_image
- : obj.original_image;
- }
- }
private scrollToTop() {
this.header.nativeElement.scrollIntoView();
}
- async submitDataScouting() {
- this.errorMessage = '';
-
+ async submitActions() {
const builder = new Builder();
- const compLevel = builder.createString(this.compLevel);
- const comment = builder.createString(this.comment);
- SubmitDataScouting.startSubmitDataScouting(builder);
- SubmitDataScouting.addTeam(builder, this.teamNumber);
- SubmitDataScouting.addMatch(builder, this.matchNumber);
- SubmitDataScouting.addSetNumber(builder, this.setNumber);
- SubmitDataScouting.addCompLevel(builder, compLevel);
- SubmitDataScouting.addMissedShotsAuto(builder, this.autoShotsMissed);
- SubmitDataScouting.addUpperGoalAuto(builder, this.autoUpperShotsMade);
- SubmitDataScouting.addLowerGoalAuto(builder, this.autoLowerShotsMade);
- SubmitDataScouting.addMissedShotsTele(builder, this.teleShotsMissed);
- SubmitDataScouting.addUpperGoalTele(builder, this.teleUpperShotsMade);
- SubmitDataScouting.addLowerGoalTele(builder, this.teleLowerShotsMade);
- SubmitDataScouting.addDefenseRating(builder, this.defensePlayedScore);
- SubmitDataScouting.addDefenseReceivedRating(
+ const actionOffsets: number[] = [];
+
+ for (const action of this.actionList) {
+ let actionOffset: number | undefined;
+ console.log(action.type);
+
+ switch (action.type) {
+ case 'startMatchAction':
+ const startMatchActionOffset =
+ StartMatchAction.createStartMatchAction(builder, action.position);
+ actionOffset = Action.createAction(
+ builder,
+ action.timestamp || 0,
+ ActionType.StartMatchAction,
+ startMatchActionOffset
+ );
+ break;
+
+ case 'pickupObjectAction':
+ const pickupObjectActionOffset =
+ PickupObjectAction.createPickupObjectAction(
+ builder,
+ action.objectType,
+ action.auto || false
+ );
+ actionOffset = Action.createAction(
+ builder,
+ action.timestamp || 0,
+ ActionType.PickupObjectAction,
+ pickupObjectActionOffset
+ );
+ break;
+
+ case 'placeObjectAction':
+ const placeObjectActionOffset =
+ PlaceObjectAction.createPlaceObjectAction(
+ builder,
+ action.objectType,
+ action.scoreLevel,
+ action.auto || false
+ );
+ actionOffset = Action.createAction(
+ builder,
+ action.timestamp || 0,
+ ActionType.PlaceObjectAction,
+ placeObjectActionOffset
+ );
+ break;
+
+ case 'robotDeathAction':
+ const robotDeathActionOffset =
+ RobotDeathAction.createRobotDeathAction(builder, action.robotOn);
+ actionOffset = Action.createAction(
+ builder,
+ action.timestamp || 0,
+ ActionType.RobotDeathAction,
+ robotDeathActionOffset
+ );
+ break;
+
+ case 'endMatchAction':
+ const endMatchActionOffset = EndMatchAction.createEndMatchAction(
+ builder,
+ action.docked,
+ action.engaged
+ );
+ actionOffset = Action.createAction(
+ builder,
+ action.timestamp || 0,
+ ActionType.EndMatchAction,
+ endMatchActionOffset
+ );
+ break;
+
+ case 'endAutoPhase':
+ // Not important action.
+ break;
+
+ default:
+ throw new Error(`Unknown action type`);
+ }
+
+ if (actionOffset !== undefined) {
+ actionOffsets.push(actionOffset);
+ }
+ }
+
+ const actionsVector = SubmitActions.createActionsListVector(
builder,
- this.defensePlayedOnScore
+ actionOffsets
);
- SubmitDataScouting.addAutoBall1(builder, this.ball1);
- SubmitDataScouting.addAutoBall2(builder, this.ball2);
- SubmitDataScouting.addAutoBall3(builder, this.ball3);
- SubmitDataScouting.addAutoBall4(builder, this.ball4);
- SubmitDataScouting.addAutoBall5(builder, this.ball5);
- SubmitDataScouting.addStartingQuadrant(builder, this.quadrant);
- SubmitDataScouting.addClimbLevel(builder, this.level);
- SubmitDataScouting.addComment(builder, comment);
- builder.finish(SubmitDataScouting.endSubmitDataScouting(builder));
+ SubmitActions.startSubmitActions(builder);
+ SubmitActions.addActionsList(builder, actionsVector);
+ builder.finish(SubmitActions.endSubmitActions(builder));
const buffer = builder.asUint8Array();
- const res = await fetch('/requests/submit/data_scouting', {
+ const res = await fetch('/requests/submit/actions', {
method: 'POST',
body: buffer,
});
@@ -192,6 +272,7 @@
if (res.ok) {
// We successfully submitted the data. Report success.
this.section = 'Success';
+ this.actionList = [];
} else {
const resBuffer = await res.arrayBuffer();
const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));
diff --git a/scouting/www/entry/entry.module.ts b/scouting/www/entry/entry.module.ts
index a2aa7bb..c74d4d0 100644
--- a/scouting/www/entry/entry.module.ts
+++ b/scouting/www/entry/entry.module.ts
@@ -1,22 +1,11 @@
import {NgModule, Pipe, PipeTransform} from '@angular/core';
import {CommonModule} from '@angular/common';
import {FormsModule} from '@angular/forms';
-
-import {CounterButtonModule} from '@org_frc971/scouting/www/counter_button';
import {EntryComponent} from './entry.component';
-import {ClimbLevel} from '../../webserver/requests/messages/submit_data_scouting_generated';
-
-@Pipe({name: 'levelToString'})
-export class LevelToStringPipe implements PipeTransform {
- transform(level: ClimbLevel): string {
- return ClimbLevel[level];
- }
-}
-
@NgModule({
- declarations: [EntryComponent, LevelToStringPipe],
+ declarations: [EntryComponent],
exports: [EntryComponent],
- imports: [CommonModule, FormsModule, CounterButtonModule],
+ imports: [CommonModule, FormsModule],
})
export class EntryModule {}
diff --git a/scouting/www/entry/entry.ng.html b/scouting/www/entry/entry.ng.html
index e73cfb0..ec95f39 100644
--- a/scouting/www/entry/entry.ng.html
+++ b/scouting/www/entry/entry.ng.html
@@ -49,386 +49,133 @@
<div class="buttons">
<!-- hack to right align the next button -->
<div></div>
- <button class="btn btn-primary" (click)="nextSection()">Next</button>
+ <button class="btn btn-primary" (click)="changeSectionTo('Init');">
+ Next
+ </button>
</div>
</div>
- <div *ngSwitchCase="'Auto'" id="auto" class="container-fluid">
- <button class="buttons" id="switch_field_button" (click)="flipImages()">
- Flip
- </button>
- <div class="row">
- <img
- id="field_quadrants_image"
- src="/sha256/cbb99a057a2504e80af526dae7a0a04121aed84c56a6f4889e9576fe1c20c61e/pictures/field/quadrants.jpeg"
- alt="Quadrants Image"
- />
- <form>
+ <div *ngSwitchCase="'Init'" id="init" class="container-fluid">
+ <h2>Select Starting Position</h2>
+ <div *ngFor="let i of [1, 2, 3, 4]">
+ <label>
<input
type="radio"
- [(ngModel)]="quadrant"
- name="quadrant"
- id="quadrant1"
- [value]="1"
+ name="radio-group"
+ [value]="i"
+ (change)="selectedValue = $event.target.value"
/>
- <label for="quadrant1">Quadrant 1</label>
- <input
- type="radio"
- [(ngModel)]="quadrant"
- name="quadrant"
- id="quadrant2"
- [value]="2"
- />
- <label for="quadrant2">Quadrant 2</label>
- <br />
- <input
- type="radio"
- [(ngModel)]="quadrant"
- name="quadrant"
- id="quadrant3"
- [value]="3"
- />
- <label for="quadrant3">Quadrant 3</label>
- <input
- type="radio"
- [(ngModel)]="quadrant"
- name="quadrant"
- id="quadrant4"
- [value]="4"
- />
- <label for="quadrant4">Quadrant 4</label>
- </form>
- </div>
- <div class="row">
- <img
- id="field_balls_image"
- src="/sha256/e095cc8a75d804b0e2070e0a941fab37154176756d4c1a775e53cc48c3a732b9/pictures/field/balls.jpeg"
- alt="Balls Image"
- />
- <form>
- <!--Choice for each ball location-->
- <input
- [(ngModel)]="ball1"
- type="checkbox"
- name="1ball"
- value="1"
- id="ball-1"
- />
- <label for="ball-1">Ball 1</label>
- <input
- [(ngModel)]="ball2"
- type="checkbox"
- name="2ball"
- value="2"
- id="ball-2"
- />
- <label for="ball-2">Ball 2</label>
- <br />
- <input
- [(ngModel)]="ball3"
- type="checkbox"
- name="3ball"
- value="3"
- id="ball-3"
- />
- <label for="ball-3">Ball 3</label>
- <input
- [(ngModel)]="ball4"
- type="checkbox"
- name="4ball"
- value="4"
- id="ball-4"
- />
- <label for="ball-4">Ball 4</label>
- <br />
- <input
- [(ngModel)]="ball5"
- type="checkbox"
- name="5ball"
- value="5"
- id="ball-5"
- />
- <label for="ball-5">Ball 5</label>
- </form>
- </div>
- <div class="row justify-content-center">
- <frc971-counter-button class="col-4" [(value)]="autoUpperShotsMade">
- Upper
- </frc971-counter-button>
- <frc971-counter-button class="col-4" [(value)]="autoLowerShotsMade">
- Lower
- </frc971-counter-button>
- <frc971-counter-button class="col-4" [(value)]="autoShotsMissed">
- Missed
- </frc971-counter-button>
- </div>
- <div class="buttons">
- <button class="btn btn-primary" (click)="prevSection()">Back</button>
- <button class="btn btn-primary" (click)="nextSection()">Next</button>
- </div>
- </div>
-
- <div *ngSwitchCase="'TeleOp'" id="teleop" class="container-fluid">
- <div class="row justify-content-center">
- <frc971-counter-button class="col-4" [(value)]="teleUpperShotsMade">
- Upper
- </frc971-counter-button>
- <frc971-counter-button class="col-4" [(value)]="teleLowerShotsMade">
- Lower
- </frc971-counter-button>
- <frc971-counter-button class="col-4" [(value)]="teleShotsMissed">
- Missed
- </frc971-counter-button>
- </div>
- <div class="buttons">
- <button class="btn btn-primary" (click)="prevSection()">Back</button>
- <button class="btn btn-primary" (click)="nextSection()">Next</button>
- </div>
- </div>
-
- <div *ngSwitchCase="'Climb'" id="climb" class="container-fluid">
- <form>
- <input
- [(ngModel)]="level"
- type="radio"
- name="level"
- id="no_attempt"
- [value]="ClimbLevel.NoAttempt"
- />
- <label for="no_attempt">No climbing attempt</label>
- <br />
- <input
- [(ngModel)]="level"
- type="radio"
- name="level"
- id="low"
- [value]="ClimbLevel.Low"
- />
- <label for="low">Low</label>
- <br />
- <input
- [(ngModel)]="level"
- type="radio"
- name="level"
- id="medium"
- [value]="ClimbLevel.Medium"
- />
- <label for="medium">Medium</label>
- <br />
- <input
- [(ngModel)]="level"
- type="radio"
- name="level"
- id="high"
- [value]="ClimbLevel.High"
- />
- <label for="high">High</label>
- <br />
- <input
- [(ngModel)]="level"
- type="radio"
- name="level"
- id="traversal"
- [value]="ClimbLevel.Traversal"
- />
- <label for="traversal">Traversal</label>
- <br />
- <input
- [(ngModel)]="level"
- type="radio"
- name="level"
- id="failed"
- [value]="ClimbLevel.Failed"
- />
- <label for="failed">Failed</label>
- <br />
- <input
- [(ngModel)]="level"
- type="radio"
- name="level"
- id="failed_with_plenty_of_time"
- [value]="ClimbLevel.FailedWithPlentyOfTime"
- />
- <label for="failed_with_plenty_of_time">
- Failed (attempted with more than 10 seconds left)
+ {{ i }}
</label>
- <br />
- </form>
+ </div>
<div class="buttons">
- <button class="btn btn-primary" (click)="prevSection()">Back</button>
- <button class="btn btn-primary" (click)="nextSection()">Next</button>
+ <button
+ class="btn btn-primary"
+ [disabled]="!selectedValue"
+ (click)="changeSectionTo('Pickup'); addAction({type: 'startMatchAction', position: selectedValue});"
+ >
+ Start Match
+ </button>
</div>
</div>
- <div *ngSwitchCase="'Other'" id="defense" class="container-fluid">
- <h4 class="text-center">
- How much did other robots play defense against this robot?
- </h4>
- 0 - No defense played against this robot
- <br />
- 1 - Minimal defense
- <br />
- 2 - Some defense
- <br />
- 3 - About half the match was played against defense
- <br />
- 4 - Good amount of defense
- <br />
- 5 - Constant defense
- <div class="row" style="min-height: 50px">
- <div class="col-10">
- <input
- type="range"
- min="0"
- max="5"
- [(ngModel)]="defensePlayedOnScore"
- />
- </div>
- <div class="col">
- <h6>{{defensePlayedOnScore}}</h6>
- </div>
- </div>
-
- <h4 class="text-center">
- How much did this robot play defense against other robots?
- </h4>
- 0 - This robot did not play defense
- <br />
- 1 - Minimal defense
- <br />
- 2 - Some defense
- <br />
- 3 - Defense was played for about half the match
- <br />
- 4 - Good amount of defense
- <br />
- 5 - Constant defense
- <div class="row">
- <div class="col-10">
- <input type="range" min="0" max="5" [(ngModel)]="defensePlayedScore" />
- </div>
- <div class="col">
- <h6>{{defensePlayedScore}}</h6>
- </div>
- </div>
-
- <div class="row">
- <form>
- <input
- type="checkbox"
- [(ngModel)]="noShow"
- name="no_show"
- id="no_show"
- />
- <label for="no_show">No show</label>
- <br />
- <input
- type="checkbox"
- [(ngModel)]="neverMoved"
- name="never_moved"
- id="never_moved"
- />
- <label for="never_moved">Never moved</label>
- <br />
- <input
- type="checkbox"
- [(ngModel)]="batteryDied"
- name="battery_died"
- id="battery_died"
- />
- <label for="battery_died">Battery died</label>
- <br />
- <input
- type="checkbox"
- [(ngModel)]="mechanicallyBroke"
- name="mechanically_broke"
- id="mechanically_broke"
- />
- <label for="mechanically_broke">Broke (mechanically)</label>
- <br />
- <input
- type="checkbox"
- [(ngModel)]="lostComs"
- name="lost_coms"
- id="lost_coms"
- />
- <label for="lost_coms">Lost coms</label>
- </form>
- </div>
-
- <div class="row">
- <h4>General Comments About Match</h4>
- <textarea
- [(ngModel)]="comment"
- id="comment"
- placeholder="optional"
- ></textarea>
- </div>
-
- <div class="buttons">
- <button class="btn btn-primary" (click)="prevSection()">Back</button>
- <button class="btn btn-primary" (click)="nextSection()">Next</button>
- </div>
+ <div *ngSwitchCase="'Pickup'" id="PickUp" class="container-fluid">
+ <button class="btn btn-danger" (click)="undoLastAction()">UNDO</button>
+ <button
+ class="btn btn-warning"
+ (click)="changeSectionTo('Place'); addAction({type: 'pickupObjectAction', objectType: ObjectType.kCone});"
+ >
+ CONE
+ </button>
+ <button
+ class="btn btn-primary"
+ (click)="changeSectionTo('Place'); addAction({type: 'pickupObjectAction', objectType: ObjectType.kCube});"
+ >
+ CUBE
+ </button>
+ <button
+ *ngIf="autoPhase"
+ class="btn btn-info"
+ (click)="autoPhase = false; addAction({type: 'endAutoPhase'});"
+ >
+ Start Teleop
+ </button>
+ <button
+ *ngIf="!autoPhase"
+ class="btn btn-info"
+ (click)="changeSectionTo('Endgame')"
+ >
+ Endgame
+ </button>
</div>
- <div *ngSwitchCase="'Review and Submit'" id="review" class="container-fluid">
- <h4>Team Selection</h4>
- <ul>
- <li>Match number: {{matchNumber}}</li>
- <li>Team number: {{teamNumber}}</li>
- <li>SetNumber: {{setNumber}}</li>
- <li>Comp Level: {{COMP_LEVEL_LABELS[compLevel]}}</li>
- </ul>
-
- <h4>Auto</h4>
- <ul>
- <li>Quadrant: {{quadrant}}</li>
- <li>Collected Ball 1: {{ball1}}</li>
- <li>Collected Ball 2: {{ball2}}</li>
- <li>Collected Ball 3: {{ball3}}</li>
- <li>Collected Ball 4: {{ball4}}</li>
- <li>Collected Ball 5: {{ball5}}</li>
- <li>Upper Shots Made: {{autoUpperShotsMade}}</li>
- <li>Lower Shots Made: {{autoLowerShotsMade}}</li>
- <li>Missed Shots: {{autoShotsMissed}}</li>
- </ul>
-
- <h4>TeleOp</h4>
- <ul>
- <li>Upper Shots Made: {{teleUpperShotsMade}}</li>
- <li>Lower Shots Made: {{teleLowerShotsMade}}</li>
- <li>Missed Shots: {{teleShotsMissed}}</li>
- </ul>
-
- <h4>Climb</h4>
- <ul>
- <li>Climb Level: {{level | levelToString}}</li>
- </ul>
-
- <h4>Other</h4>
- <ul>
- <li>Defense Played On Rating: {{defensePlayedOnScore}}</li>
- <li>Defense Played Rating: {{defensePlayedScore}}</li>
- <li>No show: {{noShow}}</li>
- <li>Never moved: {{neverMoved}}</li>
- <li>Battery died: {{batteryDied}}</li>
- <li>Broke (mechanically): {{mechanicallyBroke}}</li>
- <li>Lost coms: {{lostComs}}</li>
- <li>Comments: {{comment}}</li>
- </ul>
-
- <span class="error_message">{{ errorMessage }}</span>
-
- <div class="buttons">
- <button class="btn btn-primary" (click)="prevSection()">Back</button>
- <button class="btn btn-primary" (click)="nextSection()">Submit</button>
- </div>
+ <div *ngSwitchCase="'Place'" id="Place" class="container-fluid">
+ <button class="btn btn-danger" (click)="undoLastAction()">UNDO</button>
+ <button
+ class="btn btn-success"
+ (click)="changeSectionTo('Pickup'); addAction({type: 'placeObjectAction', scoreLevel: ScoreLevel.kHigh});"
+ >
+ HIGH
+ </button>
+ <button
+ class="btn btn-warning"
+ (click)="changeSectionTo('Pickup'); addAction({type: 'placeObjectAction', scoreLevel: ScoreLevel.kMiddle});"
+ >
+ MID
+ </button>
+ <button
+ class="btn btn-danger"
+ (click)="changeSectionTo('Pickup'); addAction({type: 'placeObjectAction', scoreLevel: ScoreLevel.kLow});"
+ >
+ LOW
+ </button>
+ <button
+ *ngIf="autoPhase"
+ class="btn btn-info"
+ (click)="autoPhase = false; addAction({type: 'endAutoPhase'});"
+ >
+ Start Teleop
+ </button>
+ <button
+ *ngIf="!autoPhase"
+ class="btn btn-info"
+ (click)="changeSectionTo('Endgame')"
+ >
+ Endgame
+ </button>
</div>
- <div *ngSwitchCase="'Success'" id="success" class="container-fluid">
- <span>Successfully submitted scouting data.</span>
- <div class="buttons justify-content-end">
- <button class="btn btn-primary" (click)="nextSection()">Continue</button>
- </div>
+ <div *ngSwitchCase="'Endgame'" id="Endgame" class="container-fluid">
+ <button class="btn btn-danger" (click)="undoLastAction()">UNDO</button>
+ <label>
+ <input type="checkbox" (change)="dockedValue = $event.target.value" />
+ Docked
+ </label>
+ <label>
+ <input type="checkbox" (change)="engagedValue = $event.target.value" />
+ Engaged
+ </label>
+ <button
+ *ngIf="!autoPhase"
+ class="btn btn-info"
+ (click)="changeSectionTo('Review and Submit'); addAction({type: 'endMatchAction', docked: dockedValue, engaged: engagedValue});"
+ >
+ End Match
+ </button>
+ </div>
+
+ <div *ngSwitchCase="'Review and Submit'" id="Review" class="container-fluid">
+ <button class="btn btn-danger" (click)="undoLastAction()">UNDO</button>
+ <button
+ *ngIf="!autoPhase"
+ class="btn btn-warning"
+ (click)="submitActions();"
+ >
+ Submit
+ </button>
+ </div>
+
+ <div *ngSwitchCase="'Success'" id="Success" class="container-fluid">
+ <h2>Successfully submitted data.</h2>
</div>
</ng-container>
diff --git a/scouting/www/index.html b/scouting/www/index.html
index 208141a..afc589e 100644
--- a/scouting/www/index.html
+++ b/scouting/www/index.html
@@ -5,16 +5,16 @@
<title>FRC971 Scouting Application</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
- href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
rel="stylesheet"
- integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3"
+ href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css"
+ integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD"
crossorigin="anonymous"
/>
<link
rel="stylesheet"
- href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.2/font/bootstrap-icons.css"
+ href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.3/font/bootstrap-icons.c"
/>
- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
</head>
<body>
<my-app></my-app>
diff --git a/tools/build_rules/js.bzl b/tools/build_rules/js.bzl
index f66d9c4..ceb67aa 100644
--- a/tools/build_rules/js.bzl
+++ b/tools/build_rules/js.bzl
@@ -12,6 +12,7 @@
load("//tools/build_rules/js:ts.bzl", _ts_project = "ts_project")
load("@aspect_rules_rollup//rollup:defs.bzl", upstream_rollup_bundle = "rollup_bundle")
load("@aspect_rules_terser//terser:defs.bzl", "terser_minified")
+load("@aspect_rules_cypress//cypress:defs.bzl", "cypress_module_test")
ts_project = _ts_project
@@ -359,3 +360,42 @@
"suffix": attr.string(mandatory = True),
},
)
+
+def cypress_test(runner, data = None, **kwargs):
+ """Runs a cypress test with the specified runner.
+
+ Args:
+ runner: The runner that starts up any necessary servers and then
+ invokes Cypress itself. See the Module API documentation for more
+ information: https://docs.cypress.io/guides/guides/module-api
+ data: The spec files (*.cy.js) and the servers under test. Also any
+ other files needed at runtime.
+ kwargs: Arguments forwarded to the upstream cypress_module_test().
+ """
+
+ # Figure out how many directories deep this package is relative to the
+ # workspace root.
+ package_depth = len(native.package_name().split("/"))
+
+ # Chrome is located at the runfiles root. So we need to go up one more
+ # directory than the workspace root.
+ chrome_location = "../" * (package_depth + 1) + "chrome_linux/chrome"
+ config_location = "../" * package_depth + "tools/build_rules/js/cypress.config.js"
+
+ data = data or []
+ data.append("//tools/build_rules/js:cypress.config.js")
+ data.append("@xvfb_amd64//:wrapped_bin/Xvfb")
+
+ cypress_module_test(
+ args = [
+ "run",
+ "--config-file=" + config_location,
+ "--browser=" + chrome_location,
+ ],
+ browsers = ["@chrome_linux//:all"],
+ copy_data_to_bin = False,
+ cypress = "//:node_modules/cypress",
+ data = data,
+ runner = runner,
+ **kwargs
+ )
diff --git a/tools/build_rules/js/BUILD b/tools/build_rules/js/BUILD
index 2381fcb..1bbe769 100644
--- a/tools/build_rules/js/BUILD
+++ b/tools/build_rules/js/BUILD
@@ -1,6 +1,10 @@
load("@npm//:@angular/compiler-cli/package_json.bzl", angular_compiler_cli = "bin")
load(":ts.bzl", "ts_project")
+exports_files([
+ "cypress.config.js",
+])
+
# Define the @angular/compiler-cli ngc bin binary as a target
angular_compiler_cli.ngc_binary(
name = "ngc",
diff --git a/scouting/cypress.config.js b/tools/build_rules/js/cypress.config.js
similarity index 80%
rename from scouting/cypress.config.js
rename to tools/build_rules/js/cypress.config.js
index 4eb1a82..c8d6988 100644
--- a/scouting/cypress.config.js
+++ b/tools/build_rules/js/cypress.config.js
@@ -7,6 +7,9 @@
setupNodeEvents(on, config) {
on('before:browser:launch', (browser = {}, launchOptions) => {
launchOptions.args.push('--disable-gpu-shader-disk-cache');
+ launchOptions.args.push('--enable-logging');
+ launchOptions.args.push('--v=stderr');
+ return launchOptions;
});
// Lets users print to the console:
diff --git a/tools/js/README.md b/tools/js/README.md
new file mode 100644
index 0000000..68f2264
--- /dev/null
+++ b/tools/js/README.md
@@ -0,0 +1,28 @@
+Javascript-related Information
+================================================================================
+
+Debugging Cypress tests
+--------------------------------------------------------------------------------
+You can run Cypress tests interactively. I.e. the browser will open up and you
+can interact with it. Use the `tools/js/run_cypress_test_interactively.sh`
+script for this.
+
+```console
+$ ./tools/js/run_cypress_test_interactively.sh //path/to:test
+(opens Chrome and runs tests)
+```
+
+All arguments to the script are passed to `bazel test`. So you can specify `-c
+opt` and other bazel options.
+
+### Pausing execution
+Use [`cy.pause()`](https://docs.cypress.io/api/commands/pause) to pause
+execution. Resume by hitting the green "Play" icon near the top left.
+
+Pausing can be helpful when you're trying to understand more about the state of
+the web page in the middle of a test.
+
+### Getting `console.log()` output
+Add `--test_env=DEBUG=cypress:launcher:browsers` to your test execution.
+Cypress will then log Chrome's output. Among which will be all `console.log()`
+statements.
diff --git a/tools/js/run_cypress_test_interactively.sh b/tools/js/run_cypress_test_interactively.sh
new file mode 100755
index 0000000..8869cc0
--- /dev/null
+++ b/tools/js/run_cypress_test_interactively.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+
+set -o errexit
+set -o nounset
+set -o pipefail
+
+bazel test \
+ --test_env=DISPLAY="${DISPLAY}" \
+ --strategy=TestRunner=processwrapper-sandbox \
+ --test_output=streamed \
+ --test_arg=--headed \
+ --test_timeout=9999 \
+ "$@"
diff --git a/y2023/BUILD b/y2023/BUILD
index 309ada4..bdeaea9 100644
--- a/y2023/BUILD
+++ b/y2023/BUILD
@@ -6,11 +6,13 @@
binaries = [
"//aos/network:web_proxy_main",
"//aos/events/logging:log_cat",
+ "//y2023/constants:constants_sender",
"//aos/events:aos_timing_report_streamer",
],
data = [
":aos_config",
"//aos/starter:roborio_irq_config.json",
+ "//y2023/constants:constants.json",
"@ctre_phoenix_api_cpp_athena//:shared_libraries",
"@ctre_phoenix_cci_athena//:shared_libraries",
"@ctre_phoenixpro_api_cpp_athena//:shared_libraries",
diff --git a/y2023/constants.cc b/y2023/constants.cc
index b07240f..8908811 100644
--- a/y2023/constants.cc
+++ b/y2023/constants.cc
@@ -102,22 +102,26 @@
0.11972765117321;
wrist->subsystem_params.zeroing_constants.measured_absolute_position =
- 0.10640608532802;
+ 0.420104471500763;
break;
case kPracticeTeamNumber:
- arm_proximal->zeroing.measured_absolute_position = 0.0;
- arm_proximal->potentiometer_offset = 0.0;
+ arm_proximal->zeroing.measured_absolute_position = 0.254437958024658;
+ arm_proximal->potentiometer_offset =
+ 10.5178592988554 + 0.0944609125285876;
- arm_distal->zeroing.measured_absolute_position = 0.0;
- arm_distal->potentiometer_offset = 0.0;
+ arm_distal->zeroing.measured_absolute_position = 0.51986178669514;
+ arm_distal->potentiometer_offset = 7.673132586937 - 0.0799284644472573 -
+ 0.0323574039310657 +
+ 0.0143810684138064;
- roll_joint->zeroing.measured_absolute_position = 0.0;
- roll_joint->potentiometer_offset = 0.0;
+ roll_joint->zeroing.measured_absolute_position = 1.86685853969852;
+ roll_joint->potentiometer_offset =
+ 0.624713611895747 + 3.10458504917251 - 0.0966407797407789;
wrist->subsystem_params.zeroing_constants.measured_absolute_position =
- 0.0;
+ -0.607792293122026;
break;
diff --git a/y2023/control_loops/python/graph_codegen.py b/y2023/control_loops/python/graph_codegen.py
index 054a32d..6f3bc0d 100644
--- a/y2023/control_loops/python/graph_codegen.py
+++ b/y2023/control_loops/python/graph_codegen.py
@@ -46,13 +46,14 @@
start_index = None
end_index = None
- for point, name in graph_paths.points:
+ for key in sorted(graph_paths.points.keys()):
+ point = graph_paths.points[key]
if (point[:2] == segment.start
).all() and point[2] == segment.alpha_rolls[0][1]:
- start_index = name
+ start_index = key
if (point[:2] == segment.end
).all() and point[2] == segment.alpha_rolls[-1][1]:
- end_index = name
+ end_index = key
if reverse:
start_index, end_index = end_index, start_index
@@ -146,16 +147,17 @@
h_file.append("")
# Now dump out the vertices and associated constexpr vertex name functions.
- for index, point in enumerate(graph_paths.points):
+ for index, key in enumerate(sorted(graph_paths.points.keys())):
+ point = graph_paths.points[key]
h_file.append("")
h_file.append("constexpr uint32_t %s() { return %d; }" %
- (index_function_name(point[1]), index))
- h_file.append("inline ::Eigen::Matrix<double, 3, 1> %sPoint() {" %
- point[1])
- h_file.append(
+ (index_function_name(key), index))
+ h_file.append("::Eigen::Matrix<double, 3, 1> %sPoint();" % key)
+ cc_file.append("::Eigen::Matrix<double, 3, 1> %sPoint() {" % key)
+ cc_file.append(
" return (::Eigen::Matrix<double, 3, 1>() << %f, %f, %f).finished();"
- % (point[0][0], point[0][1], point[0][2]))
- h_file.append("}")
+ % (point[0], point[1], point[2]))
+ cc_file.append("}")
front_points = [
index_function_name(point[1]) + "()"
@@ -214,10 +216,11 @@
cc_file.append(
"::std::vector<::Eigen::Matrix<double, 3, 1>> PointList() {")
cc_file.append(" ::std::vector<::Eigen::Matrix<double, 3, 1>> points;")
- for point in graph_paths.points:
+ for key in sorted(graph_paths.points.keys()):
+ point = graph_paths.points[key]
cc_file.append(
" points.push_back((::Eigen::Matrix<double, 3, 1>() << %.12s, %.12s, %.12s).finished());"
- % (point[0][0], point[0][1], point[0][2]))
+ % (point[0], point[1], point[2]))
cc_file.append(" return points;")
cc_file.append("}")
diff --git a/y2023/control_loops/python/graph_edit.py b/y2023/control_loops/python/graph_edit.py
index d841509..a976807 100644
--- a/y2023/control_loops/python/graph_edit.py
+++ b/y2023/control_loops/python/graph_edit.py
@@ -217,7 +217,7 @@
self.theta_version = False
self.reinit_extents()
- self.last_pos = to_xy(*graph_paths.neutral[:2])
+ self.last_pos = to_xy(*graph_paths.points['Neutral'][:2])
self.circular_index_select = 1
# Extra stuff for drawing lines.
@@ -494,11 +494,13 @@
else:
self.index = len(self.segments) - 1
print("Switched to segment:", self.segments[self.index].name)
+ self.segments[self.index].Print(graph_paths.points)
elif keyval == Gdk.KEY_n:
self.index += 1
self.index = self.index % len(self.segments)
print("Switched to segment:", self.segments[self.index].name)
+ self.segments[self.index].Print(graph_paths.points)
elif keyval == Gdk.KEY_t:
# Toggle between theta and xy renderings
@@ -546,18 +548,15 @@
else:
self.segments[self.index].control2 = self.now_segment_pt
- print('Clicked at theta: %s' % (repr(self.now_segment_pt, )))
+ print('Clicked at theta: np.array([%s, %s])' %
+ (self.now_segment_pt[0], self.now_segment_pt[1]))
if not self.theta_version:
- print('Clicked at xy, circular index: (%f, %f, %f)' %
- (self.last_pos[0], self.last_pos[1],
+ print(
+ 'Clicked at to_theta_with_circular_index(%.3f, %.3f, circular_index=%d)'
+ % (self.last_pos[0], self.last_pos[1],
self.circular_index_select))
- print('c1: np.array([%f, %f])' %
- (self.segments[self.index].control1[0],
- self.segments[self.index].control1[1]))
- print('c2: np.array([%f, %f])' %
- (self.segments[self.index].control2[0],
- self.segments[self.index].control2[1]))
+ self.segments[self.index].Print(graph_paths.points)
self.redraw()
@@ -565,4 +564,5 @@
arm_ui = ArmUi()
arm_ui.segments = graph_paths.segments
print('Starting with segment: ', arm_ui.segments[arm_ui.index].name)
+arm_ui.segments[arm_ui.index].Print(graph_paths.points)
basic_window.RunApp()
diff --git a/y2023/control_loops/python/graph_paths.py b/y2023/control_loops/python/graph_paths.py
index 66ca158..cde885c 100644
--- a/y2023/control_loops/python/graph_paths.py
+++ b/y2023/control_loops/python/graph_paths.py
@@ -2,186 +2,224 @@
from y2023.control_loops.python.graph_tools import *
-neutral = np.array((np.pi, 0.0, 0.0))
+named_segments = []
+points = {}
-# NeutralToGroundPickupBackConeUp
-neutral_to_cone_up_1 = np.array([3.170156, -0.561227])
-neutral_to_cone_up_2 = np.array([2.972776, -1.026820])
-ground_pickup_back_cone_up = to_theta_with_circular_index_and_roll(
+points['Neutral'] = np.array((np.pi, 0.0, 0.0))
+
+points['GroundPickupBackConeUp'] = to_theta_with_circular_index_and_roll(
-1.07774334, 0.36308701, np.pi / 2.0, circular_index=1)
-# NeutralToGroundPickupBackConeDown
-neutral_to_ground_pickup_back_cone_down_1 = np.array([3.170156, -0.561227])
-neutral_to_ground_pickup_back_cone_down_2 = np.array([2.972776, -1.026820])
-ground_pickup_back_cone_down = to_theta_with_circular_index_and_roll(
+named_segments.append(
+ ThetaSplineSegment(
+ name="NeutralToGroundPickupBackConeUp",
+ start=points['Neutral'],
+ control1=np.array([3.170156, -0.561227]),
+ control2=np.array([2.972776, -1.026820]),
+ end=points['GroundPickupBackConeUp'],
+ control_alpha_rolls=[(0.30, 0.0), (.95, np.pi / 2.0)],
+ ))
+
+points['GroundPickupBackConeDown'] = to_theta_with_circular_index_and_roll(
-1.11487594, 0.23140145, np.pi / 2.0, circular_index=1)
-# NeutralToGroundPickupBackCube
-neutral_to_ground_pickup_back_cube_1 = np.array([3.153228, -0.497009])
-neutral_to_ground_pickup_back_cube_2 = np.array([2.972776, -1.026820])
-neutral_to_hp_pickup_back_cube_alpha_rolls = [
- (0.7, 0.0),
- (.9, -np.pi / 2.0),
-]
-ground_pickup_back_cube = to_theta_with_circular_index_and_roll(
- -1.102, 0.224, -np.pi / 2.0, circular_index=1)
+named_segments.append(
+ ThetaSplineSegment(
+ name="NeutralToGroundPickupBackConeDown",
+ start=points['Neutral'],
+ control1=np.array([3.170156, -0.561227]),
+ control2=np.array([2.972776, -1.026820]),
+ end=points['GroundPickupBackConeDown'],
+ control_alpha_rolls=[(0.30, 0.0), (.95, np.pi / 2.0)],
+ ))
-# NeutralToBackMidConeUpScore
-neutral_to_score_back_mid_cone_up_1 = np.array([0.994244, -1.417442])
-neutral_to_score_back_mid_cone_up_2 = np.array([1.711325, -0.679748])
-score_back_mid_cone_up_pos = to_theta_with_circular_index_and_roll(
+points['GroundPickupBackCube'] = to_theta_with_circular_index_and_roll(
+ -1.102, 0.25, -np.pi / 2.0, circular_index=1)
+
+named_segments.append(
+ ThetaSplineSegment(
+ name="NeutralToGroundPickupBackCube",
+ start=points['Neutral'],
+ control1=np.array([3.153228, -0.497009]),
+ control2=np.array([2.972776, -1.026820]),
+ end=points['GroundPickupBackCube'],
+ control_alpha_rolls=[(0.7, 0.0), (.9, -np.pi / 2.0)],
+ ))
+
+points['GroundPickupFrontCube'] = to_theta_with_circular_index_and_roll(
+ 0.325603, 0.255189, np.pi / 2.0, circular_index=0)
+
+named_segments.append(
+ ThetaSplineSegment(
+ name="NeutralToGroundPickupFrontCube",
+ start=points['Neutral'],
+ control1=np.array([3.338852196583635, 0.34968650009090885]),
+ control2=np.array([4.28246270189025, 1.492916470137478]),
+ end=points['GroundPickupFrontCube'],
+ control_alpha_rolls=[(0.4, 0.0), (.9, np.pi / 2.0)],
+ ))
+
+points['ScoreBackMidConeUpPos'] = to_theta_with_circular_index_and_roll(
-1.41871454, 1.07476162, np.pi / 2.0, circular_index=0)
-# NeutralToMidConeDownScore
-neutral_to_score_mid_cone_down_1 = np.array([3.394572, -0.239378])
-neutral_to_score_mid_cone_down_2 = np.array([3.654854, -0.626835])
-score_mid_cone_down_pos = to_theta_with_circular_index_and_roll(
+named_segments.append(
+ ThetaSplineSegment(
+ name="NeutralToBackMidConeUpScore",
+ start=points['Neutral'],
+ control1=np.array([0.994244, -1.417442]),
+ control2=np.array([1.711325, -0.679748]),
+ end=points['ScoreBackMidConeUpPos'],
+ control_alpha_rolls=[(0.40, 0.0), (.95, np.pi / 2.0)],
+ ))
+
+points['ScoreBackMidConeDownPos'] = to_theta_with_circular_index_and_roll(
-1.37792406, 0.81332449, np.pi / 2.0, circular_index=1)
-# NeutralToMidConeDownScore
-neutral_to_hp_pickup_back_cone_up_1 = np.array([2.0, -0.239378])
-neutral_to_hp_pickup_back_cone_up_2 = np.array([1.6, -0.626835])
-neutral_to_hp_pickup_back_cone_up_alpha_rolls = [
- (0.7, 0.0),
- (.9, np.pi / 2.0),
-]
-hp_pickup_back_cone_up = to_theta_with_circular_index_and_roll(
+named_segments.append(
+ ThetaSplineSegment(
+ name="NeutralToMidConeDownScore",
+ start=points['Neutral'],
+ control1=np.array([3.394572, -0.239378]),
+ control2=np.array([3.654854, -0.626835]),
+ end=points['ScoreBackMidConeDownPos'],
+ control_alpha_rolls=[(0.40, 0.0), (.95, np.pi / 2.0)],
+ ))
+
+points['HPPickupBackConeUp'] = to_theta_with_circular_index_and_roll(
-1.1050539, 1.31390128, np.pi / 2.0, circular_index=0)
-# NeutralToFrontHighConeUpScore
-neutral_to_score_front_high_cone_up_1 = np.array([2.594244, 0.417442])
-neutral_to_score_front_high_cone_up_2 = np.array([1.51325, 0.679748])
-score_front_high_cone_up_pos = to_theta_with_circular_index_and_roll(
+named_segments.append(
+ ThetaSplineSegment(
+ name="NeutralToHPPickupBackConeUp",
+ start=points['Neutral'],
+ control1=np.array([2.0, -0.239378]),
+ control2=np.array([1.6, -0.626835]),
+ end=points['HPPickupBackConeUp'],
+ control_alpha_rolls=[(0.7, 0.0), (.9, np.pi / 2.0)],
+ ))
+
+points['ScoreFrontHighConeUpPos'] = to_theta_with_circular_index_and_roll(
0.98810344, 1.37536719, -np.pi / 2.0, circular_index=0)
-# NeutralToFrontMidConeUpScore
-neutral_to_score_front_mid_cone_up_1 = np.array([3.0, 0.317442])
-neutral_to_score_front_mid_cone_up_2 = np.array([2.9, 0.479748])
-score_front_mid_cone_up_pos = to_theta_with_circular_index_and_roll(
+named_segments.append(
+ ThetaSplineSegment(
+ name="NeutralToFrontHighConeUpScore",
+ start=points['Neutral'],
+ control1=np.array([2.594244, 0.417442]),
+ control2=np.array([1.51325, 0.679748]),
+ end=points['ScoreFrontHighConeUpPos'],
+ control_alpha_rolls=[(0.40, 0.0), (.95, -np.pi / 2.0)],
+ ))
+
+points['ScoreFrontMidConeUpPos'] = to_theta_with_circular_index_and_roll(
0.43740453, 1.06330555, -np.pi / 2.0, circular_index=0)
-neutral_to_cone_down_1 = np.array([2.396694, 0.508020])
-neutral_to_cone_down_2 = np.array([2.874513, 0.933160])
-cone_down_pos = to_theta_with_circular_index_and_roll(0.7,
- 0.11,
- np.pi / 2.0,
- circular_index=0)
+named_segments.append(
+ ThetaSplineSegment(
+ name="NeutralToFrontMidConeUpScore",
+ start=points['Neutral'],
+ control1=np.array([3.0, 0.317442]),
+ control2=np.array([2.9, 0.479748]),
+ end=points['ScoreFrontMidConeUpPos'],
+ control_alpha_rolls=[(0.40, 0.0), (.95, -np.pi / 2.0)],
+ ))
-neutral_to_cube_1 = np.array([2.396694, 0.508020])
-neutral_to_cube_2 = np.array([2.874513, 0.933160])
+points['ScoreFrontLowCube'] = to_theta_with_circular_index_and_roll(
+ 0.325603, 0.30, np.pi / 2.0, circular_index=0)
-cube_pos = to_theta_with_circular_index_and_roll(0.7,
- 0.24,
- np.pi / 2.0,
- circular_index=0)
+named_segments.append(
+ ThetaSplineSegment(
+ name="NeutralToScoreFrontLowCube",
+ start=points['Neutral'],
+ control1=np.array([3.338852196583635, 0.34968650009090885]),
+ control2=np.array([4.28246270189025, 1.492916470137478]),
+ end=points['ScoreFrontLowCube'],
+ control_alpha_rolls=[(0.4, 0.0), (.9, np.pi / 2.0)],
+ ))
-neutral_to_pickup_control_alpha_rolls = [
- (0.30, 0.0),
- (.95, np.pi / 2.0),
-]
+points['ScoreBackLowCube'] = to_theta_with_circular_index_and_roll(
+ -1.102, 0.30, -np.pi / 2.0, circular_index=1)
-neutral_to_score_1 = np.array([0.994244, -1.417442])
-neutral_to_score_2 = np.array([1.711325, -0.679748])
+named_segments.append(
+ ThetaSplineSegment(
+ name="NeutralToScoreLowBackCube",
+ start=points['Neutral'],
+ control1=np.array([3.153228, -0.497009]),
+ control2=np.array([2.972776, -1.026820]),
+ end=points['ScoreBackLowCube'],
+ control_alpha_rolls=[(0.7, 0.0), (.9, -np.pi / 2.0)],
+ ))
-score_low_pos = to_theta_with_circular_index_and_roll(-(0.41 / 2 + 0.49),
- 0 + 0.05,
- np.pi / 2.0,
- circular_index=1)
+points['ScoreFrontMidCube'] = to_theta_with_circular_index_and_roll(
+ 0.517846, 0.87, np.pi / 2.0, circular_index=0)
-neutral_to_score_low_2 = np.array([3.37926599, -0.73664663])
+named_segments.append(
+ ThetaSplineSegment(
+ name="NeutralToScoreFrontMidCube",
+ start=points["Neutral"],
+ control1=np.array([3.1310824883477952, 0.23591705727105095]),
+ control2=np.array([3.0320025094685965, 0.43674789928668933]),
+ end=points["ScoreFrontMidCube"],
+ control_alpha_rolls=[(0.4, np.pi * 0.0), (0.95, np.pi * 0.5)],
+ ))
-score_mid_cube_pos = to_theta_with_circular_index_and_roll(-(0.58 + 0.49),
- 0.6 + 0.05,
- np.pi / 2.0,
- circular_index=0)
+points['ScoreFrontHighCube'] = to_theta_with_circular_index_and_roll(
+ 0.901437, 1.16, np.pi / 2.0, circular_index=0)
-score_high_cone_pos = to_theta_with_circular_index_and_roll(-1.01,
- 1.17 + 0.05,
- np.pi / 2.0,
- circular_index=0)
+named_segments.append(
+ ThetaSplineSegment(
+ name="NeutralToScoreFrontHighCube",
+ start=points["Neutral"],
+ control1=np.array([2.537484161662287, 0.059700523547219]),
+ control2=np.array([2.449391812539668, 0.4141564369176016]),
+ end=points["ScoreFrontHighCube"],
+ control_alpha_rolls=[(0.4, np.pi * 0.0), (0.95, np.pi * 0.5)],
+ ))
-score_high_cube_pos = to_theta_with_circular_index_and_roll(-1.01,
- 0.90 + 0.05,
- np.pi / 2.0,
- circular_index=0)
+points['ScoreBackMidCube'] = to_theta_with_circular_index_and_roll(
+ -1.27896, 0.84, -np.pi / 2.0, circular_index=1)
-neutral_to_back_score_control_alpha_rolls = [(0.40, 0.0), (.95, np.pi / 2.0)]
-neutral_to_front_score_control_alpha_rolls = [(0.40, 0.0), (.95, -np.pi / 2.0)]
+named_segments.append(
+ ThetaSplineSegment(
+ name="NeutralToScoreBackMidCube",
+ start=points["Neutral"],
+ control1=np.array([3.3485646154655404, -0.4369603013926491]),
+ control2=np.array([3.2653593368256995, -0.789587049476034]),
+ end=points["ScoreBackMidCube"],
+ control_alpha_rolls=[(0.3, -np.pi * 0.0), (0.95, -np.pi * 0.5)],
+ ))
-points = [(neutral, "NeutralPos"),
- (ground_pickup_back_cone_up, "GroundPickupBackConeUp"),
- (ground_pickup_back_cone_down, "GroundPickupBackConeDown"),
- (ground_pickup_back_cube, "GroundPickupBackCube"),
- (hp_pickup_back_cone_up, "HPPickupBackConeUp"),
- (cone_down_pos, "ConeDownPos"), (score_low_pos, "ScoreLowPos"),
- (score_back_mid_cone_up_pos, "ScoreBackMidConeUpPos"),
- (score_front_high_cone_up_pos, "ScoreFrontHighConeUpPos"),
- (score_front_mid_cone_up_pos, "ScoreFrontMidConeUpPos"),
- (score_mid_cone_down_pos, "ScoreBackMidConeDownPos"),
- (score_mid_cube_pos, "ScoreMidCubePos"),
- (score_high_cone_pos, "ScoreHighConePos"),
- (score_high_cube_pos, "ScoreHighCubePos"), (cube_pos, "CubePos")]
+# TODO(austin): This doesn't produce the next line...
+#points['ScoreBackHighCube'] = to_theta_with_circular_index_and_roll(
+# -1.60932, 1.16839, np.pi / 2.0, circular_index=0)
+points['ScoreBackHighCube'] = np.array(
+ (4.77284735761704, -1.19952193130714, -np.pi / 2.0))
+
+named_segments.append(
+ ThetaSplineSegment(
+ name="NeutralToScoreBackHighCube",
+ start=points["Neutral"],
+ control1=np.array([3.6804854484103684, -0.3494541095053125]),
+ control2=np.array([3.9889380578509517, -0.6637934755748516]),
+ end=points["ScoreBackHighCube"],
+ control_alpha_rolls=[(0.3, -np.pi * 0.0), (0.95, -np.pi * 0.5)],
+ ))
+
+points['ConeDownPos'] = to_theta_with_circular_index_and_roll(0.7,
+ 0.11,
+ np.pi / 2.0,
+ circular_index=0)
+named_segments.append(
+ ThetaSplineSegment(
+ name="NeutralToConeDown",
+ start=points['Neutral'],
+ control1=np.array([2.396694, 0.508020]),
+ control2=np.array([2.874513, 0.933160]),
+ end=points['ConeDownPos'],
+ control_alpha_rolls=[(0.30, 0.0), (.95, np.pi / 2.0)],
+ ))
+
front_points = []
back_points = []
unnamed_segments = []
-named_segments = [
- ThetaSplineSegment("NeutralToGroundPickupBackConeUp", neutral,
- neutral_to_cone_up_1, neutral_to_cone_up_2,
- ground_pickup_back_cone_up,
- neutral_to_pickup_control_alpha_rolls),
- ThetaSplineSegment("NeutralToGroundPickupBackConeDown", neutral,
- neutral_to_ground_pickup_back_cone_down_1,
- neutral_to_ground_pickup_back_cone_down_2,
- ground_pickup_back_cone_down,
- neutral_to_pickup_control_alpha_rolls),
- ThetaSplineSegment("NeutralToGroundPickupBackCube", neutral,
- neutral_to_ground_pickup_back_cube_1,
- neutral_to_ground_pickup_back_cube_2,
- ground_pickup_back_cube,
- neutral_to_hp_pickup_back_cube_alpha_rolls),
- ThetaSplineSegment("NeutralToHPPickupBackConeUp", neutral,
- neutral_to_hp_pickup_back_cone_up_1,
- neutral_to_hp_pickup_back_cone_up_2,
- hp_pickup_back_cone_up,
- neutral_to_hp_pickup_back_cone_up_alpha_rolls),
- ThetaSplineSegment("NeutralToFrontHighConeUpScore", neutral,
- neutral_to_score_front_high_cone_up_1,
- neutral_to_score_front_high_cone_up_2,
- score_front_high_cone_up_pos,
- neutral_to_front_score_control_alpha_rolls),
- ThetaSplineSegment("NeutralToFrontMidConeUpScore", neutral,
- neutral_to_score_front_mid_cone_up_1,
- neutral_to_score_front_mid_cone_up_2,
- score_front_mid_cone_up_pos,
- neutral_to_front_score_control_alpha_rolls),
- ThetaSplineSegment("NeutralToConeDown", neutral, neutral_to_cone_down_1,
- neutral_to_cone_down_2, cone_down_pos,
- neutral_to_pickup_control_alpha_rolls),
- ThetaSplineSegment("NeutralToCube", neutral, neutral_to_cube_1,
- neutral_to_cube_2, cube_pos,
- neutral_to_pickup_control_alpha_rolls),
- ThetaSplineSegment("NeutralToLowScore", neutral, neutral_to_score_1,
- neutral_to_score_low_2, score_low_pos,
- neutral_to_back_score_control_alpha_rolls),
- ThetaSplineSegment("NeutralToBackMidConeUpScore", neutral,
- neutral_to_score_back_mid_cone_up_1,
- neutral_to_score_back_mid_cone_up_2,
- score_back_mid_cone_up_pos,
- neutral_to_back_score_control_alpha_rolls),
- ThetaSplineSegment("NeutralToMidConeDownScore", neutral,
- neutral_to_score_mid_cone_down_1,
- neutral_to_score_mid_cone_down_2,
- score_mid_cone_down_pos,
- neutral_to_back_score_control_alpha_rolls),
- ThetaSplineSegment("NeutralToMidCubeScore", neutral, neutral_to_score_1,
- neutral_to_score_2, score_mid_cube_pos,
- neutral_to_back_score_control_alpha_rolls),
- ThetaSplineSegment("NeutralToHighConeScore", neutral, neutral_to_score_1,
- neutral_to_score_2, score_high_cone_pos,
- neutral_to_back_score_control_alpha_rolls),
- ThetaSplineSegment("NeutralToHighCubeScore", neutral, neutral_to_score_1,
- neutral_to_score_2, score_high_cube_pos,
- neutral_to_back_score_control_alpha_rolls),
-]
-
segments = named_segments + unnamed_segments
diff --git a/y2023/control_loops/python/graph_tools.py b/y2023/control_loops/python/graph_tools.py
index 8751935..ac926aa 100644
--- a/y2023/control_loops/python/graph_tools.py
+++ b/y2023/control_loops/python/graph_tools.py
@@ -180,11 +180,11 @@
# The limit for the proximal and distal is relative,
# so define constraints for this delta.
UPPER_DELTA_LIMIT = 0.0
-LOWER_DELTA_LIMIT = -1.9 * np.pi
+LOWER_DELTA_LIMIT = -1.98 * np.pi
# TODO(milind): put actual proximal limits
-UPPER_PROXIMAL_LIMIT = np.pi * 1.5
-LOWER_PROXIMAL_LIMIT = -np.pi
+UPPER_PROXIMAL_LIMIT = np.pi * 2.0
+LOWER_PROXIMAL_LIMIT = -np.pi * 2.0
UPPER_DISTAL_LIMIT = 0.75 * np.pi
LOWER_DISTAL_LIMIT = -0.75 * np.pi
@@ -202,7 +202,8 @@
def get_circular_index(theta):
- return int(np.floor((theta[1] - theta[0]) / np.pi))
+ return int(
+ np.floor((shift_angle(theta[1]) - shift_angle(theta[0])) / np.pi))
def get_xy(theta):
@@ -447,6 +448,52 @@
self.alpha_unitizer = alpha_unitizer
self.vmax = vmax
+ def Print(self, points):
+ # Find the name of the start end end points.
+ start_name = None
+ end_name = None
+ for name in points:
+ point = points[name]
+ if (self.start == point[:2]).all():
+ start_name = name
+ elif (self.end == point[:2]).all():
+ end_name = name
+
+ alpha_points = '[' + ', '.join([
+ f"({alpha}, np.pi * {theta / np.pi})"
+ for alpha, theta in self.alpha_rolls[1:-1]
+ ]) + ']'
+
+ def FormatToTheta(point):
+ x, y = get_xy(point)
+ circular_index = get_circular_index(point)
+ return "to_theta_with_circular_index(%.3f, %.3f, circular_index=%d)" % (
+ x, y, circular_index)
+
+ def FormatToThetaRoll(point, roll):
+ x, y = get_xy(point)
+ circular_index = get_circular_index(point)
+ return "to_theta_with_circular_index_and_roll(%.3f, %.3f, np.pi * %.2f, circular_index=%d)" % (
+ x, y, roll / np.pi, circular_index)
+
+ print('named_segments.append(')
+ print(' ThetaSplineSegment(')
+ print(f' name="{self.name}",')
+ print(
+ f' start=points["{start_name}"], # {FormatToThetaRoll(self.start, self.alpha_rolls[0][1])}'
+ )
+ print(
+ f' control1=np.array([{self.control1[0]}, {self.control1[1]}]), # {FormatToTheta(self.control1)}'
+ )
+ print(
+ f' control2=np.array([{self.control2[0]}, {self.control2[1]}]), # {FormatToTheta(self.control2)}'
+ )
+ print(
+ f' end=points["{end_name}"], # {FormatToThetaRoll(self.end, self.alpha_rolls[-1][1])}'
+ )
+ print(f' control_alpha_rolls={alpha_points},')
+ print(f'))')
+
def __repr__(self):
return "ThetaSplineSegment(%s, %s, %s, %s)" % (repr(
self.start), repr(self.control1), repr(
diff --git a/y2023/control_loops/superstructure/arm/arm.cc b/y2023/control_loops/superstructure/arm/arm.cc
index fd265c4..03a1b21 100644
--- a/y2023/control_loops/superstructure/arm/arm.cc
+++ b/y2023/control_loops/superstructure/arm/arm.cc
@@ -38,7 +38,7 @@
search_graph_(MakeSearchGraph(&dynamics_, &trajectories_, alpha_unitizer_,
kVMax(), &hybrid_roll_joint_loop_)),
// Go to the start of the first trajectory.
- follower_(&dynamics_, &hybrid_roll_joint_loop_, NeutralPosPoint()),
+ follower_(&dynamics_, &hybrid_roll_joint_loop_, NeutralPoint()),
points_(PointList()),
current_node_(0) {
int i = 0;
@@ -92,7 +92,7 @@
}
// TODO(milind): should we default to the closest position?
- uint32_t filtered_goal = arm::NeutralPosIndex();
+ uint32_t filtered_goal = arm::NeutralIndex();
if (unsafe_goal != nullptr) {
filtered_goal = *unsafe_goal;
}
diff --git a/y2023/control_loops/superstructure/end_effector.cc b/y2023/control_loops/superstructure/end_effector.cc
index 874cfad..287f0e7 100644
--- a/y2023/control_loops/superstructure/end_effector.cc
+++ b/y2023/control_loops/superstructure/end_effector.cc
@@ -35,9 +35,6 @@
game_piece_ = GamePiece::CUBE;
}
- // Cube voltage is flipped
- double voltage_sign = (game_piece_ == GamePiece::CUBE ? -1.0 : 1.0);
-
// Go into spitting if we were told to, no matter where we are
if (roller_goal == RollerGoal::SPIT && state_ != EndEffectorState::SPITTING) {
state_ = EndEffectorState::SPITTING;
@@ -59,6 +56,12 @@
break;
case EndEffectorState::INTAKING:
// If intaking and beam break is not triggered, keep intaking
+ if (roller_goal == RollerGoal::INTAKE_CONE ||
+ roller_goal == RollerGoal::INTAKE_CUBE ||
+ roller_goal == RollerGoal::INTAKE_LAST) {
+ timer_ = timestamp;
+ }
+
if (beambreak_status) {
// Beam has been broken, switch to loaded.
state_ = EndEffectorState::LOADED;
@@ -77,15 +80,19 @@
break;
case EndEffectorState::LOADED:
+ timer_ = timestamp;
// If loaded and beam break not triggered, intake
if (!beambreak_status) {
state_ = EndEffectorState::INTAKING;
- timer_ = timestamp;
}
break;
case EndEffectorState::SPITTING:
// If spit requested, spit
- *roller_voltage = voltage_sign * kRollerSpitVoltage();
+ if (game_piece_ == GamePiece::CUBE) {
+ *roller_voltage = kRollerCubeSpitVoltage();
+ } else {
+ *roller_voltage = kRollerConeSpitVoltage();
+ }
if (beambreak_) {
if (!beambreak_status) {
timer_ = timestamp;
diff --git a/y2023/control_loops/superstructure/end_effector.h b/y2023/control_loops/superstructure/end_effector.h
index d93d868..14245c8 100644
--- a/y2023/control_loops/superstructure/end_effector.h
+++ b/y2023/control_loops/superstructure/end_effector.h
@@ -15,8 +15,10 @@
class EndEffector {
public:
static constexpr double kRollerConeSuckVoltage() { return 12.0; }
+ static constexpr double kRollerConeSpitVoltage() { return -9.0; }
+
static constexpr double kRollerCubeSuckVoltage() { return -5.0; }
- static constexpr double kRollerSpitVoltage() { return -9.0; }
+ static constexpr double kRollerCubeSpitVoltage() { return 3.0; }
EndEffector();
void RunIteration(const ::aos::monotonic_clock::time_point timestamp,
diff --git a/y2023/control_loops/superstructure/superstructure_lib_test.cc b/y2023/control_loops/superstructure/superstructure_lib_test.cc
index 0ecc5cf..220f3d8 100644
--- a/y2023/control_loops/superstructure/superstructure_lib_test.cc
+++ b/y2023/control_loops/superstructure/superstructure_lib_test.cc
@@ -192,7 +192,7 @@
event_loop_->MakeFetcher<Status>("/superstructure")),
superstructure_output_fetcher_(
event_loop_->MakeFetcher<Output>("/superstructure")) {
- InitializeArmPosition(arm::NeutralPosPoint());
+ InitializeArmPosition(arm::NeutralPoint());
phased_loop_handle_ = event_loop_->AddPhasedLoop(
[this](int) {
// Skip this the first time.
@@ -447,7 +447,7 @@
*builder.fbb(), constants::Values::kWristRange().middle());
Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
- goal_builder.add_arm_goal_position(arm::NeutralPosIndex());
+ goal_builder.add_arm_goal_position(arm::NeutralIndex());
goal_builder.add_trajectory_override(false);
goal_builder.add_wrist(wrist_offset);
goal_builder.add_roller_goal(RollerGoal::IDLE);
@@ -473,7 +473,7 @@
Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
- goal_builder.add_arm_goal_position(arm::NeutralPosIndex());
+ goal_builder.add_arm_goal_position(arm::NeutralIndex());
goal_builder.add_trajectory_override(false);
goal_builder.add_wrist(wrist_offset);
goal_builder.add_roller_goal(RollerGoal::IDLE);
@@ -505,7 +505,7 @@
Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
goal_builder.add_wrist(wrist_offset);
- goal_builder.add_arm_goal_position(arm::NeutralPosIndex());
+ goal_builder.add_arm_goal_position(arm::NeutralIndex());
ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
}
@@ -526,7 +526,7 @@
goal_builder.add_wrist(wrist_offset);
- goal_builder.add_arm_goal_position(arm::NeutralPosIndex());
+ goal_builder.add_arm_goal_position(arm::NeutralIndex());
ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
}
@@ -572,7 +572,9 @@
SetEnabled(true);
WaitUntilZeroed();
- double voltage_sign = (GetParam() == GamePiece::CUBE ? -1.0 : 1.0);
+ double spit_voltage =
+ (GetParam() == GamePiece::CUBE ? EndEffector::kRollerCubeSpitVoltage()
+ : EndEffector::kRollerConeSpitVoltage());
double suck_voltage =
(GetParam() == GamePiece::CUBE ? EndEffector::kRollerCubeSuckVoltage()
: EndEffector::kRollerConeSuckVoltage());
@@ -582,7 +584,7 @@
Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
- goal_builder.add_arm_goal_position(arm::NeutralPosIndex());
+ goal_builder.add_arm_goal_position(arm::NeutralIndex());
goal_builder.add_trajectory_override(false);
goal_builder.add_roller_goal(GetParam() == GamePiece::CONE
? RollerGoal::INTAKE_CONE
@@ -623,7 +625,7 @@
Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
- goal_builder.add_arm_goal_position(arm::NeutralPosIndex());
+ goal_builder.add_arm_goal_position(arm::NeutralIndex());
goal_builder.add_trajectory_override(false);
goal_builder.add_roller_goal(RollerGoal::IDLE);
@@ -659,7 +661,7 @@
Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
- goal_builder.add_arm_goal_position(arm::NeutralPosIndex());
+ goal_builder.add_arm_goal_position(arm::NeutralIndex());
goal_builder.add_trajectory_override(false);
goal_builder.add_roller_goal(GetParam() == GamePiece::CONE
? RollerGoal::INTAKE_CONE
@@ -698,7 +700,7 @@
Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
- goal_builder.add_arm_goal_position(arm::NeutralPosIndex());
+ goal_builder.add_arm_goal_position(arm::NeutralIndex());
goal_builder.add_trajectory_override(false);
goal_builder.add_roller_goal(RollerGoal::SPIT);
@@ -712,8 +714,7 @@
ASSERT_TRUE(superstructure_output_fetcher_.Fetch());
ASSERT_TRUE(superstructure_status_fetcher_.Fetch());
- EXPECT_EQ(superstructure_output_fetcher_->roller_voltage(),
- voltage_sign * EndEffector::kRollerSpitVoltage());
+ EXPECT_EQ(superstructure_output_fetcher_->roller_voltage(), spit_voltage);
EXPECT_EQ(superstructure_status_fetcher_->end_effector_state(),
EndEffectorState::SPITTING);
EXPECT_EQ(superstructure_status_fetcher_->game_piece(), GetParam());
@@ -725,8 +726,7 @@
ASSERT_TRUE(superstructure_output_fetcher_.Fetch());
ASSERT_TRUE(superstructure_status_fetcher_.Fetch());
- EXPECT_EQ(superstructure_output_fetcher_->roller_voltage(),
- voltage_sign * EndEffector::kRollerSpitVoltage());
+ EXPECT_EQ(superstructure_output_fetcher_->roller_voltage(), spit_voltage);
EXPECT_EQ(superstructure_status_fetcher_->end_effector_state(),
EndEffectorState::SPITTING);
EXPECT_EQ(superstructure_status_fetcher_->game_piece(), GetParam());
@@ -736,7 +736,7 @@
Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
- goal_builder.add_arm_goal_position(arm::NeutralPosIndex());
+ goal_builder.add_arm_goal_position(arm::NeutralIndex());
goal_builder.add_trajectory_override(false);
goal_builder.add_roller_goal(RollerGoal::IDLE);
@@ -780,7 +780,7 @@
{
auto builder = superstructure_goal_sender_.MakeBuilder();
Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
- goal_builder.add_arm_goal_position(arm::NeutralPosIndex());
+ goal_builder.add_arm_goal_position(arm::NeutralIndex());
ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
}
@@ -803,7 +803,7 @@
TEST_F(SuperstructureTest, ArmMultistepMove) {
SetEnabled(true);
WaitUntilZeroed();
- superstructure_plant_.InitializeArmPosition(arm::NeutralPosPoint());
+ superstructure_plant_.InitializeArmPosition(arm::NeutralPoint());
{
auto builder = superstructure_goal_sender_.MakeBuilder();
@@ -819,7 +819,7 @@
{
auto builder = superstructure_goal_sender_.MakeBuilder();
Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
- goal_builder.add_arm_goal_position(arm::ScoreLowPosIndex());
+ goal_builder.add_arm_goal_position(arm::ConeDownPosIndex());
ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
}
diff --git a/y2023/joystick_reader.cc b/y2023/joystick_reader.cc
index 5cb148c..489934b 100644
--- a/y2023/joystick_reader.cc
+++ b/y2023/joystick_reader.cc
@@ -37,20 +37,26 @@
namespace joysticks {
// TODO(milind): add correct locations
-const ButtonLocation kIntake(4, 5);
const ButtonLocation kScore(4, 4);
const ButtonLocation kSpit(4, 13);
-const ButtonLocation kMidBackTipConeScoreLeft(4, 15);
-const ButtonLocation kHighBackTipConeScoreLeft(4, 14);
-const ButtonLocation kMidBackTipConeScoreRight(3, 2);
+const ButtonLocation kHighConeScoreLeft(4, 14);
+const ButtonLocation kHighConeScoreRight(3, 1);
+
+const ButtonLocation kMidConeScoreLeft(4, 15);
+const ButtonLocation kMidConeScoreRight(3, 2);
+
+const ButtonLocation kHighCube(4, 1);
+const ButtonLocation kMidCube(4, 2);
+const ButtonLocation kLowCube(4, 3);
const ButtonLocation kGroundPickupConeUp(4, 7);
const ButtonLocation kGroundPickupConeDown(4, 8);
const ButtonLocation kGroundPickupCube(4, 10);
const ButtonLocation kHPConePickup(4, 6);
-const ButtonLocation kSuck(4, 12);
+const ButtonLocation kSuck(4, 11);
+const ButtonLocation kBack(4, 12);
const ButtonLocation kWrist(4, 10);
@@ -63,12 +69,18 @@
CUBE = 2,
};
+enum class Side {
+ FRONT = 0,
+ BACK = 1,
+};
+
struct ArmSetpoint {
uint32_t index;
double wrist_goal;
std::optional<double> score_wrist_goal = std::nullopt;
GamePiece game_piece;
- ButtonLocation button;
+ std::vector<ButtonLocation> buttons;
+ Side side;
};
const std::vector<ArmSetpoint> setpoints = {
@@ -76,50 +88,109 @@
.index = arm::GroundPickupBackConeUpIndex(),
.wrist_goal = 0.0,
.game_piece = GamePiece::CONE_UP,
- .button = kGroundPickupConeUp,
+ .buttons = {kGroundPickupConeUp},
+ .side = Side::BACK,
},
{
.index = arm::GroundPickupBackConeDownIndex(),
.wrist_goal = 0.0,
.game_piece = GamePiece::CONE_DOWN,
- .button = kGroundPickupConeDown,
+ .buttons = {kGroundPickupConeDown},
+ .side = Side::BACK,
},
{
.index = arm::ScoreBackMidConeUpPosIndex(),
.wrist_goal = 0.55,
.game_piece = GamePiece::CONE_UP,
- .button = kMidBackTipConeScoreRight,
+ .buttons = {kMidConeScoreRight},
+ .side = Side::BACK,
},
{
.index = arm::ScoreBackMidConeDownPosIndex(),
.wrist_goal = 2.2,
.score_wrist_goal = 0.0,
.game_piece = GamePiece::CONE_DOWN,
- .button = kMidBackTipConeScoreRight,
+ .buttons = {kMidConeScoreRight},
+ .side = Side::BACK,
},
{
.index = arm::HPPickupBackConeUpIndex(),
.wrist_goal = 0.2,
.game_piece = GamePiece::CONE_UP,
- .button = kHPConePickup,
+ .buttons = {kHPConePickup},
+ .side = Side::BACK,
},
{
.index = arm::ScoreFrontHighConeUpPosIndex(),
.wrist_goal = 0.05,
.game_piece = GamePiece::CONE_UP,
- .button = kHighBackTipConeScoreLeft,
+ .buttons = {kHighConeScoreLeft, kHighConeScoreRight},
+ .side = Side::FRONT,
},
{
.index = arm::ScoreFrontMidConeUpPosIndex(),
.wrist_goal = 0.05,
.game_piece = GamePiece::CONE_UP,
- .button = kMidBackTipConeScoreLeft,
+ .buttons = {kMidConeScoreLeft, kMidConeScoreRight},
+ .side = Side::FRONT,
},
{
.index = arm::GroundPickupBackCubeIndex(),
.wrist_goal = 0.6,
.game_piece = GamePiece::CUBE,
- .button = kGroundPickupCube,
+ .buttons = {kGroundPickupCube},
+ .side = Side::BACK,
+ },
+ {
+ .index = arm::ScoreFrontMidCubeIndex(),
+ .wrist_goal = 0.6,
+ .game_piece = GamePiece::CUBE,
+ .buttons = {kMidCube},
+ .side = Side::FRONT,
+ },
+ {
+ .index = arm::ScoreBackMidCubeIndex(),
+ .wrist_goal = 0.6,
+ .score_wrist_goal = 0.0,
+ .game_piece = GamePiece::CUBE,
+ .buttons = {kMidCube},
+ .side = Side::BACK,
+ },
+ {
+ .index = arm::ScoreFrontLowCubeIndex(),
+ .wrist_goal = 0.6,
+ .game_piece = GamePiece::CUBE,
+ .buttons = {kLowCube},
+ .side = Side::FRONT,
+ },
+ {
+ .index = arm::ScoreBackLowCubeIndex(),
+ .wrist_goal = 0.6,
+ .game_piece = GamePiece::CUBE,
+ .buttons = {kLowCube},
+ .side = Side::BACK,
+ },
+ {
+ .index = arm::ScoreFrontHighCubeIndex(),
+ .wrist_goal = 0.6,
+ .game_piece = GamePiece::CUBE,
+ .buttons = {kHighCube},
+ .side = Side::FRONT,
+ },
+ {
+ .index = arm::ScoreBackHighCubeIndex(),
+ .wrist_goal = 0.6,
+ .score_wrist_goal = 0.0,
+ .game_piece = GamePiece::CUBE,
+ .buttons = {kHighCube},
+ .side = Side::BACK,
+ },
+ {
+ .index = arm::GroundPickupFrontCubeIndex(),
+ .wrist_goal = 0.6,
+ .game_piece = GamePiece::CUBE,
+ .buttons = {kGroundPickupCube},
+ .side = Side::FRONT,
},
};
@@ -155,7 +226,7 @@
double wrist_goal = 0.0;
RollerGoal roller_goal = RollerGoal::IDLE;
- arm_goal_position_ = arm::NeutralPosIndex();
+ arm_goal_position_ = arm::NeutralIndex();
std::optional<double> score_wrist_goal = std::nullopt;
if (data.IsPressed(kGroundPickupConeUp) || data.IsPressed(kHPConePickup)) {
@@ -173,14 +244,19 @@
wrist_goal = 0.6;
}
+ const Side current_side = data.IsPressed(kBack) ? Side::BACK : Side::FRONT;
+
// Search for the active setpoint.
for (const ArmSetpoint &setpoint : setpoints) {
- if (data.IsPressed(setpoint.button)) {
- if (setpoint.game_piece == current_game_piece_) {
- wrist_goal = setpoint.wrist_goal;
- arm_goal_position_ = setpoint.index;
- score_wrist_goal = setpoint.score_wrist_goal;
- break;
+ for (const ButtonLocation &button : setpoint.buttons) {
+ if (data.IsPressed(button)) {
+ if (setpoint.game_piece == current_game_piece_ &&
+ setpoint.side == current_side) {
+ wrist_goal = setpoint.wrist_goal;
+ arm_goal_position_ = setpoint.index;
+ score_wrist_goal = setpoint.score_wrist_goal;
+ break;
+ }
}
}
}
diff --git a/y2023/localizer/localizer.cc b/y2023/localizer/localizer.cc
index 4378f61..ace3663 100644
--- a/y2023/localizer/localizer.cc
+++ b/y2023/localizer/localizer.cc
@@ -3,9 +3,16 @@
#include "aos/containers/sized_array.h"
#include "frc971/control_loops/drivetrain/localizer_generated.h"
#include "frc971/control_loops/pose.h"
+#include "gflags/gflags.h"
#include "y2023/constants.h"
#include "y2023/localizer/utils.h"
+DEFINE_double(max_pose_error, 1e-6,
+ "Throw out target poses with a higher pose error than this");
+DEFINE_double(distortion_noise_scalar, 1.0,
+ "Scale the target pose distortion factor by this when computing "
+ "the noise.");
+
namespace y2023::localizer {
namespace {
constexpr std::array<std::string_view, Localizer::kNumCameras> kPisToUse{
@@ -37,7 +44,6 @@
}
} // namespace
-
std::array<Localizer::CameraState, Localizer::kNumCameras>
Localizer::MakeCameras(const Constants &constants, aos::EventLoop *event_loop) {
CHECK(constants.has_cameras());
@@ -278,6 +284,8 @@
// TODO(james): Tune this. Also, gain schedule for auto mode?
Eigen::Matrix<double, 3, 1> noises(1.0, 1.0, 0.5);
+ // Scale noise by the distortion factor for this detection
+ noises *= (1.0 + FLAGS_distortion_noise_scalar * target.distortion_factor());
Eigen::Matrix3d R = Eigen::Matrix3d::Zero();
R.diagonal() = noises.cwiseAbs2();
@@ -293,6 +301,11 @@
if (!state_at_capture.has_value()) {
VLOG(1) << "Rejecting image due to being too old.";
return RejectImage(camera_index, RejectionReason::IMAGE_TOO_OLD, &builder);
+ } else if (target.pose_error() > FLAGS_max_pose_error) {
+ VLOG(1) << "Rejecting target due to high pose error "
+ << target.pose_error();
+ return RejectImage(camera_index, RejectionReason::HIGH_POSE_ERROR,
+ &builder);
}
const Input U = ekf_.MostRecentInput();
diff --git a/y2023/localizer/localizer_test.cc b/y2023/localizer/localizer_test.cc
index 554ab5d..947771f 100644
--- a/y2023/localizer/localizer_test.cc
+++ b/y2023/localizer/localizer_test.cc
@@ -152,6 +152,7 @@
target_builder.add_id(send_target_id_);
target_builder.add_position(position_offset);
target_builder.add_orientation(quat_offset);
+ target_builder.add_pose_error(pose_error_);
auto target_offset = target_builder.Finish();
auto targets_offset = builder.fbb()->CreateVector({target_offset});
@@ -273,6 +274,7 @@
std::unique_ptr<aos::logger::Logger> logger_;
uint64_t send_target_id_ = kTargetId;
+ double pose_error_ = 1e-7;
gflags::FlagSaver flag_saver_;
};
@@ -457,4 +459,26 @@
status_fetcher_->statistics()->Get(0)->total_candidates());
}
+// Tests that we correctly reject a detection with a high pose error.
+TEST_F(LocalizerTest, HighPoseError) {
+ output_voltages_ << 0.0, 0.0;
+ send_targets_ = true;
+ // Send the minimum pose error to be rejected
+ constexpr double kEps = 1e-9;
+ pose_error_ = 1e-6 + kEps;
+
+ event_loop_factory_.RunFor(std::chrono::seconds(4));
+ CHECK(status_fetcher_.Fetch());
+ ASSERT_TRUE(status_fetcher_->has_statistics());
+ ASSERT_EQ(4u /* number of cameras */, status_fetcher_->statistics()->size());
+ ASSERT_EQ(0, status_fetcher_->statistics()->Get(0)->total_accepted());
+ ASSERT_LT(10, status_fetcher_->statistics()->Get(0)->total_candidates());
+ ASSERT_EQ(status_fetcher_->statistics()
+ ->Get(0)
+ ->rejection_reasons()
+ ->Get(static_cast<size_t>(RejectionReason::HIGH_POSE_ERROR))
+ ->count(),
+ status_fetcher_->statistics()->Get(0)->total_candidates());
+}
+
} // namespace y2023::localizer::testing
diff --git a/y2023/localizer/status.fbs b/y2023/localizer/status.fbs
index 4d0a9c1..1b5c6f6 100644
--- a/y2023/localizer/status.fbs
+++ b/y2023/localizer/status.fbs
@@ -15,6 +15,8 @@
MESSAGE_BRIDGE_DISCONNECTED = 2,
// The target ID does not exist.
NO_SUCH_TARGET = 3,
+ // Pose estimation error was higher than any normal detection.
+ HIGH_POSE_ERROR = 4,
}
table RejectionCount {
diff --git a/y2023/vision/aprilrobotics.cc b/y2023/vision/aprilrobotics.cc
index 0102538..70a3f30 100644
--- a/y2023/vision/aprilrobotics.cc
+++ b/y2023/vision/aprilrobotics.cc
@@ -26,13 +26,12 @@
: calibration_data_(event_loop),
image_size_(0, 0),
ftrace_(),
- image_callback_(
- event_loop, channel_name,
- [&](cv::Mat image_color_mat,
- const aos::monotonic_clock::time_point eof) {
- HandleImage(image_color_mat, eof);
- },
- chrono::milliseconds(5)),
+ image_callback_(event_loop, channel_name,
+ [&](cv::Mat image_color_mat,
+ const aos::monotonic_clock::time_point eof) {
+ HandleImage(image_color_mat, eof);
+ },
+ chrono::milliseconds(5)),
target_map_sender_(
event_loop->MakeSender<frc971::vision::TargetMap>("/camera")),
image_annotations_sender_(
@@ -166,6 +165,17 @@
return std::min(distortion_factor / FLAGS_max_expected_distortion, 1.0);
}
+std::vector<cv::Point2f> AprilRoboticsDetector::MakeCornerVector(
+ const apriltag_detection_t *det) {
+ std::vector<cv::Point2f> corner_points;
+ corner_points.emplace_back(det->p[0][0], det->p[0][1]);
+ corner_points.emplace_back(det->p[1][0], det->p[1][1]);
+ corner_points.emplace_back(det->p[2][0], det->p[2][1]);
+ corner_points.emplace_back(det->p[3][0], det->p[3][1]);
+
+ return corner_points;
+}
+
std::vector<AprilRoboticsDetector::Detection> AprilRoboticsDetector::DetectTags(
cv::Mat image, aos::monotonic_clock::time_point eof) {
const aos::monotonic_clock::time_point start_time =
@@ -186,21 +196,25 @@
std::vector<Detection> results;
- std::vector<std::vector<cv::Point2f>> orig_corners_vector;
- std::vector<std::vector<cv::Point2f>> corners_vector;
-
auto builder = image_annotations_sender_.MakeBuilder();
+ std::vector<flatbuffers::Offset<foxglove::PointsAnnotation>> foxglove_corners;
for (int i = 0; i < zarray_size(detections); i++) {
apriltag_detection_t *det;
zarray_get(detections, i, &det);
if (det->decision_margin > FLAGS_min_decision_margin) {
+ // TODO<jim>: Should we check for top/bottom of image?
if (det->p[0][0] < min_x || det->p[0][0] > max_x ||
det->p[1][0] < min_x || det->p[1][0] > max_x ||
det->p[2][0] < min_x || det->p[2][0] > max_x ||
det->p[3][0] < min_x || det->p[3][0] > max_x) {
VLOG(1) << "Rejecting detection because corner is outside pixel border";
+ // Send rejected corner points in red
+ std::vector<cv::Point2f> rejected_corner_points = MakeCornerVector(det);
+ foxglove_corners.push_back(frc971::vision::BuildPointsAnnotation(
+ builder.fbb(), eof, rejected_corner_points,
+ std::vector<double>{1.0, 0.0, 0.0, 0.5}));
continue;
}
VLOG(1) << "Found tag number " << det->id << " hamming: " << det->hamming
@@ -217,14 +231,11 @@
info.cx = intrinsics_.at<double>(0, 2);
info.cy = intrinsics_.at<double>(1, 2);
- // Store out the original, pre-undistortion corner points for sending
- std::vector<cv::Point2f> orig_corner_points;
- orig_corner_points.emplace_back(det->p[0][0], det->p[0][1]);
- orig_corner_points.emplace_back(det->p[1][0], det->p[1][1]);
- orig_corner_points.emplace_back(det->p[2][0], det->p[2][1]);
- orig_corner_points.emplace_back(det->p[3][0], det->p[3][1]);
-
- orig_corners_vector.emplace_back(orig_corner_points);
+ // Send original corner points in green
+ std::vector<cv::Point2f> orig_corner_points = MakeCornerVector(det);
+ foxglove_corners.push_back(frc971::vision::BuildPointsAnnotation(
+ builder.fbb(), eof, orig_corner_points,
+ std::vector<double>{0.0, 1.0, 0.0, 0.5}));
UndistortDetection(det);
@@ -243,13 +254,11 @@
<< " seconds for pose estimation";
VLOG(1) << "Pose err: " << pose_error;
- std::vector<cv::Point2f> corner_points;
- corner_points.emplace_back(det->p[0][0], det->p[0][1]);
- corner_points.emplace_back(det->p[1][0], det->p[1][1]);
- corner_points.emplace_back(det->p[2][0], det->p[2][1]);
- corner_points.emplace_back(det->p[3][0], det->p[3][1]);
-
- corners_vector.emplace_back(corner_points);
+ // Send undistorted corner points in pink
+ std::vector<cv::Point2f> corner_points = MakeCornerVector(det);
+ foxglove_corners.push_back(frc971::vision::BuildPointsAnnotation(
+ builder.fbb(), eof, corner_points,
+ std::vector<double>{1.0, 0.75, 0.8, 1.0}));
double distortion_factor =
ComputeDistortionFactor(orig_corner_points, corner_points);
@@ -261,9 +270,10 @@
}
}
- const auto annotations_offset = frc971::vision::BuildAnnotations(
- eof, orig_corners_vector, 5.0, builder.fbb());
- builder.CheckOk(builder.Send(annotations_offset));
+ foxglove::ImageAnnotations::Builder annotation_builder(*builder.fbb());
+ const auto corners_offset = builder.fbb()->CreateVector(foxglove_corners);
+ annotation_builder.add_points(corners_offset);
+ builder.CheckOk(builder.Send(annotation_builder.Finish()));
apriltag_detections_destroy(detections);
diff --git a/y2023/vision/aprilrobotics.h b/y2023/vision/aprilrobotics.h
index fd371c7..bf9265b 100644
--- a/y2023/vision/aprilrobotics.h
+++ b/y2023/vision/aprilrobotics.h
@@ -40,6 +40,9 @@
// Undistorts the april tag corners using the camera calibration
void UndistortDetection(apriltag_detection_t *det) const;
+ // Helper function to store detection points in vector of Point2f's
+ std::vector<cv::Point2f> MakeCornerVector(const apriltag_detection_t *det);
+
std::vector<Detection> DetectTags(cv::Mat image,
aos::monotonic_clock::time_point eof);
diff --git a/y2023/y2023_roborio.json b/y2023/y2023_roborio.json
index 8ef43d3..0ed62c7 100644
--- a/y2023/y2023_roborio.json
+++ b/y2023/y2023_roborio.json
@@ -561,6 +561,7 @@
},
{
"name": "constants_sender_roborio",
+ "executable_name": "constants_sender",
"autorestart": false,
"nodes": [
"roborio"