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 java.io.IOException;
010import java.util.Optional;
011import java.util.function.DoubleFunction;
012
013import com.ctre.phoenix6.StatusSignal.SignalMeasurement;
014import com.ctre.phoenix6.jni.HootReplayJNI;
015
016import edu.wpi.first.units.Measure;
017import edu.wpi.first.units.Unit;
018import edu.wpi.first.util.protobuf.Protobuf;
019import edu.wpi.first.util.protobuf.ProtobufBuffer;
020import edu.wpi.first.util.struct.Struct;
021import edu.wpi.first.util.struct.StructBuffer;
022
023/**
024 * Static class for controlling Phoenix 6 hoot log replay.
025 * <p>
026 * This replays all signals in the given hoot log in simulation. Hoot logs can
027 * be created by a robot program using {@link SignalLogger}. Only one hoot log,
028 * corresponding to one CAN bus, may be replayed at a time.
029 * <p>
030 * The signal logger always runs while replay is running. All custom signals written
031 * during replay will be automatically placed under {@code hoot_replay/}. Additionally,
032 * the log will contain all status signals and custom signals from the original log.
033 * <p>
034 * During replay, all transmits from the robot program are ignored. This includes
035 * features such as control requests, configs, and setting signal update frequency.
036 * Additionally, Tuner X is not functional during log replay.
037 * <p>
038 * To use Hoot Replay, call {@link #loadFile(String)} before any devices are constructed
039 * to load a hoot file and start replay. Alternatively, the {@link CANBus#CANBus(String, String)}
040 * constructor can be used when constructing devices.
041 * <p>
042 * After devices are constructed, Hoot Replay can be controlled using {@link #play()},
043 * {@link #pause()}, {@link #stop()}, and {@link #restart()}. Additionally, Hoot Replay
044 * supports {@link #stepTiming(double)} while paused. The current file can be closed using
045 * {@link #closeFile()}, after which a new file may be loaded.
046 */
047public class HootReplay {
048    /**
049     * Loads the given file and starts signal log replay. Only one hoot
050     * log, corresponding to one CAN bus, may be replayed at a time.
051     * <p>
052     * This must be called before constructing any devices or checking
053     * CAN bus status. The {@link CANBus#CANBus(String, String)}
054     * constructor can be used when constructing devices to guarantee
055     * that this API is called first.
056     * <p>
057     * When using relative paths, the file path is typically relative
058     * to the top-level folder of the robot project.
059     * <p>
060     * This API is blocking on the file read.
061     *
062     * @param filepath Path and name of the hoot file to load
063     * @return Status of opening and reading the file for replay
064     * @throws IllegalArgumentException The file is invalid, unlicensed, or
065     *                                  targets a different version of Phoenix 6
066     */
067    public static StatusCode loadFile(String filepath) {
068        var retval = StatusCode.valueOf(HootReplayJNI.JNI_LoadFile(filepath));
069        switch (retval) {
070            case InvalidFile:
071            case UnlicensedHootLog:
072            case HootLogTooOld:
073            case HootLogTooNew:
074                throw new IllegalArgumentException(retval.getDescription());
075            default:
076                break;
077        }
078        return retval;
079    }
080
081    /**
082     * Ends the hoot log replay. This stops the replay if it is running,
083     * closes the hoot log, and clears all signals read from the file.
084     */
085    public static void closeFile() {
086        HootReplayJNI.JNI_CloseFile();
087    }
088
089    /**
090     * Gets whether a valid hoot log file is currently loaded.
091     *
092     * @return true if a valid hoot log file is loaded
093     */
094    public static boolean isFileLoaded() {
095        return HootReplayJNI.JNI_IsFileLoaded();
096    }
097
098    /**
099     * Starts or resumes the hoot log replay.
100     *
101     * @return Status of starting or resuming replay
102     */
103    public static StatusCode play() {
104        return StatusCode.valueOf(HootReplayJNI.JNI_Play());
105    }
106
107    /**
108     * Pauses the hoot log replay. This maintains the current position
109     * in the log replay so it can be resumed later.
110     *
111     * @return Status of pausing replay
112     */
113    public static StatusCode pause() {
114        return StatusCode.valueOf(HootReplayJNI.JNI_Pause());
115    }
116
117    /**
118     * Stops the hoot log replay. This resets the current position in
119     * the log replay to the start.
120     *
121     * @return Status of stopping replay
122     */
123    public static StatusCode stop() {
124        return StatusCode.valueOf(HootReplayJNI.JNI_Stop());
125    }
126
127    /**
128     * Restarts the hoot log replay from the start of the log.
129     * This is equivalent to calling {@link #stop} followed by
130     * {@link #play}.
131     *
132     * @return Status of restarting replay
133     */
134    public static StatusCode restart() {
135        var retval = stop();
136        if (retval.isOK()) {
137            retval = play();
138        }
139        return retval;
140    }
141
142    /**
143     * Gets whether hoot log replay is actively playing.
144     * <p>
145     * This API will return true in programs that do not support
146     * replay, making it safe to call without first checking if
147     * the program supports replay.
148     *
149     * @return true if replay is playing back signals
150     */
151    public static boolean isPlaying() {
152        return waitForPlaying(0.0);
153    }
154
155    /**
156     * Waits until hoot log replay is actively playing.
157     * <p>
158     * This API will immediately return true in programs that do
159     * not support replay, making it safe to call without first
160     * checking if the program supports replay.
161     * <p>
162     * Since this can block the calling thread, this should not
163     * be called with a non-zero timeout on the main thread.
164     * <p>
165     * This can also be used with a timeout of 0 to perform
166     * a non-blocking check, which is equivalent to {@link #isPlaying}.
167     *
168     * @param timeoutSeconds Max time to wait for replay to start playing
169     * @return true if replay is playing back signals
170     */
171    public static boolean waitForPlaying(double timeoutSeconds) {
172        return HootReplayJNI.JNI_IsPlaying(timeoutSeconds);
173    }
174
175    /**
176     * Gets whether hoot log replay has reached the end of the log.
177     *
178     * @return true if replay has reached the end of the log, or
179     *         if no log is currently loaded
180     */
181    public static boolean isFinished() {
182        return HootReplayJNI.JNI_IsFinished();
183    }
184
185    /**
186     * Sets the speed of the hoot log replay. A speed of 1.0 corresponds to
187     * replaying the file in real time, and larger values increase the speed.
188     *
189     * <ul>
190     *   <li> <b>Minimum Value:</b> 0.01
191     *   <li> <b>Maximum Value:</b> 100.0
192     *   <li> <b>Default Value:</b> 1.0
193     * </ul>
194     *
195     * @param speed Speed of the hoot log replay
196     */
197    public static void setSpeed(double speed) {
198        HootReplayJNI.JNI_SetSpeed(speed);
199    }
200
201    /**
202     * Advances the hoot log replay time by the given value. Replay must
203     * be paused or stopped before advancing its time.
204     *
205     * @param stepTimeSeconds The amount of time to advance
206     * @return Status of advancing the replay time
207     */
208    public static StatusCode stepTiming(double stepTimeSeconds) {
209        return StatusCode.valueOf(HootReplayJNI.JNI_StepTiming(stepTimeSeconds));
210    }
211
212    /**
213     * Gets a schema-serialized user signal.
214     *
215     * Users can call {@link #getStruct}, {@link #getStructArray}, and
216     * {@link #getProtobuf} to directly get schema values instead.
217     *
218     * @param name Name of the signal
219     * @param type Type of the schema, such as struct or protobuf
220     * @return Structure with all information about the signal
221     */
222    public static SignalMeasurement<byte[]> getSchemaValue(String name, HootSchemaType type) {
223        var retval = new SignalMeasurement<byte[]>();
224        retval.name = name;
225
226        var jni = new HootReplayJNI();
227        retval.status = StatusCode.valueOf(jni.JNI_GetSchemaValue(name, type.value));
228        if (retval.status.isOK()) {
229            retval.value = (byte[])jni.data;
230            retval.timestamp = jni.timestampSec;
231            retval.units = jni.units;
232        } else {
233            retval.value = new byte[0];
234        }
235        return retval;
236    }
237
238    /**
239     * Gets a WPILib Struct user signal.
240     *
241     * @param name Name of the signal
242     * @param struct Struct deserialization implementation
243     * @return Structure with all information about the signal
244     */
245    public static <T> SignalMeasurement<Optional<T>> getStruct(String name, Struct<T> struct) {
246        var retval = new SignalMeasurement<Optional<T>>();
247        retval.name = name;
248
249        var jni = new HootReplayJNI();
250        retval.status = StatusCode.valueOf(jni.JNI_GetSchemaValue(name, HootSchemaType.Struct.value));
251        if (retval.status.isOK()) {
252            final byte[] data = (byte[])jni.data;
253            if (data.length == struct.getSize()) {
254                final StructBuffer<T> buf = StructBuffer.create(struct);
255                retval.value = Optional.of(buf.read((byte[])jni.data));
256                retval.timestamp = jni.timestampSec;
257                retval.units = jni.units;
258            } else {
259                retval.value = Optional.empty();
260                retval.status = StatusCode.InvalidParamValue;
261            }
262        } else {
263            retval.value = Optional.empty();
264        }
265        return retval;
266    }
267
268    /**
269     * Gets a WPILib Struct array user signal.
270     *
271     * @param name Name of the signal
272     * @param struct Struct deserialization implementation
273     * @return Structure with all information about the signal
274     */
275    public static <T> SignalMeasurement<Optional<T[]>> getStructArray(String name, Struct<T> struct) {
276        var retval = new SignalMeasurement<Optional<T[]>>();
277        retval.name = name;
278
279        var jni = new HootReplayJNI();
280        retval.status = StatusCode.valueOf(jni.JNI_GetSchemaValue(name, HootSchemaType.Struct.value));
281        if (retval.status.isOK()) {
282            final byte[] data = (byte[])jni.data;
283            if (data.length % struct.getSize() == 0) {
284                final StructBuffer<T> buf = StructBuffer.create(struct);
285                retval.value = Optional.of(buf.readArray((byte[])jni.data));
286                retval.timestamp = jni.timestampSec;
287                retval.units = jni.units;
288            } else {
289                retval.value = Optional.empty();
290                retval.status = StatusCode.InvalidParamValue;
291            }
292        } else {
293            retval.value = Optional.empty();
294        }
295        return retval;
296    }
297
298    /**
299     * Gets a Protobuf user signal.
300     *
301     * @param name Name of the signal
302     * @param proto Protobuf deserialization implementation
303     * @return Structure with all information about the signal
304     */
305    public static <T> SignalMeasurement<Optional<T>> getProtobuf(String name, Protobuf<T, ?> proto) {
306        var retval = new SignalMeasurement<Optional<T>>();
307        retval.name = name;
308
309        var jni = new HootReplayJNI();
310        retval.status = StatusCode.valueOf(jni.JNI_GetSchemaValue(name, HootSchemaType.Protobuf.value));
311        if (retval.status.isOK()) {
312            final ProtobufBuffer<T, ?> buf = ProtobufBuffer.create(proto);
313            try {
314                retval.value = Optional.of(buf.read((byte[])jni.data));
315                retval.timestamp = jni.timestampSec;
316                retval.units = jni.units;
317            } catch (IOException e) {
318                retval.value = Optional.empty();
319                retval.status = StatusCode.InvalidParamValue;
320            }
321        } else {
322            retval.value = null;
323        }
324        return retval;
325    }
326
327    /**
328     * Gets a raw-bytes user signal.
329     *
330     * @param name Name of the signal
331     * @return Structure with all information about the signal
332     */
333    public static SignalMeasurement<byte[]> getRaw(String name) {
334        var retval = new SignalMeasurement<byte[]>();
335        retval.name = name;
336
337        var jni = new HootReplayJNI();
338        retval.status = StatusCode.valueOf(jni.JNI_GetRaw(name));
339        if (retval.status.isOK()) {
340            retval.value = (byte[])jni.data;
341            retval.timestamp = jni.timestampSec;
342            retval.units = jni.units;
343        } else {
344            retval.value = new byte[0];
345        }
346        return retval;
347    }
348
349    /**
350     * Gets a boolean user signal.
351     *
352     * @param name Name of the signal
353     * @return Structure with all information about the signal
354     */
355    public static SignalMeasurement<Boolean> getBoolean(String name) {
356        var retval = new SignalMeasurement<Boolean>();
357        retval.name = name;
358
359        var jni = new HootReplayJNI();
360        retval.status = StatusCode.valueOf(jni.JNI_GetBoolean(name));
361        if (retval.status.isOK()) {
362            retval.value = (Boolean)jni.data;
363            retval.timestamp = jni.timestampSec;
364            retval.units = jni.units;
365        } else {
366            retval.value = false;
367        }
368        return retval;
369    }
370
371    /**
372     * Gets an integer user signal.
373     *
374     * @param name Name of the signal
375     * @return Structure with all information about the signal
376     */
377    public static SignalMeasurement<Long> getInteger(String name) {
378        var retval = new SignalMeasurement<Long>();
379        retval.name = name;
380
381        var jni = new HootReplayJNI();
382        retval.status = StatusCode.valueOf(jni.JNI_GetInteger(name));
383        if (retval.status.isOK()) {
384            retval.value = (Long)jni.data;
385            retval.timestamp = jni.timestampSec;
386            retval.units = jni.units;
387        } else {
388            retval.value = 0L;
389        }
390        return retval;
391    }
392
393    /**
394     * Gets a float user signal.
395     *
396     * @param name Name of the signal
397     * @return Structure with all information about the signal
398     */
399    public static SignalMeasurement<Float> getFloat(String name) {
400        var retval = new SignalMeasurement<Float>();
401        retval.name = name;
402
403        var jni = new HootReplayJNI();
404        retval.status = StatusCode.valueOf(jni.JNI_GetFloat(name));
405        if (retval.status.isOK()) {
406            retval.value = (Float)jni.data;
407            retval.timestamp = jni.timestampSec;
408            retval.units = jni.units;
409        } else {
410            retval.value = 0.0f;
411        }
412        return retval;
413    }
414
415    /**
416     * Gets a double user signal.
417     *
418     * @param name Name of the signal
419     * @return Structure with all information about the signal
420     */
421    public static SignalMeasurement<Double> getDouble(String name) {
422        var retval = new SignalMeasurement<Double>();
423        retval.name = name;
424
425        var jni = new HootReplayJNI();
426        retval.status = StatusCode.valueOf(jni.JNI_GetDouble(name));
427        if (retval.status.isOK()) {
428            retval.value = (Double)jni.data;
429            retval.timestamp = jni.timestampSec;
430            retval.units = jni.units;
431        } else {
432            retval.value = 0.0;
433        }
434        return retval;
435    }
436
437    /**
438     * Gets a string user signal.
439     *
440     * @param name Name of the signal
441     * @return Structure with all information about the signal
442     */
443    public static SignalMeasurement<String> getString(String name) {
444        var retval = new SignalMeasurement<String>();
445        retval.name = name;
446
447        var jni = new HootReplayJNI();
448        retval.status = StatusCode.valueOf(jni.JNI_GetString(name));
449        if (retval.status.isOK()) {
450            retval.value = (String)jni.data;
451            retval.timestamp = jni.timestampSec;
452            retval.units = jni.units;
453        } else {
454            retval.value = "";
455        }
456        return retval;
457    }
458
459    /**
460     * Gets a unit value user signal.
461     *
462     * @param name Name of the signal
463     * @param unitFactory Factory for creating the unit type, such as {@link Unit#of Rotations::of}
464     * @return Structure with all information about the signal
465     */
466    public static
467    <MEAS extends Measure<?>>
468    SignalMeasurement<MEAS> getValue(String name, DoubleFunction<MEAS> unitFactory) {
469        var retval = new SignalMeasurement<MEAS>();
470        retval.name = name;
471
472        var jni = new HootReplayJNI();
473        retval.status = StatusCode.valueOf(jni.JNI_GetDouble(name));
474        if (retval.status.isOK()) {
475            retval.value = unitFactory.apply((double)jni.data);
476            retval.timestamp = jni.timestampSec;
477            retval.units = jni.units;
478        } else {
479            retval.value = unitFactory.apply(0.0);
480        }
481        return retval;
482    }
483
484    /**
485     * Gets a boolean user signal.
486     *
487     * @param name Name of the signal
488     * @return Structure with all information about the signal
489     */
490    public static SignalMeasurement<boolean[]> getBooleanArray(String name) {
491        var retval = new SignalMeasurement<boolean[]>();
492        retval.name = name;
493
494        var jni = new HootReplayJNI();
495        retval.status = StatusCode.valueOf(jni.JNI_GetBooleanArray(name));
496        if (retval.status.isOK()) {
497            retval.value = (boolean[])jni.data;
498            retval.timestamp = jni.timestampSec;
499            retval.units = jni.units;
500        } else {
501            retval.value = new boolean[0];
502        }
503        return retval;
504    }
505
506    /**
507     * Gets a boolean array user signal.
508     *
509     * @param name Name of the signal
510     * @return Structure with all information about the signal
511     */
512    public static SignalMeasurement<long[]> getIntegerArray(String name) {
513        var retval = new SignalMeasurement<long[]>();
514        retval.name = name;
515
516        var jni = new HootReplayJNI();
517        retval.status = StatusCode.valueOf(jni.JNI_GetIntegerArray(name));
518        if (retval.status.isOK()) {
519            retval.value = (long[])jni.data;
520            retval.timestamp = jni.timestampSec;
521            retval.units = jni.units;
522        } else {
523            retval.value = new long[0];
524        }
525        return retval;
526    }
527
528    /**
529     * Gets a float array user signal.
530     *
531     * @param name Name of the signal
532     * @return Structure with all information about the signal
533     */
534    public static SignalMeasurement<float[]> getFloatArray(String name) {
535        var retval = new SignalMeasurement<float[]>();
536        retval.name = name;
537
538        var jni = new HootReplayJNI();
539        retval.status = StatusCode.valueOf(jni.JNI_GetFloatArray(name));
540        if (retval.status.isOK()) {
541            retval.value = (float[])jni.data;
542            retval.timestamp = jni.timestampSec;
543            retval.units = jni.units;
544        } else {
545            retval.value = new float[0];
546        }
547        return retval;
548    }
549
550    /**
551     * Gets a double array user signal.
552     *
553     * @param name Name of the signal
554     * @return Structure with all information about the signal
555     */
556    public static SignalMeasurement<double[]> getDoubleArray(String name) {
557        var retval = new SignalMeasurement<double[]>();
558        retval.name = name;
559
560        var jni = new HootReplayJNI();
561        retval.status = StatusCode.valueOf(jni.JNI_GetDoubleArray(name));
562        if (retval.status.isOK()) {
563            retval.value = (double[])jni.data;
564            retval.timestamp = jni.timestampSec;
565            retval.units = jni.units;
566        } else {
567            retval.value = new double[0];
568        }
569        return retval;
570    }
571
572    /**
573     * Gets a string array user signal.
574     *
575     * @param name Name of the signal
576     * @return Structure with all information about the signal
577     */
578    public static SignalMeasurement<String[]> getStringArray(String name) {
579        var retval = new SignalMeasurement<String[]>();
580        retval.name = name;
581
582        var jni = new HootReplayJNI();
583        retval.status = StatusCode.valueOf(jni.JNI_GetStringArray(name));
584        if (retval.status.isOK()) {
585            retval.value = (String[])jni.data;
586            retval.timestamp = jni.timestampSec;
587            retval.units = jni.units;
588        } else {
589            retval.value = new String[0];
590        }
591        return retval;
592    }
593}