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}