001// Copyright (c) FIRST and other WPILib contributors.
002// Open Source Software; you can modify and/or share it under the terms of
003// the WPILib BSD license file in the root directory of this project.
004
005package edu.wpi.first.util.datalog;
006
007import java.nio.BufferUnderflowException;
008import java.nio.ByteBuffer;
009import java.nio.ByteOrder;
010import java.nio.DoubleBuffer;
011import java.nio.FloatBuffer;
012import java.nio.LongBuffer;
013import java.nio.charset.StandardCharsets;
014import java.util.InputMismatchException;
015
016/**
017 * A record in the data log. May represent either a control record (entry == 0) or a data record.
018 * Used only for reading (e.g. with DataLogReader).
019 */
020public class DataLogRecord {
021  private static final int kControlStart = 0;
022  private static final int kControlFinish = 1;
023  private static final int kControlSetMetadata = 2;
024
025  DataLogRecord(int entry, long timestamp, ByteBuffer data) {
026    m_entry = entry;
027    m_timestamp = timestamp;
028    m_data = data;
029    m_data.order(ByteOrder.LITTLE_ENDIAN);
030  }
031
032  /**
033   * Gets the entry ID.
034   *
035   * @return entry ID
036   */
037  public int getEntry() {
038    return m_entry;
039  }
040
041  /**
042   * Gets the record timestamp.
043   *
044   * @return Timestamp, in integer microseconds
045   */
046  public long getTimestamp() {
047    return m_timestamp;
048  }
049
050  /**
051   * Gets the size of the raw data.
052   *
053   * @return size
054   */
055  public int getSize() {
056    return m_data.remaining();
057  }
058
059  /**
060   * Gets the raw data. Use the GetX functions to decode based on the data type in the entry's start
061   * record.
062   *
063   * @return byte array
064   */
065  public byte[] getRaw() {
066    ByteBuffer buf = getRawBuffer();
067    byte[] arr = new byte[buf.remaining()];
068    buf.get(arr);
069    return arr;
070  }
071
072  /**
073   * Gets the raw data. Use the GetX functions to decode based on the data type in the entry's start
074   * record.
075   *
076   * @return byte buffer
077   */
078  public ByteBuffer getRawBuffer() {
079    ByteBuffer buf = m_data.duplicate();
080    buf.order(ByteOrder.LITTLE_ENDIAN);
081    return buf;
082  }
083
084  /**
085   * Returns true if the record is a control record.
086   *
087   * @return True if control record, false if normal data record.
088   */
089  public boolean isControl() {
090    return m_entry == 0;
091  }
092
093  /**
094   * Returns true if the record is a start control record. Use GetStartData() to decode the
095   * contents.
096   *
097   * @return True if start control record, false otherwise.
098   */
099  public boolean isStart() {
100    return m_entry == 0 && m_data.remaining() >= 17 && m_data.get(0) == kControlStart;
101  }
102
103  /**
104   * Returns true if the record is a finish control record. Use GetFinishEntry() to decode the
105   * contents.
106   *
107   * @return True if finish control record, false otherwise.
108   */
109  public boolean isFinish() {
110    return m_entry == 0 && m_data.remaining() == 5 && m_data.get(0) == kControlFinish;
111  }
112
113  /**
114   * Returns true if the record is a set metadata control record. Use GetSetMetadataData() to decode
115   * the contents.
116   *
117   * @return True if set metadata control record, false otherwise.
118   */
119  public boolean isSetMetadata() {
120    return m_entry == 0 && m_data.remaining() >= 9 && m_data.get(0) == kControlSetMetadata;
121  }
122
123  /**
124   * Data contained in a start control record as created by DataLog.start() when writing the log.
125   * This can be read by calling getStartData().
126   */
127  @SuppressWarnings("MemberName")
128  public static class StartRecordData {
129    StartRecordData(int entry, String name, String type, String metadata) {
130      this.entry = entry;
131      this.name = name;
132      this.type = type;
133      this.metadata = metadata;
134    }
135
136    /** Entry ID; this will be used for this entry in future records. */
137    public final int entry;
138
139    /** Entry name. */
140    public final String name;
141
142    /** Type of the stored data for this entry, as a string, e.g. "double". */
143    public final String type;
144
145    /** Initial metadata. */
146    public final String metadata;
147  }
148
149  /**
150   * Decodes a start control record.
151   *
152   * @return start record decoded data
153   * @throws InputMismatchException on error
154   */
155  public StartRecordData getStartData() {
156    if (!isStart()) {
157      throw new InputMismatchException("not a start record");
158    }
159    ByteBuffer buf = getRawBuffer();
160    buf.position(1); // skip over control type
161    int entry = buf.getInt();
162    String name = readInnerString(buf);
163    String type = readInnerString(buf);
164    String metadata = readInnerString(buf);
165    return new StartRecordData(entry, name, type, metadata);
166  }
167
168  /**
169   * Data contained in a set metadata control record as created by DataLog.setMetadata(). This can
170   * be read by calling getSetMetadataData().
171   */
172  @SuppressWarnings("MemberName")
173  public static class MetadataRecordData {
174    MetadataRecordData(int entry, String metadata) {
175      this.entry = entry;
176      this.metadata = metadata;
177    }
178
179    /** Entry ID. */
180    public final int entry;
181
182    /** New metadata for the entry. */
183    public final String metadata;
184  }
185
186  /**
187   * Decodes a finish control record.
188   *
189   * @return finish record entry ID
190   * @throws InputMismatchException on error
191   */
192  public int getFinishEntry() {
193    if (!isFinish()) {
194      throw new InputMismatchException("not a finish record");
195    }
196    return m_data.getInt(1);
197  }
198
199  /**
200   * Decodes a set metadata control record.
201   *
202   * @return set metadata record decoded data
203   * @throws InputMismatchException on error
204   */
205  public MetadataRecordData getSetMetadataData() {
206    if (!isSetMetadata()) {
207      throw new InputMismatchException("not a set metadata record");
208    }
209    ByteBuffer buf = getRawBuffer();
210    buf.position(1); // skip over control type
211    int entry = buf.getInt();
212    String metadata = readInnerString(buf);
213    return new MetadataRecordData(entry, metadata);
214  }
215
216  /**
217   * Decodes a data record as a boolean. Note if the data type (as indicated in the corresponding
218   * start control record for this entry) is not "boolean", invalid results may be returned.
219   *
220   * @return boolean value
221   * @throws InputMismatchException on error
222   */
223  public boolean getBoolean() {
224    try {
225      return m_data.get(0) != 0;
226    } catch (IndexOutOfBoundsException ex) {
227      throw new InputMismatchException();
228    }
229  }
230
231  /**
232   * Decodes a data record as an integer. Note if the data type (as indicated in the corresponding
233   * start control record for this entry) is not "int64", invalid results may be returned.
234   *
235   * @return integer value
236   * @throws InputMismatchException on error
237   */
238  public long getInteger() {
239    try {
240      return m_data.getLong(0);
241    } catch (BufferUnderflowException | IndexOutOfBoundsException ex) {
242      throw new InputMismatchException();
243    }
244  }
245
246  /**
247   * Decodes a data record as a float. Note if the data type (as indicated in the corresponding
248   * start control record for this entry) is not "float", invalid results may be returned.
249   *
250   * @return float value
251   * @throws InputMismatchException on error
252   */
253  public float getFloat() {
254    try {
255      return m_data.getFloat(0);
256    } catch (BufferUnderflowException | IndexOutOfBoundsException ex) {
257      throw new InputMismatchException();
258    }
259  }
260
261  /**
262   * Decodes a data record as a double. Note if the data type (as indicated in the corresponding
263   * start control record for this entry) is not "double", invalid results may be returned.
264   *
265   * @return double value
266   * @throws InputMismatchException on error
267   */
268  public double getDouble() {
269    try {
270      return m_data.getDouble(0);
271    } catch (BufferUnderflowException | IndexOutOfBoundsException ex) {
272      throw new InputMismatchException();
273    }
274  }
275
276  /**
277   * Decodes a data record as a string. Note if the data type (as indicated in the corresponding
278   * start control record for this entry) is not "string", invalid results may be returned.
279   *
280   * @return string value
281   */
282  public String getString() {
283    return new String(getRaw(), StandardCharsets.UTF_8);
284  }
285
286  /**
287   * Decodes a data record as a boolean array. Note if the data type (as indicated in the
288   * corresponding start control record for this entry) is not "boolean[]", invalid results may be
289   * returned.
290   *
291   * @return boolean array
292   */
293  public boolean[] getBooleanArray() {
294    boolean[] arr = new boolean[m_data.remaining()];
295    for (int i = 0; i < m_data.remaining(); i++) {
296      arr[i] = m_data.get(i) != 0;
297    }
298    return arr;
299  }
300
301  /**
302   * Decodes a data record as an integer array. Note if the data type (as indicated in the
303   * corresponding start control record for this entry) is not "int64[]", invalid results may be
304   * returned.
305   *
306   * @return integer array
307   * @throws InputMismatchException on error
308   */
309  public long[] getIntegerArray() {
310    LongBuffer buf = getIntegerBuffer();
311    long[] arr = new long[buf.remaining()];
312    buf.get(arr);
313    return arr;
314  }
315
316  /**
317   * Decodes a data record as an integer array. Note if the data type (as indicated in the
318   * corresponding start control record for this entry) is not "int64[]", invalid results may be
319   * returned.
320   *
321   * @return integer buffer
322   * @throws InputMismatchException on error
323   */
324  public LongBuffer getIntegerBuffer() {
325    if ((m_data.limit() % 8) != 0) {
326      throw new InputMismatchException("data size is not a multiple of 8");
327    }
328    return m_data.asLongBuffer();
329  }
330
331  /**
332   * Decodes a data record as a float array. Note if the data type (as indicated in the
333   * corresponding start control record for this entry) is not "float[]", invalid results may be
334   * returned.
335   *
336   * @return float array
337   * @throws InputMismatchException on error
338   */
339  public float[] getFloatArray() {
340    FloatBuffer buf = getFloatBuffer();
341    float[] arr = new float[buf.remaining()];
342    buf.get(arr);
343    return arr;
344  }
345
346  /**
347   * Decodes a data record as a float array. Note if the data type (as indicated in the
348   * corresponding start control record for this entry) is not "float[]", invalid results may be
349   * returned.
350   *
351   * @return float buffer
352   * @throws InputMismatchException on error
353   */
354  public FloatBuffer getFloatBuffer() {
355    if ((m_data.limit() % 4) != 0) {
356      throw new InputMismatchException("data size is not a multiple of 4");
357    }
358    return m_data.asFloatBuffer();
359  }
360
361  /**
362   * Decodes a data record as a double array. Note if the data type (as indicated in the
363   * corresponding start control record for this entry) is not "double[]", invalid results may be
364   * returned.
365   *
366   * @return double array
367   * @throws InputMismatchException on error
368   */
369  public double[] getDoubleArray() {
370    DoubleBuffer buf = getDoubleBuffer();
371    double[] arr = new double[buf.remaining()];
372    buf.get(arr);
373    return arr;
374  }
375
376  /**
377   * Decodes a data record as a double array. Note if the data type (as indicated in the
378   * corresponding start control record for this entry) is not "double[]", invalid results may be
379   * returned.
380   *
381   * @return double buffer
382   * @throws InputMismatchException on error
383   */
384  public DoubleBuffer getDoubleBuffer() {
385    if ((m_data.limit() % 8) != 0) {
386      throw new InputMismatchException("data size is not a multiple of 8");
387    }
388    return m_data.asDoubleBuffer();
389  }
390
391  /**
392   * Decodes a data record as a string array. Note if the data type (as indicated in the
393   * corresponding start control record for this entry) is not "string[]", invalid results may be
394   * returned.
395   *
396   * @return string array
397   * @throws InputMismatchException on error
398   */
399  public String[] getStringArray() {
400    ByteBuffer buf = getRawBuffer();
401    try {
402      int size = buf.getInt();
403      // sanity check size
404      if (size > (buf.remaining() / 4)) {
405        throw new InputMismatchException("invalid size");
406      }
407      String[] arr = new String[size];
408      for (int i = 0; i < size; i++) {
409        arr[i] = readInnerString(buf);
410      }
411      return arr;
412    } catch (BufferUnderflowException | IndexOutOfBoundsException ex) {
413      throw new InputMismatchException();
414    }
415  }
416
417  private String readInnerString(ByteBuffer buf) {
418    int size = buf.getInt();
419    if (size > buf.remaining()) {
420      throw new InputMismatchException("invalid string size");
421    }
422    byte[] arr = new byte[size];
423    buf.get(arr);
424    return new String(arr, StandardCharsets.UTF_8);
425  }
426
427  private final int m_entry;
428  private final long m_timestamp;
429  private final ByteBuffer m_data;
430}