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.io.IOException;
008import java.io.RandomAccessFile;
009import java.nio.BufferUnderflowException;
010import java.nio.ByteBuffer;
011import java.nio.ByteOrder;
012import java.nio.channels.FileChannel;
013import java.nio.charset.StandardCharsets;
014import java.util.NoSuchElementException;
015import java.util.function.Consumer;
016
017/** Data log reader (reads logs written by the DataLog class). */
018public class DataLogReader implements Iterable<DataLogRecord> {
019  /**
020   * Constructs from a byte buffer.
021   *
022   * @param buffer byte buffer
023   */
024  public DataLogReader(ByteBuffer buffer) {
025    m_buf = buffer;
026    m_buf.order(ByteOrder.LITTLE_ENDIAN);
027  }
028
029  /**
030   * Constructs from a file.
031   *
032   * @param filename filename
033   * @throws IOException if unable to open/read file
034   */
035  public DataLogReader(String filename) throws IOException {
036    RandomAccessFile f = new RandomAccessFile(filename, "r");
037    FileChannel channel = f.getChannel();
038    m_buf = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size());
039    m_buf.order(ByteOrder.LITTLE_ENDIAN);
040    channel.close();
041    f.close();
042  }
043
044  /**
045   * Returns true if the data log is valid (e.g. has a valid header).
046   *
047   * @return True if valid, false otherwise
048   */
049  public boolean isValid() {
050    return m_buf.remaining() >= 12
051        && m_buf.get(0) == 'W'
052        && m_buf.get(1) == 'P'
053        && m_buf.get(2) == 'I'
054        && m_buf.get(3) == 'L'
055        && m_buf.get(4) == 'O'
056        && m_buf.get(5) == 'G'
057        && m_buf.getShort(6) >= 0x0100;
058  }
059
060  /**
061   * Gets the data log version. Returns 0 if data log is invalid.
062   *
063   * @return Version number; most significant byte is major, least significant is minor (so version
064   *     1.0 will be 0x0100)
065   */
066  public short getVersion() {
067    if (m_buf.remaining() < 12) {
068      return 0;
069    }
070    return m_buf.getShort(6);
071  }
072
073  /**
074   * Gets the extra header data.
075   *
076   * @return Extra header data
077   */
078  public String getExtraHeader() {
079    ByteBuffer buf = m_buf.duplicate();
080    buf.order(ByteOrder.LITTLE_ENDIAN);
081    buf.position(8);
082    int size = buf.getInt();
083    byte[] arr = new byte[size];
084    buf.get(arr);
085    return new String(arr, StandardCharsets.UTF_8);
086  }
087
088  @Override
089  public void forEach(Consumer<? super DataLogRecord> action) {
090    int size = m_buf.remaining();
091    for (int pos = 12 + m_buf.getInt(8); pos < size; pos = getNextRecord(pos)) {
092      DataLogRecord record;
093      try {
094        record = getRecord(pos);
095      } catch (NoSuchElementException ex) {
096        break;
097      }
098      action.accept(record);
099    }
100  }
101
102  @Override
103  public DataLogIterator iterator() {
104    return new DataLogIterator(this, 12 + m_buf.getInt(8));
105  }
106
107  private long readVarInt(int pos, int len) {
108    long val = 0;
109    for (int i = 0; i < len; i++) {
110      val |= ((long) (m_buf.get(pos + i) & 0xff)) << (i * 8);
111    }
112    return val;
113  }
114
115  DataLogRecord getRecord(int pos) {
116    try {
117      int lenbyte = m_buf.get(pos) & 0xff;
118      int entryLen = (lenbyte & 0x3) + 1;
119      int sizeLen = ((lenbyte >> 2) & 0x3) + 1;
120      int timestampLen = ((lenbyte >> 4) & 0x7) + 1;
121      int headerLen = 1 + entryLen + sizeLen + timestampLen;
122      int entry = (int) readVarInt(pos + 1, entryLen);
123      int size = (int) readVarInt(pos + 1 + entryLen, sizeLen);
124      long timestamp = readVarInt(pos + 1 + entryLen + sizeLen, timestampLen);
125      // build a slice of the data contents
126      ByteBuffer data = m_buf.duplicate();
127      data.position(pos + headerLen);
128      data.limit(pos + headerLen + size);
129      return new DataLogRecord(entry, timestamp, data.slice());
130    } catch (BufferUnderflowException | IndexOutOfBoundsException ex) {
131      throw new NoSuchElementException();
132    }
133  }
134
135  int getNextRecord(int pos) {
136    int lenbyte = m_buf.get(pos) & 0xff;
137    int entryLen = (lenbyte & 0x3) + 1;
138    int sizeLen = ((lenbyte >> 2) & 0x3) + 1;
139    int timestampLen = ((lenbyte >> 4) & 0x7) + 1;
140    int headerLen = 1 + entryLen + sizeLen + timestampLen;
141
142    int size = 0;
143    for (int i = 0; i < sizeLen; i++) {
144      size |= (m_buf.get(pos + 1 + entryLen + i) & 0xff) << (i * 8);
145    }
146    return pos + headerLen + size;
147  }
148
149  int size() {
150    return m_buf.remaining();
151  }
152
153  private final ByteBuffer m_buf;
154}