Build update
This one is pretty good toThis one is pretty good to

FIRST Robotics Team 4322 – Venturing Crew 1701
This one is pretty good to
This was genuinely one of the hardest pick calls we have ever had to make
We were going back and forth basically right up to the wire. The other options had gotten better day 2 and not seeing 7415 score their last match was a bummer. The actual final call was not made until about 2 min before we started alliance selection. I never want to have to go through something that stressful again ![]()
After this show of hands I called @Alon22 and said “My students are a bunch of degenerate gamblers and we want to first pick you” and I think that really set the tone for the rest of the event ![]()
I think the students were sad they couldn’t go to the casino next door /s
Centrifuge15 photosI think most people who knew anything about robot performance would’ve predicted 4414 picking 254. I also think most people would be rather confused at our first pick.
So let’s talk about that story, because it was it basically defined how day 2 went.
We figured that our odds of making finals were pretty solid. Winning, however, was another matter.
When you’re staring down a contender for “greatest robot of all time” and 254 on A1, and your robot was built in the 2nd floor of a local church community room, your chances aren’t great. We figured if we wanted even a 1% chance of doing something extremely funny, we needed to pick for a high ceiling with a partner very willing to get up to some Absolute Nonsense.
Jaguar Robotics sponsored by Feit Electric checked off both “high ceiling” and “willing to do nonsense.”
After all:
They approached us the night after Day 1 suggesting it, and after thinking about it, we had a team discussion. After all, we really did mesh well with them as we had scouted with them in Galileo and talked to @Alon22 off and on ever since.
We ended up polling the team with a show of hands in a hotel room:
After all, our 2026 inseason had been nothing but taking obscene risks and seeing it play out. Why stop now?
The plan was simple. Pick 7415 first pick for the scoring ceiling and the inter-team dynamic. Then try and pick their 200 ball hopper-only B-bot to try and starve TidePoofs.
Trying to get the TickBot to last to A2 seemed like a challenge, but for most alliances the hope was that other teams who struggled on day 1 would get their robots going so they would be sensible lower-alliance picks over the TickBot.
After all, if your alliance had limited scoring throughput in the captain and first pick, picking for either raw 3rd bot offense or strong defense was likely higher ROI to try and 3v2 or 2v1 higher alliances with only two scoring robots.
And things were definitely pointing that way. On day 2, I asked how several teams’ robots were doing and if they needed help. Many of them had figured out it was a wiring issue that they had already managed to discover and fix, so day 2 was a lot better for them with a lot more consistently driving robots.
Together, we ended up labbing out backup 2nd picks (if we didn’t get the TickBot) and trying to predict alliance selection. The only real condition was that in at least one quals match the next day we got to see their A team drive the A robot (rather than the “tickbot”) to see if they still had some of the Galileo sauce.
And so we went into day 2 with two qualification matches remaining. We faced both 7415a and 7415b and 6560’s tall defense bot in a 2v3, and hitting the needed 5 RP was not a guarantee. Mind you, our singular other partner was 254.
We were slightly worried about 254; as in their match beforehand their intake failed to deploy. We asked, they said they were gonna fix it, and we indeed saw them fix it. They found that as long as they didn’t try to stow the intake in teleop that it would all work fine, which it did.
The bigger problem, however, was 7415a. The heavy defense that was played in that quals match had dealt rather severe damage to their robot. They made some rush repairs but in the process ended up with their shooter CAN line dead in the last qualification match where they were trying to show off their abilities. They played some sick defense, though, but it made our choices a lot harder.
We began wondering “do we really want to do this?” 7415 was very understanding if we went the other way. It became a heated, painful topic internally. While we still trusted that 7415 would fix their robot, we didn’t have a ton of data to go off of in terms of scoring potential except for like one match at the end of Day 1 where they apparently ran their primary driver but without the primary drive coach.
Finally, we figured to take a temperature check on our other options, and so we asked around. Other potential partners seemed to be thinking more traditionally. A bit too traditionally, for our shenaniganful tastes.
So we decided to go all in. 7415 or bust.
Unfortunately, the tickbot got nabbed by A7, likely to help 8229B shoot without needing to reposition. So we landed on the next best option and got 5012, a notably strong defender, and 4 as a backup for their ability to score balls and the large size of their robot.
Our first match went really well. While 7415 wasn’t at 100% (they kept jamming when full with enough balls), we put up a solid 471. But we’d need to score more to even have a slight chance of taking down A1, so we went back to the pits and we worked to try and figure out the jams.
Our second playoff match was against 973’s alliance. We knew that 8005a had a shot blocker, but 7415 had a plan.
Based on the wording of the rules surrounding damage within frame perimeter, some rather important wands were attached to the sides of the shooters.
We told the opposing alliance about these wands because we didn’t want to win off of some G415 nonsense. They ended up focusing on pure scoring anyway so it didn’t really matter.
The match ran, but it ended up being way too close.
Between one of 7415’s scouts and one of our mentors, one of them said “I think we lost this one.” and the other said “I think we have this one.”

“Split the difference?” “Yeah.”
599 played some excellent bump defense, and we didn’t do a great job of screening the way for 7415. I guess we’ll have to watch more 2056 + 1114 at ONT DCMP.
After that match, I joked offhandedly,
Y’know? What if we like, made a shot blocker on 5012? Like take a polycarb flap and some rope and tie it to a shooter or intake motor or something idk
I didn’t expect anyone to take it seriously. But the next thing I know, students and mentors from 7415, 4322, and 5012 all gathered around to try and and make this a real thing.
The next match we played was against Tide, so we figured we were gonna lose. We played team 4 that match, who we lent some of our LYNK bumper skids to in order to help them go over bump.
…We lost. (Obviously). But by Match 13, it was ready. Maybe.
Watch the video on YouTubeI will say, the game announcer’s reaction to the shot blocker was really funny. While it broke really quickly, it did really well for something made in 30 minutes from scratch.
We went in with more preparation. We positioned @Alon22’s brother Lior as 5012’s drive coach. This was his first time drive coaching and he, I cannot emphasize this enough, knocked it out of the park.
We noticed that 8005 would only shoot from a fixed distance from the hub, so when Lior shouted
8005!
Gryffingear would gun it towards their preferred scoring arc…and immediately after screwing with their shooting solution they sprinted to 973 as they were doing a trenchside shot to shove them under, effectively shot-blocking them with the trench itself. I swear the entire alliance felt significantly smarter with Lior in the box. I guess generational drive coaching runs in the family, huh?
We ended up taking a much more convincing win going into finals. But by Match 13, everyone on the team had zero regrets picking 7415 even if we had lost, because this whole thing was just so much fun. Especially for all the kids for which Tidal was the first event with the team!
We lost lol. We still kept trying to iterate the shot blocker, much to the horror of the dumpers, but it never quite worked. That said, up until shift 3 we ended up neck and neck with TidePoofs. The odds were astronomically bad, but you can’t say we didn’t try!
7415 finally broke, so they ran double defense. Much like a couple other finals series this year, we found ourselves the primary (and in this case only) scoring robot in our alliance zone against Einstein-level robots.
But at this point, we all knew that we had lost, so we decided to just have fun and go smack around TidePoofs defensively anyway and play it out.
Yeah! Obviously! Everyone on that alliance had a ton of fun. We had an absolute blast working with our whole alliance coming up with all sorts of off the wall Nonsense together.
There’s a lot I’m skimming over that I wish I could give justice to. I’ll leave that to everyone else to fill in.
Sure, at a district event, we would’ve likely made a more conservative pick. (7415’s strat team were like “yeah duh obviously”) But there’s no Championship qualification on the line at Tidal. And I think the team (and everyone else involved was much better off for it.
We have zero regrets for our choices.
See you at Beach Blitz.
CentrifugeA closeup of 1148 nearly tipping us:
Centrifuge11 photosFor the first time in 2026, team 4322 Clockwork from Orange, California sponsored by Boy Scouts of America, Christ Lutheran Church of Orange, the City of Orange Public Library Foundation, Foothill High School, BAE Systems, RTX, Northrop Grumman, Intuitive Foundation, California Filament, Gene Haas Foundation, Molex, StayLinked, Fabworks, Redux Robotics, and various family/friends, finally got a good match schedule.
And boy, did we capitalize on it.
One of the biggest curses we suffered from in-season was that our robot would seemingly operate on a 5 match cycle. It would work great for 5 matches, then eat it for 5 matches, then work again for yet another 5 matches before breaking again.
However, by comparison, match reliability at Tidal was a nearly spotless record:
Mechanically, our robot was more or less what we ran at Champs, except we ran TPU stub rollers on our intake and we had rewired our robot for Systemcore. A lot of teams were struggling with reliability at Tidal and based on my limited sampling of asking around, a lot of it seemed to boil down to wiring issues.
And the TPU stubs held up great, even under some really violent interactions and impacts to the intake. We actually disassembled our rollers at the end of the first day, and the stubs looked mint. In fact, it was the polycarbonate tubes that had cracked (as seen above), but at least those seemed to degrade over time rather than spontaneously explode.
That particular back-left megatron had a habit of snapping off in like a dozen matches over the robot’s life, but this time it snapped off in a way that somehow managed to get stuck in the shooter and lathed off a chunk of the shooter wheel (that fortunately didn’t really matter). The debris, however, jammed the azimuth and the ensuing stressful repairs (involving detaching the turret shooter) ended up breaking the 3d printed turret cabling shelf, so we ran the next match with it taped down. (It only fell off in the last ~10-15 seconds at which point we had already won the match, so mission success.)
But ranking high and locking in A2 captain was one thing. It was fun to have a robot that was performing at its peak closer to 90%+ of the time.
The real fun came in playoffs.
We were finalists at Tidal Tumble yesterday as the captain of Alliance 2 in partnership with Teams 7415, 5012 and 4! Many thanks to Team 4414 and all the volunteers for putting on an awesome event, even in the face of having no power to the venue on Friday. We weren’t looking forward to calibrating our vision to allow for tags in shadow, so it was great to see that the power was back on Saturday morning.
I’m so proud of our students’ performance! They made multiple repairs between matches under tight deadlines and they didn’t give up knowing we would need to defeat the powerhouse combination of Teams 4414 and 254 to win. They gambled on a high risk elims strategy that had a non-zero chance of success, even if match13.com didn’t believe. We had rewired our power and CAN when we installed Systemcore and this was our first competition ever in which we didn’t have a single failure. Now we just need to do the same for our camera cables.
After resolving some teething issues Friday night, we had no issues with Systemcore the entire weekend. The Friday night issues were due to the last minute switch to running the 2026 FMS, which required us to set a static IP address on our Systemcore and run the 2026 DS with software updates to remap our buttons.
We decided to not go back to the CANivore, despite the current lack of CAN FD support on Systemcore. We had 13 Krakens and 3 encoders on bus 0 running at 68% utilization. That level of utilization had caused us issues several years ago before we switched to the CANivore, but it worked fine this weekend with the improvements in the hardware and stack. Given the issues reported by @waialua359 from SoCal Showdown, I’m glad that we stuck with what was working despite our reservations.
Thanks to @jdao for introducing us to the Ghost CSA mode provided with Systemcore, even if it might put him out of a job.
We had an 8 second disconnect near the end of a match. We had already used the free Human Intelligence (HI) model to isolate the issue to not locking down the Ethernet plugs into the radio to guard against vibration. It seems that some lessons need to be relearned occasionally. We took advantage of the situation to test out Ghost CSA. We told it the issue, what we thought the cause was (although not how we reached that conclusion), gave it the boot and AdvantageKit logs and it came back with the same answer, reconstructing our reasoning. I was very impressed, although the HI analysis took 10 minutes while the Expert Agent analysis took 30 minutes, which is not ideal for competition. The Apprentice Agent answer was faster, although I didn’t time it.
The AI did make one mistake in advising us that the Ethernet cable was bad because the link wasn’t running at GigE speed, however, the RIO port on the radio doesn’t support GigE. Unlike a regular AI chat session, I couldn’t find a way to report that oversight and let the AI update its conclusions. The advice to clear our logs was helpful because we had gotten complacent in relying on our automatic log clearing software and didn’t notice that it had partially broken due to the switch to Systemcore. Our battery performance during the match was exactly as expected. We had none of the issues that the AI feared. Like most high performance bots, we push our battery to the limit using carefully tuned current limits on all motors. We did later experience an issue in Finals 2 in which our battery deep-drained, likely due to a newly failed motor, which caused us to miss shots at the end of the match. We haven’t reviewed that log yet to identify the bad motor.
Centrifuge2 photosHonestly? Nothing really dramatic changed. We found a couple wiring issues, replaced a camera, tested some new auto paths at 3309’s field only to have them crash into walls and only at the very end of the meeting did we figure out the ambient lighting had changed enough that AprilTags were no longer detecting. Oops.
I guess there’s always Tidal practice matches to try new autos.
It’s been an interesting experience trying to take on a greater role mentoring software; because I traditionally haven’t really been as involved in it, and I’m trying my best to engage the students in the process, but I still feel like I have plenty to learn on how to teach.
Couchy Champs was really stacked this year, huh?
We did get an offer on short notice to be the first backup team if a SoCal Showdown team declined in the 11th hour. Fortunately for all the attendeees, nobody did, and I’m utimately happy that all 42 teams were able to compete, even though I’m also disappointed that we didn’t go. I would like to thank 4201 for the offer anyway.
During the Sunday meeting, as we were doing some robot maintenance, we watched SoCal Showdown, and cheered on those we knew competing that day. A lot of teams seemed to have issues during that competition but a lot of said issues seemed to stem from many teams bringing up brand new robots for SoCal and other issues typical of teams in offseason; many of them eventually found their groove and were eventually able to score at or above in-season peak performance. You can see this happen at comps like Chezy too.
And my stat isn’t “stupid!”
– Bald Canadian man with strong basketball opinions
One of my areas of interest into further research is trying to create good picklist information fast so our team can get more sleep. SoCal was a bit of a worst case scenario; the performance of teams often varied wildly during qualification matches and some of the very last matches had major ranking impacts.
One approach I tried was to examine every team’s last qualification match of Day 1 to pre-seed an impression of the team. You can then compare it against their baseline performance from other qualification matches to see if it’s unusually high or low, and try to find explanations for this.
It did end up creating a reasonable pick list for day 1, but I think you’d likely have to compare it with other information from day 2 to get a fully cooked picklist (e.g. to recognize that 687 was the ~3rd overall best bot there scoring-wise).
I also found that metrics that incorporate data from previous events like EPA or xP don’t really work super well when, say, 2102 is running a new corner turret without the turret and thus can’t score balls. I found (c)OPR to be a more useful metric as a result. I don’t think they make sense as gospel so much as guiding buckets for team performance.
Clockwork gotta get to hightide numbers so we can [runback OC at Beach Blitz] – @JohnFogarty
All things considered, despite how vocal our LM may be about Systemcore documentation and 2027 changes, I’m pretty happy that we were able to get our robot quickly to a level of functionality comparable to its Champs performance, and I’m excited to show it off at the offseasons we are attending.
We got it to jog. Yay!
It didn’t turn on the first time, but we eventually figured out the e-stop line had came loose and tightening up the wire harness made it boot up and recognize in LightBurn fine. Next step is to give it distilled water and all that and try cutting parts, but that’s a later problem.
Remember this thing?
It works great, at least for connecting to the robot. I can stand anywhere at 3476’s and still be able to pull up the PhotonVision web UI.
The internet took more work, however. Our original Tello sim card seemed to basically be throttling us to like, 1 kilobyte per second. It seems likely that the Tello was metering us as hotspot data and throttling us after 10 GB of data usage. I have now installed a card from Computers 4 People, a nonprofit focused on cheap internet access whose SIM cards are much more intended to live in routers.
The QCS priority is supposedly worse but the internet was good enough to watch SoCal off of, and the throttle threshold is apparently closer to 100 GB and only kicks in on congested networks.
I do want to investigate in-kind sponsorship potential in this direction though. It would be nice to have a nice data plan for ourselves.
While the SystemcoreTesting repo was quite helpful, I did personally find the information a little scattered of things like:
A lot of information ended up being scattered between various GitHub issues in various projects, links to initial GitHub documents, releases pages, Discord channels, Slacks, and DMs. Figuring out how to use Tunables for example required:
We did figure it out, but this sort of easter egg hunt isn’t ideal – e.g. I had assumed alpha 7 had changed the coordinate system but it turns out that hadn’t actually happened (yet.)
As someone who gets paid to work with something with a lot of moving parts, this stuff is really hard to coordinate. I do appreciate all the support that I got hunting all this stuff down, and the hard work of many volunteers as we work towards 2027 is admirable.
I do think this would’ve benefited from a more central editable wiki to put everything in one place. As many of us like to say, Discord and GitHub are not substitutes for documentation.
If anything, organizing this sort of stuff, along with maintenance of critical software like WPILib could (and perhaps should) be a full-time paid job for someone.
I salute every CSA who chooses to take on 2027 events, because you guys are really brave.

I am a little worried about Systemcore support/backup unit availability at some of the later offseasons. 10% of the teams at Tidal and 40% of the teams at Beach Blitz are at time of writing running beta Systemcores.
While I think the likelihood is really really low (much like it was for the roboRIO), it would be unfortunate if a team had to panic-rewire their robot at the last second because a Systemcore got impaled.
Sometimes, things happen that no reasonable vendor can defend against.

Despite us wiring 13 Krakens into bus 0 and 5 more devices into bus 1 and absolutely none of this running at CAN-FD speeds we didn’t really see issues stemming from bus contention or from the alpha units having only 2 SPI buses used for CAN.
I think many of our performance issues software-wise stemmed from how marginal our CPU budget often was on the roboRIO compared to the Systemcore platform.
Then again, our drivetrain template only configures the relevant drivetrain signals to 100 Hz in a Canivore-less config, so perhaps we should try what happens when we crank this up. Our robot still largely scoring fine is more a function of how vision-dependent our robot’s software strategy is.
I think it’s easy for FRC teams to assume certain taglines/slogans/memes thrown around will be the problem, even when they may not be. Another local turret team ran a dedicated Canivore for their turret control in particular for azimuth, but when we faced a similar issue we ended up applying a feedforward to our azimuth and that seemed to work. Sometimes the problem isn’t pure control update speed as much as physics.
Our students would never have fallen into this trap of changing the behavior of a fundamental method and silently causing endless breakage and grief because we teach them to specify their units. Seriously, if you’re going to go to the effort to put “Time” in a method name, it’s infinitely better to use those 4 precious characters for “usec” or “nsec” instead. After all, they are still in the time domain and they are clear what unit of time they reference. Even with the astronomical cost of memory these days, I’d still accept students being even more verbose and using “microSec” or “nanoSec”. (I actually did have to shorten the variable names in my first programs to fit in memory and my school was charged by the number of bytes used!)
Centrifuge5 photosAfter a couple weeks of meetings and concerted effort, we have finally gotten our robot match-ready[1] with a Systemcore (alpha) unit installed. This was an adventure in and of itself.
We rewired much of the robot to take advantage of the five buses the Systemcore has, as well as give newer students hands-on experience wiring a robot after we had just taught them all to crimp Molex-SL.
The general distribution of buses was as follows:
Actually executing the wiring came with its own trials and tribulations, and we ran into the rather annoying fact that we didn’t have clear references/planning on which devices were supposed to go to which bus and thus how they should be wired. A common mistake made was wiring devices into the wrong bus, and then the software not finding the devices on the buses software expected. Maybe Mr. DeYoe was right.
(Although software did initially mix up CANPort.CAN_S1 and CANPort.CAN_D1…while I understand these names correspond to the actual socketcan names they do have a small edit distance…)
Okay, I don’t think we’re gonna CAD our wires, but the idea did get thrown around about laser-cutting a wooden bellypan ~build week 1 just to get started on how the robot should/will be wired.
Trying to come up with the wiring plan as we wire the robot has become a rather apparent timesink/schedule hazard as we strive to build better robots, and the 2025 and 2026 seasons have brought this to the forefront. Here’s hoping next year’s wiring goes fast.
Using the alpha Systemcore with Redux devices revealed some issues in the published alpha7 vendor dependency. The subsequent feedback from the team was then used to publish a patched vendor dependency.
If only the vendor had simply not written buggy code! I should go hunt down whoever wrote that code and give them a piece of my mind. Don’t they know that you shouldn’t reference object members in Java before they are initialized?
PV now requires Debian 13. The upstream Orange Pi 6+ image is only Debian 12. Fortunately Armbian does have a Orange Pi 6 build with Debian 13, so we just switched to that. Should’ve remembered to properly export the cal data instead of trying to copy-paste the photonvision_config file only to find that camera cals didn’t remap on the new OS install, but it is how it is.
Hundreds of MRCal captures later (and the rediscovery of "you need camera calibration in order to run them in 3D mode) and we had three cameras up again and got poses. (The fourth was found to need a re-crimp we didn’t have time to do at the practice field, as the wiring was found to be flaky when jiggled.)
WPILib alpha 7 did in fact document the units switch in the release notes, but who reads those? Admittedly, we probably should’ve skimmed our own repo for existing micros->seconds units conversions (e.g. finding / 1e6 in the repo).
After getting PhotonVision to report sane poses to the robot, we found that the global pose estimator wasn’t actually taking them in.
At first, the thought was that the vision poses/timings fed into the VisionConsumer consumer = drive::addVisionMeasurement were wrong.
xyStdDev = avgTagDistance * avgTagDistance;
thetaStdDev = avgTagDistance * avgTagDistance;
consumer.accept(
disambiguatedRobotPose.toPose2d(),
observation.timestamp(),
VecBuilder.fill(
baseStdDev.get() * xyStdDev * singleTagStdDev.get(),
baseStdDev.get() * xyStdDev * singleTagStdDev.get(),
baseStdDev.get() * thetaStdDev * singleTagStdDev.get()));
if (Constants.VisionGlobalPose.enableVerbosePoseLogging) {
Logger.recordOutput(
"VisionGlobalPose/Camera" + Integer.toString(cameraIndex) + "/StdDev/XY",
baseStdDev.get() * xyStdDev * singleTagStdDev.get());
Logger.recordOutput(
"VisionGlobalPose/Camera" + Integer.toString(cameraIndex) + "/StdDev/Theta",
baseStdDev.get() * thetaStdDev * singleTagStdDev.get());
}
However, staring at the timestamps (all in seconds and seemingly synced to Timer.getMonotonicTimestamp()) and the robot poses (sane), the next step was to try and be like, “well, let’s look at what consumer.accept actually does.”
My favorite keybind in VSCode is F12 (Go to definition/Find usages). You place your cursor over a variable or type and press F12 to find its definition. Pressing it more times on the same variable or type finds its usages.
Everyone should learn to use it. I use it every day at work and at home.
Applying F12 to consumer leads to its definition as derived from PoseEstimator.addVisionMeasurement.
public void addVisionMeasurement(
Pose2d visionRobotPose, double timestamp, Matrix<N3, N1> visionMeasurementStdDevs) {
setVisionMeasurementStdDevs(visionMeasurementStdDevs);
addVisionMeasurement(visionRobotPose, timestamp);
}
The first call mostly just updates some state and doesn’t really have control flow. The second call however has some early returns based on if the pose estimator doesn’t like the timestamp. Applying the F12 key to addVisionMeasurement, we find:
// Step 0: If this measurement is old enough to be outside the pose buffer's timespan, skip.
if (m_odometryPoseBuffer.getInternalBuffer().isEmpty()
|| m_odometryPoseBuffer.getInternalBuffer().lastKey() - BUFFER_DURATION > timestamp) {
return;
}
// Step 1: Clean up any old entries
cleanUpVisionUpdates();
// Step 2: Get the pose measured by odometry at the moment the vision measurement was made.
var odometrySample = m_odometryPoseBuffer.getSample(timestamp);
if (odometrySample.isEmpty()) {
return;
}
// Step 3: Get the vision-compensated pose estimate at the moment the vision measurement was
// made.
var visionSample = sampleAt(timestamp);
if (visionSample.isEmpty()) {
return;
}
Well, if we already know that the vision timestamps are sane, then it’s likely some other input into the PoseEstimator is causing bad behavior. Going back up to where the poseEstimator was instantiated and using the F12 key to find usages, we see that it’s also getting updates from the drivetrain:
// Apply update
poseEstimator.updateWithTime(sampleTimestamps[i], rawGyroRotation, modulePositions);
Given that much of the vision sample rejection branches in PoseEstimator comes from out-of-range timestamps, we should probably also look at the timestamps coming from the drivetrain, as that may be what PoseEstimator may be comparing incoming vision timestamps to.
Hmm…where does sampleTimestamps come from? Let’s apply more F12, shall we?
// Update odometry
double[] sampleTimestamps =
modules[0].getOdometryTimestamps(); // All signals are sampled together
/** Returns the timestamps of the samples received this cycle. */
public double[] getOdometryTimestamps() {
return inputs.odometryTimestamps;
}
Okay, well this is the Module.java file and the inputs are probably gonna come from ModuleIOTalonFX.java on a real robot (as this is AdvantageKit), so let’s find how inputs.odometryTimestamps gets populated in there:
// Update odometry inputs
inputs.odometryTimestamps =
timestampQueue.stream().mapToDouble((Double value) -> value).toArray();
This is just a fancy way of taking all the elements out of timestampQueue and then converting it into an array of doubles. But what puts data into the queue? Time for more F12, this time on timestampQueue.
// Create timestamp queue
timestampQueue = PhoenixOdometryThread.getInstance().makeTimestampQueue();
/** Returns a new queue that returns timestamp values for each sample. */
public Queue<Double> makeTimestampQueue() {
Queue<Double> queue = new ArrayBlockingQueue<>(20);
Drive.odometryLock.lock();
try {
timestampQueues.add(queue);
} finally {
Drive.odometryLock.unlock();
}
return queue;
}
So it gets added to a list of queues that get updated. What updates the list of queues? Let’s mash F12 more.
// Save new data to queues
Drive.odometryLock.lock();
try {
// Sample timestamp is current FPGA time minus average CAN latency
// Default timestamps from Phoenix are NOT compatible with
// FPGA timestamps, this solution is imperfect but close
double timestamp = RobotController.getTime() / 1e6;
double totalLatency = 0.0;
for (BaseStatusSignal signal : phoenixSignals) {
totalLatency += signal.getTimestamp().getLatency();
}
if (phoenixSignals.length > 0) {
timestamp -= totalLatency / phoenixSignals.length;
}
// Add new samples to queues
for (int i = 0; i < phoenixSignals.length; i++) {
phoenixQueues.get(i).offer(phoenixSignals[i].getValueAsDouble());
}
for (int i = 0; i < genericSignals.size(); i++) {
genericQueues.get(i).offer(genericSignals.get(i).getAsDouble());
}
for (int i = 0; i < timestampQueues.size(); i++) {
timestampQueues.get(i).offer(timestamp);
}
} finally {
Drive.odometryLock.unlock();
}
}
I see. How’s the timestamp computed?..oh.
// Sample timestamp is current FPGA time minus average CAN latency
// Default timestamps from Phoenix are NOT compatible with
// FPGA timestamps, this solution is imperfect but close
double timestamp = RobotController.getTime() / 1e6;
Well, there’s your problem. RobotController.getTime() used to be in microseconds and is now in nanoseconds.
Changing that 1e6 to a 1e9 instantly made the robot able to shoot on the move again, and the video at the start of this post was from the first deploy after this change.
Don’t you love it when you’re hungry and thirsty in scorching 100 deg F weather and the van needs a jumpstart?
We ended up being short on time at Code Orange, so we didn’t have a ton of time to practice or tune the bot. We were able to test that auto routines still worked but didn’t quite figure out why our autonomous tunable chooser didn’t seem to push the options to NetworkTables (way too easy to do with Tunables unless you use the API specifically to the docs)
But as is, the robot is more or less ready to run for competition. We still need to fix one of the cameras, do some final wiring cleanup, and figure out what’s up with the auto selector and make some tweaked paths to account for offseason rules changes.
But we’re in a pretty good spot all things considered. We should probably read more of the new docs about Tunables first.
note that I didn’t say “fully functional”, but it’s pretty close ↩︎
Thanks. It’s gone in our 2027 code.
We are Clockwork, FRC Team 4322, Venturing Crew 1701, centered in the city of Orange, CA. We first started as a small team in 2011, but have now grown like oranges to influence our community.
Meetings, virtual sessions, local events, and competition dates, merged from the team's calendars and The Blue Alliance.
FRC 4322 Clockwork
Saturday, January 9, 2027 · (noon Eastern)
The 2027 game, BIOCORE™ presented by Haas, is revealed in FIRST’s kickoff broadcast.
Part of the FIRST® CANOPY™ season.
Powered by The Blue Alliance
Your color theme, Weather sky and team colors follow you to every device you sign in on. Your location is never synced.
A short list of changes when you visit after an update, until you choose "Don't show me again" for that update. Turn this off to stop the pop-up for every update; it is always in your notifications.
Off opens The Blue Alliance's normal site. Saved in this browser.