001/************************ PROJECT ZIXI ************************/ 002/* Copyright (c) 2026 StuyPulse Robotics. All rights reserved.*/ 003/* This work is licensed under the terms of the MIT license. */ 004/**************************************************************/ 005package com.stuypulse.robot.subsystems.swerve; 006 007import org.wpilib.system.Timer; 008import org.wpilib.units.measure.Angle; 009 010import com.ctre.phoenix6.BaseStatusSignal; 011import com.ctre.phoenix6.StatusSignal; 012 013import java.util.ArrayList; 014import java.util.List; 015import java.util.Queue; 016import java.util.concurrent.ArrayBlockingQueue; 017import java.util.concurrent.locks.Lock; 018import java.util.concurrent.locks.ReentrantLock; 019import java.util.function.DoubleSupplier; 020 021/** 022 * Provides an interface for asynchronously reading high-frequency measurements to a set of queues. 023 * 024 * <p>This version is intended for Phoenix 6 devices on both the RIO and CANivore buses. When using 025 * a CANivore, the thread uses the "waitForAll" blocking method to enable more consistent sampling. 026 * This also allows Phoenix Pro users to benefit from lower latency between devices using CANivore 027 * time synchronization. 028 */ 029public class PhoenixOdometryThread extends Thread { 030 private final Lock signalsLock = 031 new ReentrantLock(); // Prevents conflicts when registering signals 032 private final List<BaseStatusSignal> phoenixSignals = new ArrayList<>(); 033 private final List<DoubleSupplier> genericSignals = new ArrayList<>(); 034 private final List<Queue<Double>> phoenixQueues = new ArrayList<>(); 035 private final List<Queue<Double>> genericQueues = new ArrayList<>(); 036 private final List<Queue<Double>> timestampQueues = new ArrayList<>(); 037 038 private static boolean isCANFD = TunerConstants.kCANBus.isNetworkFD(); 039 private static PhoenixOdometryThread instance = null; 040 041 public static PhoenixOdometryThread getInstance() { 042 if (instance == null) { 043 instance = new PhoenixOdometryThread(); 044 } 045 return instance; 046 } 047 048 private PhoenixOdometryThread() { 049 setName("PhoenixOdometryThread"); 050 setDaemon(true); 051 } 052 053 @Override 054 public void start() { 055 if (timestampQueues.size() > 0) { 056 super.start(); 057 } 058 } 059 060 /** Registers a Phoenix signal to be read from the thread. */ 061 public Queue<Double> registerSignal(StatusSignal<Angle> signal) { 062 Queue<Double> queue = new ArrayBlockingQueue<>(20); 063 signalsLock.lock(); 064 Swerve.odometryLock.lock(); 065 try { 066 phoenixSignals.add(signal); 067 phoenixQueues.add(queue); 068 } finally { 069 signalsLock.unlock(); 070 Swerve.odometryLock.unlock(); 071 } 072 return queue; 073 } 074 075 /** Registers a generic signal to be read from the thread. */ 076 public Queue<Double> registerSignal(DoubleSupplier signal) { 077 Queue<Double> queue = new ArrayBlockingQueue<>(20); 078 signalsLock.lock(); 079 Swerve.odometryLock.lock(); 080 try { 081 genericSignals.add(signal); 082 genericQueues.add(queue); 083 } finally { 084 signalsLock.unlock(); 085 Swerve.odometryLock.unlock(); 086 } 087 return queue; 088 } 089 090 /** Returns a new queue that returns timestamp values for each sample. */ 091 public Queue<Double> makeTimestampQueue() { 092 Queue<Double> queue = new ArrayBlockingQueue<>(20); 093 Swerve.odometryLock.lock(); 094 try { 095 timestampQueues.add(queue); 096 } finally { 097 Swerve.odometryLock.unlock(); 098 } 099 return queue; 100 } 101 102 @Override 103 public void run() { 104 while (true) { 105 // Wait for updates from all signals 106 signalsLock.lock(); 107 try { 108 if (isCANFD && phoenixSignals.size() > 0) { 109 BaseStatusSignal.waitForAll(2.0 / Swerve.ODOMETRY_FREQUENCY, phoenixSignals); 110 } else { 111 // "waitForAll" does not support blocking on multiple signals with a bus 112 // that is not CAN FD, regardless of Pro licensing. No reasoning for this 113 // behavior is provided by the documentation. 114 Thread.sleep((long) (1000.0 / Swerve.ODOMETRY_FREQUENCY)); 115 if (phoenixSignals.size() > 0) BaseStatusSignal.refreshAll(phoenixSignals); 116 } 117 } catch (InterruptedException e) { 118 e.printStackTrace(); 119 } finally { 120 signalsLock.unlock(); 121 } 122 123 // Save new data to queues 124 Swerve.odometryLock.lock(); 125 try { 126 // Sample timestamp is current FPGA time minus average CAN latency 127 // Default timestamps from Phoenix are NOT compatible with 128 // FPGA timestamps, this solution is imperfect but close 129 double timestamp = Timer.getMonotonicTimestamp(); 130 double totalLatency = 0.0; 131 for (BaseStatusSignal signal : phoenixSignals) { 132 totalLatency += signal.getTimestamp().getLatency(); 133 } 134 if (phoenixSignals.size() > 0) { 135 timestamp -= totalLatency / phoenixSignals.size(); 136 } 137 138 // Add new samples to queues 139 for (int i = 0; i < phoenixSignals.size(); i++) { 140 phoenixQueues.get(i).offer(phoenixSignals.get(i).getValueAsDouble()); 141 } 142 for (int i = 0; i < genericSignals.size(); i++) { 143 genericQueues.get(i).offer(genericSignals.get(i).getAsDouble()); 144 } 145 for (int i = 0; i < timestampQueues.size(); i++) { 146 timestampQueues.get(i).offer(timestamp); 147 } 148 } finally { 149 Swerve.odometryLock.unlock(); 150 } 151 } 152 } 153}