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}