blob: a6bcb4d1f12bcb0b25ef7863a9f43377579bf2d4 [file] [log] [blame]
Jim Ostrowski977850f2022-01-22 21:04:22 -08001#include "frc971/vision/v4l2_reader.h"
Brian Silverman9dd793b2020-01-31 23:52:21 -08002
3#include <fcntl.h>
4#include <linux/videodev2.h>
5#include <sys/ioctl.h>
6#include <sys/stat.h>
7#include <sys/types.h>
8
Jim Ostrowski8565b402020-02-29 20:26:53 -08009DEFINE_bool(ignore_timestamps, false,
10 "Don't require timestamps on images. Used to allow webcams");
11
Brian Silverman9dd793b2020-01-31 23:52:21 -080012namespace frc971 {
13namespace vision {
14
Austin Schuh77d0bbd2022-12-26 14:00:51 -080015V4L2ReaderBase::V4L2ReaderBase(aos::EventLoop *event_loop,
16 const std::string &device_name)
17 : fd_(open(device_name.c_str(), O_RDWR | O_NONBLOCK)),
18 event_loop_(event_loop) {
Jim Ostrowskifec0c332022-02-06 23:28:26 -080019 PCHECK(fd_.get() != -1) << " Failed to open device " << device_name;
Brian Silverman9dd793b2020-01-31 23:52:21 -080020
Austin Schuh77d0bbd2022-12-26 14:00:51 -080021 // Figure out if we are multi-planar or not.
22 {
23 struct v4l2_capability capability;
24 memset(&capability, 0, sizeof(capability));
25 PCHECK(Ioctl(VIDIOC_QUERYCAP, &capability) == 0);
26
27 LOG(INFO) << "Opening " << device_name;
28 LOG(INFO) << " driver " << capability.driver;
29 LOG(INFO) << " card " << capability.card;
30 LOG(INFO) << " bus_info " << capability.bus_info;
31 if (capability.capabilities & V4L2_CAP_VIDEO_CAPTURE_MPLANE) {
32 LOG(INFO) << " Multi-planar";
33 multiplanar_ = true;
34 }
35 }
36
Brian Silverman9dd793b2020-01-31 23:52:21 -080037 // First, clean up after anybody else who left the device streaming.
Brian Silverman8f24adb2020-02-02 17:15:58 -080038 StreamOff();
Austin Schuh77d0bbd2022-12-26 14:00:51 -080039}
Brian Silverman9dd793b2020-01-31 23:52:21 -080040
Austin Schuh77d0bbd2022-12-26 14:00:51 -080041void V4L2ReaderBase::StreamOn() {
Brian Silverman9dd793b2020-01-31 23:52:21 -080042 {
43 struct v4l2_requestbuffers request;
44 memset(&request, 0, sizeof(request));
45 request.count = buffers_.size();
Austin Schuh77d0bbd2022-12-26 14:00:51 -080046 request.type = multiplanar() ? V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE
47 : V4L2_BUF_TYPE_VIDEO_CAPTURE;
Brian Silverman9dd793b2020-01-31 23:52:21 -080048 request.memory = V4L2_MEMORY_USERPTR;
49 PCHECK(Ioctl(VIDIOC_REQBUFS, &request) == 0);
50 CHECK_EQ(request.count, buffers_.size())
51 << ": Kernel refused to give us the number of buffers we asked for";
52 }
53
Austin Schuh77d0bbd2022-12-26 14:00:51 -080054 {
55 struct v4l2_format format;
56 memset(&format, 0, sizeof(format));
57 format.type = multiplanar() ? V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE
58 : V4L2_BUF_TYPE_VIDEO_CAPTURE;
59 PCHECK(Ioctl(VIDIOC_G_FMT, &format) == 0);
60
61 if (multiplanar()) {
62 cols_ = format.fmt.pix_mp.width;
63 rows_ = format.fmt.pix_mp.height;
64 LOG(INFO) << "Format is " << cols_ << ", " << rows_;
65 CHECK_EQ(format.fmt.pix_mp.pixelformat, V4L2_PIX_FMT_YUYV)
66 << ": Invalid pixel format";
67
68 CHECK_EQ(format.fmt.pix_mp.num_planes, 1u);
69
70 CHECK_EQ(static_cast<int>(format.fmt.pix_mp.plane_fmt[0].bytesperline),
71 cols_ * 2 /* bytes per pixel */);
72 CHECK_EQ(format.fmt.pix_mp.plane_fmt[0].sizeimage, ImageSize());
73 } else {
74 cols_ = format.fmt.pix.width;
75 rows_ = format.fmt.pix.height;
76 LOG(INFO) << "Format is " << cols_ << ", " << rows_;
77 CHECK_EQ(format.fmt.pix.pixelformat, V4L2_PIX_FMT_YUYV)
78 << ": Invalid pixel format";
79
80 CHECK_EQ(static_cast<int>(format.fmt.pix.bytesperline),
81 cols_ * 2 /* bytes per pixel */);
82 CHECK_EQ(format.fmt.pix.sizeimage, ImageSize());
83 }
84 }
85
Brian Silverman9dd793b2020-01-31 23:52:21 -080086 for (size_t i = 0; i < buffers_.size(); ++i) {
Austin Schuh77d0bbd2022-12-26 14:00:51 -080087 buffers_[i].sender = event_loop_->MakeSender<CameraImage>("/camera");
Brian Silverman9dd793b2020-01-31 23:52:21 -080088 EnqueueBuffer(i);
89 }
Austin Schuh77d0bbd2022-12-26 14:00:51 -080090 int type = multiplanar() ? V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE
91 : V4L2_BUF_TYPE_VIDEO_CAPTURE;
92 PCHECK(Ioctl(VIDIOC_STREAMON, &type) == 0);
Brian Silverman9dd793b2020-01-31 23:52:21 -080093}
94
Ravago Jones65469be2023-01-13 21:28:23 -080095void V4L2ReaderBase::MaybeEnqueue() {
Jim Ostrowski977850f2022-01-22 21:04:22 -080096 // First, enqueue any old buffer we already have. This is the one which
97 // may have been sent.
Brian Silverman967e5df2020-02-09 16:43:34 -080098 if (saved_buffer_) {
99 EnqueueBuffer(saved_buffer_.index);
100 saved_buffer_.Clear();
Brian Silverman9dd793b2020-01-31 23:52:21 -0800101 }
Austin Schuhe4acc1e2022-12-26 14:01:34 -0800102 ftrace_.FormatMessage("Enqueued previous buffer %d", saved_buffer_.index);
Ravago Jones65469be2023-01-13 21:28:23 -0800103}
104
105bool V4L2ReaderBase::ReadLatestImage() {
106 MaybeEnqueue();
107
Brian Silverman9dd793b2020-01-31 23:52:21 -0800108 while (true) {
Brian Silverman967e5df2020-02-09 16:43:34 -0800109 const BufferInfo previous_buffer = saved_buffer_;
Brian Silverman9dd793b2020-01-31 23:52:21 -0800110 saved_buffer_ = DequeueBuffer();
Austin Schuhe4acc1e2022-12-26 14:01:34 -0800111 ftrace_.FormatMessage("Dequeued %d", saved_buffer_.index);
Brian Silverman967e5df2020-02-09 16:43:34 -0800112 if (saved_buffer_) {
Brian Silverman9dd793b2020-01-31 23:52:21 -0800113 // We got a new buffer. Return the previous one (if relevant) and keep
114 // going.
Brian Silverman967e5df2020-02-09 16:43:34 -0800115 if (previous_buffer) {
Austin Schuhe4acc1e2022-12-26 14:01:34 -0800116 ftrace_.FormatMessage("Previous %d", previous_buffer.index);
Brian Silverman967e5df2020-02-09 16:43:34 -0800117 EnqueueBuffer(previous_buffer.index);
Brian Silverman9dd793b2020-01-31 23:52:21 -0800118 }
119 continue;
120 }
Brian Silverman967e5df2020-02-09 16:43:34 -0800121 if (!previous_buffer) {
Brian Silverman9dd793b2020-01-31 23:52:21 -0800122 // There were no images to read. Return an indication of that.
Austin Schuhe4acc1e2022-12-26 14:01:34 -0800123 ftrace_.FormatMessage("No images to read");
Brian Silverman967e5df2020-02-09 16:43:34 -0800124 return false;
Brian Silverman9dd793b2020-01-31 23:52:21 -0800125 }
126 // We didn't get a new one, but we already got one in a previous
127 // iteration, which means we found an image so return it.
Austin Schuhe4acc1e2022-12-26 14:01:34 -0800128 ftrace_.FormatMessage("Got saved buffer %d", saved_buffer_.index);
Brian Silverman9dd793b2020-01-31 23:52:21 -0800129 saved_buffer_ = previous_buffer;
Brian Silverman967e5df2020-02-09 16:43:34 -0800130 buffers_[saved_buffer_.index].PrepareMessage(rows_, cols_, ImageSize(),
131 saved_buffer_.monotonic_eof);
132 return true;
Brian Silverman9dd793b2020-01-31 23:52:21 -0800133 }
134}
135
Austin Schuh77d0bbd2022-12-26 14:00:51 -0800136void V4L2ReaderBase::SendLatestImage() { buffers_[saved_buffer_.index].Send(); }
Brian Silverman967e5df2020-02-09 16:43:34 -0800137
Austin Schuh77d0bbd2022-12-26 14:00:51 -0800138void V4L2ReaderBase::SetExposure(size_t duration) {
milind-udaebe9b2022-01-09 18:25:24 -0800139 v4l2_control manual_control;
140 manual_control.id = V4L2_CID_EXPOSURE_AUTO;
141 manual_control.value = V4L2_EXPOSURE_MANUAL;
142 PCHECK(Ioctl(VIDIOC_S_CTRL, &manual_control) == 0);
143
144 v4l2_control exposure_control;
145 exposure_control.id = V4L2_CID_EXPOSURE_ABSOLUTE;
146 exposure_control.value = static_cast<int>(duration); // 100 micro s units
147 PCHECK(Ioctl(VIDIOC_S_CTRL, &exposure_control) == 0);
148}
149
Austin Schuh77d0bbd2022-12-26 14:00:51 -0800150void V4L2ReaderBase::UseAutoExposure() {
milind-udaebe9b2022-01-09 18:25:24 -0800151 v4l2_control control;
152 control.id = V4L2_CID_EXPOSURE_AUTO;
153 control.value = V4L2_EXPOSURE_AUTO;
154 PCHECK(Ioctl(VIDIOC_S_CTRL, &control) == 0);
155}
156
Austin Schuh77d0bbd2022-12-26 14:00:51 -0800157void V4L2ReaderBase::Buffer::InitializeMessage(size_t max_image_size) {
Brian Silverman967e5df2020-02-09 16:43:34 -0800158 message_offset = flatbuffers::Offset<CameraImage>();
159 builder = aos::Sender<CameraImage>::Builder();
160 builder = sender.MakeBuilder();
161 // The kernel has an undocumented requirement that the buffer is aligned
Austin Schuh77d0bbd2022-12-26 14:00:51 -0800162 // to 128 bytes. If you give it a nonaligned pointer, it will return EINVAL
Brian Silverman967e5df2020-02-09 16:43:34 -0800163 // and only print something in dmesg with the relevant dynamic debug
164 // prints turned on.
Austin Schuh77d0bbd2022-12-26 14:00:51 -0800165 builder.fbb()->StartIndeterminateVector(max_image_size, 1, 128,
166 &data_pointer);
167 CHECK_EQ(reinterpret_cast<uintptr_t>(data_pointer) % 128, 0u)
Brian Silverman967e5df2020-02-09 16:43:34 -0800168 << ": Flatbuffers failed to align things as requested";
169}
170
Austin Schuh77d0bbd2022-12-26 14:00:51 -0800171void V4L2ReaderBase::Buffer::PrepareMessage(
Brian Silverman967e5df2020-02-09 16:43:34 -0800172 int rows, int cols, size_t image_size,
173 aos::monotonic_clock::time_point monotonic_eof) {
174 CHECK(data_pointer != nullptr);
175 data_pointer = nullptr;
176
177 const auto data_offset = builder.fbb()->EndIndeterminateVector(image_size, 1);
178 auto image_builder = builder.MakeBuilder<CameraImage>();
179 image_builder.add_data(data_offset);
180 image_builder.add_rows(rows);
181 image_builder.add_cols(cols);
182 image_builder.add_monotonic_timestamp_ns(
183 std::chrono::nanoseconds(monotonic_eof.time_since_epoch()).count());
184 message_offset = image_builder.Finish();
Brian Silverman9dd793b2020-01-31 23:52:21 -0800185}
186
Austin Schuh77d0bbd2022-12-26 14:00:51 -0800187int V4L2ReaderBase::Ioctl(unsigned long number, void *arg) {
Brian Silverman9dd793b2020-01-31 23:52:21 -0800188 return ioctl(fd_.get(), number, arg);
189}
190
Austin Schuh77d0bbd2022-12-26 14:00:51 -0800191V4L2ReaderBase::BufferInfo V4L2ReaderBase::DequeueBuffer() {
Brian Silverman9dd793b2020-01-31 23:52:21 -0800192 struct v4l2_buffer buffer;
193 memset(&buffer, 0, sizeof(buffer));
Brian Silverman9dd793b2020-01-31 23:52:21 -0800194 buffer.memory = V4L2_MEMORY_USERPTR;
Austin Schuh77d0bbd2022-12-26 14:00:51 -0800195 if (multiplanar()) {
196 buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
197 struct v4l2_plane planes[1];
198 std::memset(planes, 0, sizeof(planes));
199 buffer.m.planes = planes;
200 buffer.length = 1;
201 const int result = Ioctl(VIDIOC_DQBUF, &buffer);
202 if (result == -1 && errno == EAGAIN) {
203 return BufferInfo();
204 }
205 PCHECK(result == 0) << ": VIDIOC_DQBUF failed";
206 CHECK_LT(buffer.index, buffers_.size());
207
208 CHECK_EQ(reinterpret_cast<uintptr_t>(buffers_[buffer.index].data_pointer),
209 planes[0].m.userptr);
210
211 CHECK_EQ(ImageSize(), planes[0].length);
212 } else {
213 buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
214 const int result = Ioctl(VIDIOC_DQBUF, &buffer);
215 if (result == -1 && errno == EAGAIN) {
216 return BufferInfo();
217 }
218 PCHECK(result == 0) << ": VIDIOC_DQBUF failed";
219 CHECK_LT(buffer.index, buffers_.size());
220 CHECK_EQ(reinterpret_cast<uintptr_t>(buffers_[buffer.index].data_pointer),
221 buffer.m.userptr);
222 CHECK_EQ(ImageSize(), buffer.length);
Brian Silverman9dd793b2020-01-31 23:52:21 -0800223 }
Brian Silverman967e5df2020-02-09 16:43:34 -0800224 CHECK(buffer.flags & V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC);
Jim Ostrowski8565b402020-02-29 20:26:53 -0800225 if (!FLAGS_ignore_timestamps) {
226 // Require that we have good timestamp on images
227 CHECK_EQ(buffer.flags & V4L2_BUF_FLAG_TSTAMP_SRC_MASK,
228 static_cast<uint32_t>(V4L2_BUF_FLAG_TSTAMP_SRC_EOF));
229 }
Brian Silverman967e5df2020-02-09 16:43:34 -0800230 return {static_cast<int>(buffer.index),
231 aos::time::from_timeval(buffer.timestamp)};
Brian Silverman9dd793b2020-01-31 23:52:21 -0800232}
233
Austin Schuh77d0bbd2022-12-26 14:00:51 -0800234void V4L2ReaderBase::EnqueueBuffer(int buffer_number) {
235 // TODO(austin): Detect multiplanar and do this all automatically.
236
Brian Silverman9dd793b2020-01-31 23:52:21 -0800237 CHECK_GE(buffer_number, 0);
238 CHECK_LT(buffer_number, static_cast<int>(buffers_.size()));
239 buffers_[buffer_number].InitializeMessage(ImageSize());
240 struct v4l2_buffer buffer;
Austin Schuh77d0bbd2022-12-26 14:00:51 -0800241 struct v4l2_plane planes[1];
Brian Silverman9dd793b2020-01-31 23:52:21 -0800242 memset(&buffer, 0, sizeof(buffer));
Austin Schuh77d0bbd2022-12-26 14:00:51 -0800243 memset(&planes, 0, sizeof(planes));
Brian Silverman9dd793b2020-01-31 23:52:21 -0800244 buffer.memory = V4L2_MEMORY_USERPTR;
245 buffer.index = buffer_number;
Austin Schuh77d0bbd2022-12-26 14:00:51 -0800246 if (multiplanar()) {
247 buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
248 buffer.m.planes = planes;
249 buffer.length = 1;
250 planes[0].m.userptr =
251 reinterpret_cast<uintptr_t>(buffers_[buffer_number].data_pointer);
252 planes[0].length = ImageSize();
253 planes[0].bytesused = planes[0].length;
254 } else {
255 buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
256 buffer.m.userptr =
257 reinterpret_cast<uintptr_t>(buffers_[buffer_number].data_pointer);
258 buffer.length = ImageSize();
259 }
260
Brian Silverman9dd793b2020-01-31 23:52:21 -0800261 PCHECK(Ioctl(VIDIOC_QBUF, &buffer) == 0);
262}
263
Austin Schuh77d0bbd2022-12-26 14:00:51 -0800264void V4L2ReaderBase::StreamOff() {
265 int type = multiplanar() ? V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE
266 : V4L2_BUF_TYPE_VIDEO_CAPTURE;
Brian Silverman8f24adb2020-02-02 17:15:58 -0800267 const int result = Ioctl(VIDIOC_STREAMOFF, &type);
268 if (result == 0) {
269 return;
270 }
Jim Ostrowski977850f2022-01-22 21:04:22 -0800271 // Some devices (like Alex's webcam) return this if streaming isn't
272 // currently on, unlike what the documentations says should happen.
Brian Silverman8f24adb2020-02-02 17:15:58 -0800273 if (errno == EBUSY) {
274 return;
275 }
276 PLOG(FATAL) << "VIDIOC_STREAMOFF failed";
277}
278
Austin Schuh77d0bbd2022-12-26 14:00:51 -0800279V4L2Reader::V4L2Reader(aos::EventLoop *event_loop,
280 const std::string &device_name)
281 : V4L2ReaderBase(event_loop, device_name) {
282 // Don't know why this magic call to SetExposure is required (before the
283 // camera settings are configured) to make things work on boot of the pi, but
284 // it seems to be-- without it, the image exposure is wrong (too dark). Note--
285 // any valid value seems to work-- just choosing 1 for now
286
287 SetExposure(1);
288
289 struct v4l2_format format;
290 memset(&format, 0, sizeof(format));
291 format.type = multiplanar() ? V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE
292 : V4L2_BUF_TYPE_VIDEO_CAPTURE;
milind-u6b094092023-01-09 19:26:12 -0800293
294 constexpr int kWidth = 640;
295 constexpr int kHeight = 480;
296 format.fmt.pix.width = kWidth;
297 format.fmt.pix.height = kHeight;
Austin Schuh77d0bbd2022-12-26 14:00:51 -0800298 format.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
299 // This means we want to capture from a progressive (non-interlaced)
300 // source.
301 format.fmt.pix.field = V4L2_FIELD_NONE;
302 PCHECK(Ioctl(VIDIOC_S_FMT, &format) == 0);
milind-u6b094092023-01-09 19:26:12 -0800303 CHECK_EQ(static_cast<int>(format.fmt.pix.width), kWidth);
304 CHECK_EQ(static_cast<int>(format.fmt.pix.height), kHeight);
Austin Schuh77d0bbd2022-12-26 14:00:51 -0800305 CHECK_EQ(static_cast<int>(format.fmt.pix.bytesperline),
milind-u6b094092023-01-09 19:26:12 -0800306 kWidth * 2 /* bytes per pixel */);
307 CHECK_EQ(format.fmt.pix.sizeimage, ImageSize(kHeight, kWidth));
Austin Schuh77d0bbd2022-12-26 14:00:51 -0800308
309 StreamOn();
310}
311
312RockchipV4L2Reader::RockchipV4L2Reader(aos::EventLoop *event_loop,
Ravago Jonesdc524752022-12-27 01:15:13 -0800313 aos::internal::EPoll *epoll,
Ravago Jones65469be2023-01-13 21:28:23 -0800314 const std::string &device_name,
315 const std::string &image_sensor_subdev)
316 : V4L2ReaderBase(event_loop, device_name),
317 epoll_(epoll),
318 image_sensor_fd_(open(image_sensor_subdev.c_str(), O_RDWR | O_NONBLOCK)) {
319 PCHECK(image_sensor_fd_.get() != -1)
320 << " Failed to open device " << device_name;
321
Austin Schuh77d0bbd2022-12-26 14:00:51 -0800322 StreamOn();
Ravago Jonesdc524752022-12-27 01:15:13 -0800323 epoll_->OnReadable(fd().get(), [this]() { OnImageReady(); });
324}
325
Ravago Jonesfd8aa202023-01-16 14:21:45 -0800326RockchipV4L2Reader::~RockchipV4L2Reader() { epoll_->DeleteFd(fd().get()); }
327
Ravago Jonesdc524752022-12-27 01:15:13 -0800328void RockchipV4L2Reader::OnImageReady() {
329 if (!ReadLatestImage()) {
330 return;
331 }
332
333 SendLatestImage();
Austin Schuh77d0bbd2022-12-26 14:00:51 -0800334}
335
Ravago Jones65469be2023-01-13 21:28:23 -0800336int RockchipV4L2Reader::ImageSensorIoctl(unsigned long number, void *arg) {
337 return ioctl(image_sensor_fd_.get(), number, arg);
338}
339
340void RockchipV4L2Reader::SetExposure(size_t duration) {
341 v4l2_control exposure_control;
342 exposure_control.id = V4L2_CID_EXPOSURE;
343 exposure_control.value = static_cast<int>(duration);
344 PCHECK(ImageSensorIoctl(VIDIOC_S_CTRL, &exposure_control) == 0);
345}
346
347void RockchipV4L2Reader::SetGain(size_t gain) {
Ravago Jonesa0a2e062023-01-03 21:45:18 -0800348 v4l2_control gain_control;
349 gain_control.id = V4L2_CID_GAIN;
350 gain_control.value = static_cast<int>(gain);
351 PCHECK(ImageSensorIoctl(VIDIOC_S_CTRL, &gain_control) == 0);
352}
353
354void RockchipV4L2Reader::SetGainExt(size_t gain) {
Ravago Jones65469be2023-01-13 21:28:23 -0800355 struct v4l2_ext_controls controls;
356 memset(&controls, 0, sizeof(controls));
357 struct v4l2_ext_control control[1];
358 memset(&control, 0, sizeof(control));
359
360 controls.ctrl_class = V4L2_CTRL_CLASS_IMAGE_SOURCE;
361 controls.count = 1;
362 controls.controls = control;
363 control[0].id = V4L2_CID_ANALOGUE_GAIN;
364 control[0].value = gain;
365
366 PCHECK(ImageSensorIoctl(VIDIOC_S_EXT_CTRLS, &controls) == 0);
367}
368
Ravago Jonesda1b0082023-01-21 15:33:19 -0800369void RockchipV4L2Reader::SetVerticalBlanking(size_t vblank) {
370 struct v4l2_ext_controls controls;
371 memset(&controls, 0, sizeof(controls));
372 struct v4l2_ext_control control[1];
373 memset(&control, 0, sizeof(control));
Ravago Jonesa0a2e062023-01-03 21:45:18 -0800374
Ravago Jonesda1b0082023-01-21 15:33:19 -0800375 controls.ctrl_class = V4L2_CTRL_CLASS_IMAGE_SOURCE;
376 controls.count = 1;
377 controls.controls = control;
378 control[0].id = V4L2_CID_VBLANK;
379 control[0].value = vblank;
380
381 PCHECK(ImageSensorIoctl(VIDIOC_S_EXT_CTRLS, &controls) == 0);
Ravago Jonesa0a2e062023-01-03 21:45:18 -0800382}
383
Brian Silverman9dd793b2020-01-31 23:52:21 -0800384} // namespace vision
385} // namespace frc971