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 org.wpilib.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  public static class StartRecordData {
128    StartRecordData(int entry, String name, String type, String metadata) {
129      this.entry = entry;
130      this.name = name;
131      this.type = type;
132      this.metadata = metadata;
133    }
134
135    /** Entry ID; this will be used for this entry in future records. */
136    public final int entry;
137
138    /** Entry name. */
139    public final String name;
140
141    /** Type of the stored data for this entry, as a string, e.g. "double". */
142    public final String type;
143
144    /** Initial metadata. */
145    public final String metadata;
146  }
147
148  /**
149   * Decodes a start control record.
150   *
151   * @return start record decoded data
152   * @throws InputMismatchException on error
153   */
154  public StartRecordData getStartData() {
155    if (!isStart()) {
156      throw new InputMismatchException("not a start record");
157    }
158    ByteBuffer buf = getRawBuffer();
159    buf.position(1); // skip over control type
160    int entry = buf.getInt();
161    String name = readInnerString(buf);
162    String type = readInnerString(buf);
163    String metadata = readInnerString(buf);
164    return new StartRecordData(entry, name, type, metadata);
165  }
166
167  /**
168   * Data contained in a set metadata control record as created by DataLog.setMetadata(). This can
169   * be read by calling getSetMetadataData().
170   */
171  public static class MetadataRecordData {
172    MetadataRecordData(int entry, String metadata) {
173      this.entry = entry;
174      this.metadata = metadata;
175    }
176
177    /** Entry ID. */
178    public final int entry;
179
180    /** New metadata for the entry. */
181    public final String metadata;
182  }
183
184  /**
185   * Decodes a finish control record.
186   *
187   * @return finish record entry ID
188   * @throws InputMismatchException on error
189   */
190  public int getFinishEntry() {
191    if (!isFinish()) {
192      throw new InputMismatchException("not a finish record");
193    }
194    return m_data.getInt(1);
195  }
196
197  /**
198   * Decodes a set metadata control record.
199   *
200   * @return set metadata record decoded data
201   * @throws InputMismatchException on error
202   */
203  public MetadataRecordData getSetMetadataData() {
204    if (!isSetMetadata()) {
205      throw new InputMismatchException("not a set metadata record");
206    }
207    ByteBuffer buf = getRawBuffer();
208    buf.position(1); // skip over control type
209    int entry = buf.getInt();
210    String metadata = readInnerString(buf);
211    return new MetadataRecordData(entry, metadata);
212  }
213
214  /**
215   * Decodes a data record as a boolean. Note if the data type (as indicated in the corresponding
216   * start control record for this entry) is not "boolean", invalid results may be returned.
217   *
218   * @return boolean value
219   * @throws InputMismatchException on error
220   */
221  public boolean getBoolean() {
222    try {
223      return m_data.get(0) != 0;
224    } catch (IndexOutOfBoundsException ex) {
225      throw new InputMismatchException();
226    }
227  }
228
229  /**
230   * Decodes a data record as an integer. Note if the data type (as indicated in the corresponding
231   * start control record for this entry) is not "int64", invalid results may be returned.
232   *
233   * @return integer value
234   * @throws InputMismatchException on error
235   */
236  public long getInteger() {
237    try {
238      return m_data.getLong(0);
239    } catch (BufferUnderflowException | IndexOutOfBoundsException ex) {
240      throw new InputMismatchException();
241    }
242  }
243
244  /**
245   * Decodes a data record as a float. Note if the data type (as indicated in the corresponding
246   * start control record for this entry) is not "float", invalid results may be returned.
247   *
248   * @return float value
249   * @throws InputMismatchException on error
250   */
251  public float getFloat() {
252    try {
253      return m_data.getFloat(0);
254    } catch (BufferUnderflowException | IndexOutOfBoundsException ex) {
255      throw new InputMismatchException();
256    }
257  }
258
259  /**
260   * Decodes a data record as a double. Note if the data type (as indicated in the corresponding
261   * start control record for this entry) is not "double", invalid results may be returned.
262   *
263   * @return double value
264   * @throws InputMismatchException on error
265   */
266  public double getDouble() {
267    try {
268      return m_data.getDouble(0);
269    } catch (BufferUnderflowException | IndexOutOfBoundsException ex) {
270      throw new InputMismatchException();
271    }
272  }
273
274  /**
275   * Decodes a data record as a string. Note if the data type (as indicated in the corresponding
276   * start control record for this entry) is not "string", invalid results may be returned.
277   *
278   * @return string value
279   */
280  public String getString() {
281    return new String(getRaw(), StandardCharsets.UTF_8);
282  }
283
284  /**
285   * Decodes a data record as a boolean array. Note if the data type (as indicated in the
286   * corresponding start control record for this entry) is not "boolean[]", invalid results may be
287   * returned.
288   *
289   * @return boolean array
290   */
291  public boolean[] getBooleanArray() {
292    boolean[] arr = new boolean[m_data.remaining()];
293    for (int i = 0; i < m_data.remaining(); i++) {
294      arr[i] = m_data.get(i) != 0;
295    }
296    return arr;
297  }
298
299  /**
300   * Decodes a data record as an integer array. Note if the data type (as indicated in the
301   * corresponding start control record for this entry) is not "int64[]", invalid results may be
302   * returned.
303   *
304   * @return integer array
305   * @throws InputMismatchException on error
306   */
307  public long[] getIntegerArray() {
308    LongBuffer buf = getIntegerBuffer();
309    long[] arr = new long[buf.remaining()];
310    buf.get(arr);
311    return arr;
312  }
313
314  /**
315   * Decodes a data record as an integer array. Note if the data type (as indicated in the
316   * corresponding start control record for this entry) is not "int64[]", invalid results may be
317   * returned.
318   *
319   * @return integer buffer
320   * @throws InputMismatchException on error
321   */
322  public LongBuffer getIntegerBuffer() {
323    if ((m_data.limit() % 8) != 0) {
324      throw new InputMismatchException("data size is not a multiple of 8");
325    }
326    return m_data.asLongBuffer();
327  }
328
329  /**
330   * Decodes a data record as a float array. Note if the data type (as indicated in the
331   * corresponding start control record for this entry) is not "float[]", invalid results may be
332   * returned.
333   *
334   * @return float array
335   * @throws InputMismatchException on error
336   */
337  public float[] getFloatArray() {
338    FloatBuffer buf = getFloatBuffer();
339    float[] arr = new float[buf.remaining()];
340    buf.get(arr);
341    return arr;
342  }
343
344  /**
345   * Decodes a data record as a float array. Note if the data type (as indicated in the
346   * corresponding start control record for this entry) is not "float[]", invalid results may be
347   * returned.
348   *
349   * @return float buffer
350   * @throws InputMismatchException on error
351   */
352  public FloatBuffer getFloatBuffer() {
353    if ((m_data.limit() % 4) != 0) {
354      throw new InputMismatchException("data size is not a multiple of 4");
355    }
356    return m_data.asFloatBuffer();
357  }
358
359  /**
360   * Decodes a data record as a double array. Note if the data type (as indicated in the
361   * corresponding start control record for this entry) is not "double[]", invalid results may be
362   * returned.
363   *
364   * @return double array
365   * @throws InputMismatchException on error
366   */
367  public double[] getDoubleArray() {
368    DoubleBuffer buf = getDoubleBuffer();
369    double[] arr = new double[buf.remaining()];
370    buf.get(arr);
371    return arr;
372  }
373
374  /**
375   * Decodes a data record as a double array. Note if the data type (as indicated in the
376   * corresponding start control record for this entry) is not "double[]", invalid results may be
377   * returned.
378   *
379   * @return double buffer
380   * @throws InputMismatchException on error
381   */
382  public DoubleBuffer getDoubleBuffer() {
383    if ((m_data.limit() % 8) != 0) {
384      throw new InputMismatchException("data size is not a multiple of 8");
385    }
386    return m_data.asDoubleBuffer();
387  }
388
389  /**
390   * Decodes a data record as a string array. Note if the data type (as indicated in the
391   * corresponding start control record for this entry) is not "string[]", invalid results may be
392   * returned.
393   *
394   * @return string array
395   * @throws InputMismatchException on error
396   */
397  public String[] getStringArray() {
398    ByteBuffer buf = getRawBuffer();
399    try {
400      int size = buf.getInt();
401      // sanity check size
402      if (size > (buf.remaining() / 4)) {
403        throw new InputMismatchException("invalid size");
404      }
405      String[] arr = new String[size];
406      for (int i = 0; i < size; i++) {
407        arr[i] = readInnerString(buf);
408      }
409      return arr;
410    } catch (BufferUnderflowException | IndexOutOfBoundsException ex) {
411      throw new InputMismatchException();
412    }
413  }
414
415  private String readInnerString(ByteBuffer buf) {
416    int size = buf.getInt();
417    if (size > buf.remaining()) {
418      throw new InputMismatchException("invalid string size");
419    }
420    byte[] arr = new byte[size];
421    buf.get(arr);
422    return new String(arr, StandardCharsets.UTF_8);
423  }
424
425  private final int m_entry;
426  private final long m_timestamp;
427  private final ByteBuffer m_data;
428}