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}