001/*
002 * Copyright (C) Cross The Road Electronics.  All rights reserved.
003 * License information can be found in CTRE_LICENSE.txt
004 * For support and suggestions contact support@ctr-electronics.com or file
005 * an issue tracker at https://github.com/CrossTheRoadElec/Phoenix-Releases
006 */
007package com.ctre.phoenix6;
008
009import com.ctre.phoenix6.Timestamp.TimestampSource;
010import com.ctre.phoenix6.hardware.DeviceIdentifier;
011import com.ctre.phoenix6.jni.ErrorReportingJNI;
012import com.ctre.phoenix6.jni.StatusSignalJNI;
013
014/**
015 * Class that provides operations to retrieve
016 * information about a status signal.
017 */
018public abstract class BaseStatusSignal {
019    protected DeviceIdentifier deviceIdentifier;
020    protected int spn;
021    protected String units;
022    protected StatusCode error = StatusCode.StatusCodeNotInitialized;
023    protected double baseValue = 0;
024    protected AllTimestamps timestamps = new AllTimestamps();
025    protected final String signalName;
026
027    protected Runnable _reportIfOldFunc;
028
029    private double _lastTimestamp = 0.0;
030
031    protected StatusSignalJNI jni = new StatusSignalJNI();
032
033    BaseStatusSignal(DeviceIdentifier deviceIdentifier, int spn, String signalName, Runnable reportIfOldFunc) {
034        this.deviceIdentifier = deviceIdentifier;
035        this.spn = spn;
036        this.signalName = signalName;
037
038        this._reportIfOldFunc = reportIfOldFunc;
039
040        jni.network = deviceIdentifier.getNetwork();
041        jni.deviceHash = deviceIdentifier.getDeviceHash();
042        jni.spn = spn;
043
044        this.units = jni.JNI_GetUnits();
045    }
046
047    /* Constructor for an invalid BaseStatusSignal */
048    BaseStatusSignal(StatusCode error) {
049        this(new DeviceIdentifier(), 0, "Invalid", () -> {});
050        this.error = error;
051    }
052
053    protected void copyFrom(BaseStatusSignal other) {
054        this.units = other.units;
055        this.error = other.error;
056        this.baseValue = other.baseValue;
057        this.timestamps = other.timestamps;
058        // Don't copy the signal name since the user expects the original name
059    }
060
061    /**
062     * Implementation of the waitForAll API.
063     *
064     * @param location Location of the calling function
065     * @param timeoutSeconds Maximum time to wait for all the signals
066     * @param signals Signals to wait on
067     * @return Status of the wait
068     */
069    private static StatusCode waitForAllImpl(String location, double timeoutSeconds, BaseStatusSignal... signals) {
070        if (signals.length < 1) {
071            /* We don't have any signals to wait for, so return early */
072            ErrorReportingJNI.reportStatusCode(StatusCode.InvalidParamValue.value, location);
073            return StatusCode.InvalidParamValue;
074        }
075
076        String network = signals[0].deviceIdentifier.getNetwork();
077        StatusSignalJNI[] toGet = new StatusSignalJNI[signals.length];
078        for (int i = 0; i < signals.length; ++i) {
079            var sig = signals[i];
080            if (i != 0 && !sig.deviceIdentifier.getNetwork().equals(network)) {
081                // Networks don't match, return early
082                ErrorReportingJNI.reportStatusCode(StatusCode.InvalidNetwork.value, location);
083                return StatusCode.InvalidNetwork;
084            }
085            toGet[i] = sig.jni;
086        }
087
088        /* Report if any device firmware versions are too old */
089        for (var signal : signals) {
090            signal._reportIfOldFunc.run();
091        }
092
093        /* Now wait for all the signals */
094        int err = StatusSignalJNI.JNI_WaitForAll(network, timeoutSeconds, toGet);
095        for (int i = 0; i < signals.length; ++i) {
096            signals[i].error = StatusCode.valueOf(toGet[i].statusCode);
097            signals[i].baseValue = toGet[i].value;
098            signals[i].timestamps.update(toGet[i].swtimeStampSeconds, TimestampSource.System, true,
099                    toGet[i].hwtimeStampSeconds, TimestampSource.CANivore, true,
100                    toGet[i].ecutimeStampSeconds, TimestampSource.Device, toGet[i].ecutimeStampSeconds != 0.0);
101        }
102
103        /* error reporting */
104        StatusCode retval = StatusCode.valueOf(err);
105        if (false == retval.isOK()) {
106            ErrorReportingJNI.reportStatusCode(retval.value, location);
107        }
108        return StatusCode.valueOf(err);
109    }
110
111    /**
112     * Waits for new data on all provided signals up to timeout.
113     * This API is typically used with CANivore Bus signals as they will be synced using the
114     * CANivore Timesync feature and arrive simultaneously. Signals on a roboRIO bus cannot
115     * be synced and may require a significantly longer blocking call to receive all signals.
116     * <p>
117     * Note that CANivore Timesync requires Phoenix Pro.
118     * <p>
119     * This can also be used with a timeout of zero to refresh many signals at once, which
120     * is faster than calling refresh() on every signal. This is equivalent to calling {@link #refreshAll}.
121     *
122     * @param timeoutSeconds Maximum time to wait for new data in seconds.
123     *                       Pass zero to refresh all signals without blocking.
124     * @param signals        Signals to wait for new data against
125     * @return An InvalidParamValue if signals array is empty,
126     *         InvalidNetwork if signals are on different CAN bus networks,
127     *         RxTimeout if it took longer than timeoutSeconds to receive all the signals,
128     *         MultiSignalNotSupported if using the roboRIO bus with more than one signal and a non-zero timeout.
129     *         An OK status code means that all signals arrived within timeoutSeconds and they are all OK.
130     *
131     *         Any other value represents the StatusCode of the first failed signal.
132     *         Call getStatus() on each signal to determine which ones failed.
133     */
134    public static StatusCode waitForAll(double timeoutSeconds, BaseStatusSignal... signals) {
135        return waitForAllImpl("ctre.phoenix6.BaseStatusSignal.waitForAll", timeoutSeconds, signals);
136    }
137
138    /**
139     * Performs a non-blocking refresh on all provided signals.
140     * <p>
141     * This provides a performance improvement over separately calling refresh() on each signal.
142     *
143     * @param signals Signals to refresh
144     * @return An InvalidParamValue if signals array is empty,
145     *         InvalidNetwork if signals are on different CAN bus networks.
146     *         An OK status code means that all signals are OK.
147     *
148     *         Any other value represents the StatusCode of the first failed signal.
149     *         Call getStatus() on each signal to determine which ones failed.
150     */
151    public static StatusCode refreshAll(BaseStatusSignal... signals) {
152        return waitForAllImpl("ctre.phoenix6.BaseStatusSignal.refreshAll", 0, signals);
153    }
154
155    /**
156     * Performs latency compensation on signal using the signalSlope and signal's latency to determine
157     * the magnitude of compensation. The caller must refresh these StatusSignals beforehand;
158     * this function only does the math required for latency compensation.
159     * <p>
160     * <b>Important</b>: The signalSlope must be the rate of change of the signal. If it is not the latency
161     * compensation may not perform as expected.
162     * <p>
163     * This compensates for up to 300ms of latency by default.
164     * <p>
165     * <b>Example</b>:
166     * double compensatedTurns = BaseStatusSignal.getLatencyCompensatedValue(fx.getPosition(), fx.getVelocity());
167     *
168     * @param signal Signal to be latency compensated. Caller must make sure this signal is up to date
169     *               either by calling {@link StatusSignal#refresh()} or {@link StatusSignal#waitForUpdate(double)}.
170     * @param signalSlope Derivative of signal that informs compensation magnitude. Caller must make sure this
171     *                    signal is up to date either by calling {@link StatusSignal#refresh()} or
172     *                    {@link StatusSignal#waitForUpdate(double)}.
173     * @return Latency compensated value from the signal StatusSignal.
174     */
175    public static double getLatencyCompensatedValue(StatusSignal<Double> signal, StatusSignal<Double> signalSlope)
176    {
177        return getLatencyCompensatedValue(signal, signalSlope, 0.300);
178    }
179
180    /**
181     * Performs latency compensation on signal using the signalSlope and signal's latency to determine
182     * the magnitude of compensation. The caller must refresh these StatusSignals beforehand;
183     * this function only does the math required for latency compensation.
184     * <p>
185     * <b>Important</b>: The signalSlope must be the rate of change of the signal. If it is not the latency
186     * compensation may not perform as expected.
187     * <p>
188     * <b>Example</b>:
189     * double compensatedTurns = BaseStatusSignal.getLatencyCompensatedValue(fx.getPosition(), fx.getVelocity());
190     *
191     * @param signal Signal to be latency compensated. Caller must make sure this signal is up to date
192     *               either by calling {@link StatusSignal#refresh()} or {@link StatusSignal#waitForUpdate(double)}.
193     * @param signalSlope Derivative of signal that informs compensation magnitude. Caller must make sure this
194     *                    signal is up to date either by calling {@link StatusSignal#refresh()} or
195     *                    {@link StatusSignal#waitForUpdate(double)}.
196     * @param maxLatencySeconds The maximum amount of latency to compensate for in seconds. A negative or zero
197     *                          value disables the max latency cap. This is used to cap the contribution of
198     *                          latency compensation for stale signals, such as after the device has been
199     *                          disconnected from the CAN bus.
200     * @return Latency compensated value from the signal StatusSignal.
201     */
202    public static double getLatencyCompensatedValue(StatusSignal<Double> signal, StatusSignal<Double> signalSlope, double maxLatencySeconds)
203    {
204        final double nonCompensatedSignal = signal.getValue();
205        final double changeInSignal = signalSlope.getValue();
206        double latency = signal.getTimestamp().getLatency();
207        if (maxLatencySeconds > 0.0 && latency > maxLatencySeconds) {
208            latency = maxLatencySeconds;
209        }
210        return nonCompensatedSignal + (changeInSignal * latency);
211    }
212
213    /**
214     * Checks if all signals have an OK error code.
215     *
216     * @param signals Signals to check error code of
217     * @return True if all are good, false otherwise
218     */
219    public static boolean isAllGood(BaseStatusSignal... signals) {
220        for (BaseStatusSignal sig : signals) {
221            if (!sig.getStatus().isOK()) {
222                return false;
223            }
224        }
225        return true;
226    }
227
228    /**
229     * Sets the update frequency of all specified status signals to the provided common frequency.
230     * <p>
231     * A frequency of 0 Hz will turn off the signal. Otherwise, the minimum supported signal
232     * frequency is 4 Hz, and the maximum is 1000 Hz.
233     * <p>
234     * If other StatusSignals in the same status frame have been set to an update frequency,
235     * the fastest requested update frequency will be applied to the frame.
236     * <p>
237     * This will wait up to 0.050 seconds (50ms) for each signal.
238     *
239     * @param frequencyHz Rate to publish the signal in Hz.
240     * @param signals Signals to apply the update frequency to
241     * @return Status code of the first failed update frequency set call, or OK if all succeeded
242     */
243    public static StatusCode setUpdateFrequencyForAll(double frequencyHz, BaseStatusSignal... signals) {
244        StatusSignalJNI[] toSet = new StatusSignalJNI[signals.length];
245        for (int i = 0; i < signals.length; ++i) {
246            toSet[i] = signals[i].jni;
247        }
248        return StatusCode.valueOf(StatusSignalJNI.JNI_SetUpdateFrequencyForAll(frequencyHz, toSet, 0.050));
249    }
250
251    /**
252     * Sets the rate at which the device will publish this signal.
253     * <p>
254     * A frequency of 0 Hz will turn off the signal. Otherwise, the minimum supported signal
255     * frequency is 4 Hz, and the maximum is 1000 Hz.
256     * <p>
257     * If other StatusSignals in the same status frame have been set to an update frequency,
258     * the fastest requested update frequency will be applied to the frame.
259     * <p>
260     * This will wait up to 0.050 seconds (50ms) by default.
261     *
262     * @param frequencyHz Rate to publish the signal in Hz.
263     * @return Status code of setting the update frequency
264     */
265    public StatusCode setUpdateFrequency(double frequencyHz)
266    {
267        return setUpdateFrequency(frequencyHz, 0.050);
268    }
269
270    /**
271     * Sets the rate at which the device will publish this signal.
272     * <p>
273     * A frequency of 0 Hz will turn off the signal. Otherwise, the minimum supported signal
274     * frequency is 4 Hz, and the maximum is 1000 Hz.
275     * <p>
276     * If other StatusSignals in the same status frame have been set to an update frequency,
277     * the fastest requested update frequency will be applied to the frame.
278     *
279     * @param frequencyHz Rate to publish the signal in Hz.
280     * @param timeoutSeconds Maximum amount of time to wait when performing the action
281     * @return Status code of setting the update frequency
282     */
283    public abstract StatusCode setUpdateFrequency(double frequencyHz, double timeoutSeconds);
284
285    /**
286     * Gets the rate at which the device will publish this signal.
287     * <p>
288     * This is typically the last value passed into {@link #setUpdateFrequency}. The returned value
289     * may be higher if another StatusSignal in the same status frame has been set to a higher
290     * update frequency.
291     *
292     * @return Applied update frequency of the signal in Hz
293     */
294    public abstract double getAppliedUpdateFrequency();
295
296    /**
297     * Gets the name of this signal
298     *
299     * @return Name of this signal
300     */
301    public String getName() {
302        return signalName;
303    }
304
305    /**
306     * Gets the units for this signal
307     *
308     * @return String representation of units for this signal
309     */
310    public String getUnits() {
311        return units;
312    }
313
314    /**
315     * Gets the value of this signal as a double
316     *
317     * @return Value of this signal as a double instead of the generic type
318     */
319    public double getValueAsDouble() {
320        return baseValue;
321    }
322
323    /**
324     * Get all timestamps relevant for this signal
325     *
326     * @return All timestamps available for this signal
327     */
328    public AllTimestamps getAllTimestamps() {
329        return timestamps;
330    }
331
332    /**
333     * Get the best available timestamp for this signal
334     *
335     * @return Best available timestamp
336     */
337    public Timestamp getTimestamp() {
338        return timestamps.getBestTimestamp();
339    }
340
341    /**
342     * Get the error code from when we last received this signal
343     *
344     * @return Last cached Error Code
345     */
346    public StatusCode getStatus() {
347        return error;
348    }
349
350    /**
351     * Check whether the signal has been updated since the last check.
352     * <p>
353     * Note that the signal must be refreshed before calling this routine.
354     *
355     * @return true if the signal has updated since the previous call of this routine
356     */
357    public boolean hasUpdated()
358    {
359        boolean retval = false;
360        /* did we receive an update */
361        final var timestamp = getAllTimestamps().getSystemTimestamp();
362        if (timestamp.isValid()) {
363            /* if the update timestamp is new, then a new frame was sent */
364            if (_lastTimestamp != timestamp.getTime()) {
365                _lastTimestamp = timestamp.getTime();
366                retval = true;
367            }
368        }
369        return retval;
370    }
371};