diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/BooleanArrayLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/BooleanArrayLogEntry.java
index 21ac596359..76b85f518c 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/BooleanArrayLogEntry.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/BooleanArrayLogEntry.java
@@ -4,6 +4,8 @@
package edu.wpi.first.util.datalog;
+import java.util.Arrays;
+
/** Log array of boolean values. */
public class BooleanArrayLogEntry extends DataLogEntry {
/** The data type for boolean array values. */
@@ -71,4 +73,79 @@ public class BooleanArrayLogEntry extends DataLogEntry {
public void append(boolean[] value) {
m_log.appendBooleanArray(m_entry, value, 0);
}
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ *
Note: the last value is local to this class instance; using update() with two instances
+ * pointing to the same underlying log entry name will likely result in unexpected results.
+ *
+ * @param value Value to record
+ * @param timestamp Time stamp (0 to indicate now)
+ */
+ public synchronized void update(boolean[] value, long timestamp) {
+ if (!equalsLast(value)) {
+ copyToLast(value);
+ append(value, timestamp);
+ }
+ }
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ *
Note: the last value is local to this class instance; using update() with two instances
+ * pointing to the same underlying log entry name will likely result in unexpected results.
+ *
+ * @param value Value to record
+ */
+ public void update(boolean[] value) {
+ update(value, 0);
+ }
+
+ /**
+ * Gets whether there is a last value.
+ *
+ *
Note: the last value is local to this class instance and updated only with update(), not
+ * append().
+ *
+ * @return True if last value exists, false otherwise.
+ */
+ public synchronized boolean hasLastValue() {
+ return m_lastValue != null;
+ }
+
+ /**
+ * Gets the last value.
+ *
+ *
Note: the last value is local to this class instance and updated only with update(), not
+ * append().
+ *
+ * @return Last value, or null if none.
+ */
+ @SuppressWarnings("PMD.ReturnEmptyCollectionRatherThanNull")
+ public synchronized boolean[] getLastValue() {
+ if (m_lastValue == null) {
+ return null;
+ }
+ return Arrays.copyOf(m_lastValue, m_lastValueLen);
+ }
+
+ private boolean equalsLast(boolean[] value) {
+ if (m_lastValue == null || m_lastValueLen != value.length) {
+ return false;
+ }
+ return Arrays.equals(m_lastValue, 0, value.length, value, 0, value.length);
+ }
+
+ private void copyToLast(boolean[] value) {
+ if (m_lastValue == null || m_lastValue.length < value.length) {
+ m_lastValue = Arrays.copyOf(value, value.length);
+ } else {
+ System.arraycopy(value, 0, m_lastValue, 0, value.length);
+ }
+ m_lastValueLen = value.length;
+ }
+
+ private boolean[] m_lastValue;
+ private int m_lastValueLen;
}
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/BooleanLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/BooleanLogEntry.java
index c413bfa42a..ba1783e161 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/BooleanLogEntry.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/BooleanLogEntry.java
@@ -71,4 +71,60 @@ public class BooleanLogEntry extends DataLogEntry {
public void append(boolean value) {
m_log.appendBoolean(m_entry, value, 0);
}
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ *
Note: the last value is local to this class instance; using update() with two instances
+ * pointing to the same underlying log entry name will likely result in unexpected results.
+ *
+ * @param value Value to record
+ * @param timestamp Time stamp (0 to indicate now)
+ */
+ public synchronized void update(boolean value, long timestamp) {
+ if (!m_hasLastValue || m_lastValue != value) {
+ m_lastValue = value;
+ m_hasLastValue = true;
+ append(value, timestamp);
+ }
+ }
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ *
Note: the last value is local to this class instance; using update() with two instances
+ * pointing to the same underlying log entry name will likely result in unexpected results.
+ *
+ * @param value Value to record
+ */
+ public void update(boolean value) {
+ update(value, 0);
+ }
+
+ /**
+ * Gets whether there is a last value.
+ *
+ *
Note: the last value is local to this class instance and updated only with update(), not
+ * append().
+ *
+ * @return True if last value exists, false otherwise.
+ */
+ public synchronized boolean hasLastValue() {
+ return m_hasLastValue;
+ }
+
+ /**
+ * Gets the last value.
+ *
+ *
Note: the last value is local to this class instance and updated only with update(), not
+ * append().
+ *
+ * @return Last value, or false if none.
+ */
+ public synchronized boolean getLastValue() {
+ return m_lastValue;
+ }
+
+ private boolean m_hasLastValue;
+ private boolean m_lastValue;
}
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/DataLog.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/DataLog.java
index bec3a7bde6..4a89542cb7 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/DataLog.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/DataLog.java
@@ -269,6 +269,12 @@ public class DataLog implements AutoCloseable {
setMetadata(entry, metadata, 0);
}
+ @Override
+ public void close() {
+ DataLogJNI.close(m_impl);
+ m_impl = 0;
+ }
+
/**
* Appends a raw record to the log.
*
@@ -318,12 +324,6 @@ public class DataLog implements AutoCloseable {
DataLogJNI.appendRaw(m_impl, entry, data, start, len, timestamp);
}
- @Override
- public void close() {
- DataLogJNI.close(m_impl);
- m_impl = 0;
- }
-
/**
* Appends a boolean record to the log.
*
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/DataLogJNI.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/DataLogJNI.java
index ea040edb71..ee868931d9 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/DataLogJNI.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/DataLogJNI.java
@@ -46,6 +46,14 @@ public class DataLogJNI extends WPIUtilJNI {
*/
static native long fgCreate(String filename, String extraHeader) throws IOException;
+ /**
+ * Create a new Data Log foreground writer to a memory buffer.
+ *
+ * @param extraHeader extra header data
+ * @return data log writer implementation handle
+ */
+ static native long fgCreateMemory(String extraHeader);
+
/**
* Explicitly flushes the log data to disk.
*
@@ -53,6 +61,16 @@ public class DataLogJNI extends WPIUtilJNI {
*/
static native void flush(long impl);
+ /**
+ * Flushes the log data to a memory buffer (only valid with fgCreateMemory data logs).
+ *
+ * @param impl data log background writer implementation handle
+ * @param buf output data buffer
+ * @param pos position in write buffer to start copying from
+ * @return Number of bytes written to buffer; 0 if no more to copy
+ */
+ static native int copyWriteBuffer(long impl, byte[] buf, int pos);
+
/**
* Pauses appending of data records to the log. While paused, no data records are saved (e.g.
* AppendX is a no-op). Has no effect on entry starts / finishes / metadata changes.
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/DataLogWriter.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/DataLogWriter.java
index 2b11b9a4d2..058d6dd8a4 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/DataLogWriter.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/DataLogWriter.java
@@ -5,6 +5,7 @@
package edu.wpi.first.util.datalog;
import java.io.IOException;
+import java.io.OutputStream;
/** A data log writer that flushes the data log to a file when flush() is called. */
public class DataLogWriter extends DataLog {
@@ -17,6 +18,8 @@ public class DataLogWriter extends DataLog {
*/
public DataLogWriter(String filename, String extraHeader) throws IOException {
super(DataLogJNI.fgCreate(filename, extraHeader));
+ m_os = null;
+ m_buf = null;
}
/**
@@ -28,4 +31,54 @@ public class DataLogWriter extends DataLog {
public DataLogWriter(String filename) throws IOException {
this(filename, "");
}
+
+ /**
+ * Construct a new Data Log with an output stream. Prefer the filename version if possible; this
+ * is much slower!
+ *
+ * @param os output stream
+ * @param extraHeader extra header data
+ */
+ public DataLogWriter(OutputStream os, String extraHeader) {
+ super(DataLogJNI.fgCreateMemory(extraHeader));
+ m_os = os;
+ m_buf = new byte[kBufferSize];
+ }
+
+ /**
+ * Construct a new Data Log with an output stream.
+ *
+ * @param os output stream
+ */
+ public DataLogWriter(OutputStream os) {
+ this(os, "");
+ }
+
+ /** Explicitly flushes the log data to disk. */
+ @Override
+ public void flush() {
+ DataLogJNI.flush(m_impl);
+ if (m_os == null) {
+ return;
+ }
+ try {
+ int pos = 0;
+ for (; ; ) {
+ int qty = DataLogJNI.copyWriteBuffer(m_impl, m_buf, pos);
+ if (qty == 0) {
+ break;
+ }
+ pos += qty;
+ m_os.write(m_buf, 0, qty);
+ }
+ m_os.flush();
+ } catch (IOException e) {
+ // ignore
+ }
+ }
+
+ private static final int kBufferSize = 16 * 1024;
+
+ private final OutputStream m_os;
+ private final byte[] m_buf;
}
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/DoubleArrayLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/DoubleArrayLogEntry.java
index 485a9c8c82..2fe528f1bd 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/DoubleArrayLogEntry.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/DoubleArrayLogEntry.java
@@ -4,6 +4,8 @@
package edu.wpi.first.util.datalog;
+import java.util.Arrays;
+
/** Log array of double values. */
public class DoubleArrayLogEntry extends DataLogEntry {
/** The data type for double array values. */
@@ -71,4 +73,79 @@ public class DoubleArrayLogEntry extends DataLogEntry {
public void append(double[] value) {
m_log.appendDoubleArray(m_entry, value, 0);
}
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ *
Note: the last value is local to this class instance; using update() with two instances
+ * pointing to the same underlying log entry name will likely result in unexpected results.
+ *
+ * @param value Value to record
+ * @param timestamp Time stamp (0 to indicate now)
+ */
+ public synchronized void update(double[] value, long timestamp) {
+ if (!equalsLast(value)) {
+ copyToLast(value);
+ append(value, timestamp);
+ }
+ }
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ *
Note: the last value is local to this class instance; using update() with two instances
+ * pointing to the same underlying log entry name will likely result in unexpected results.
+ *
+ * @param value Value to record
+ */
+ public void update(double[] value) {
+ update(value, 0);
+ }
+
+ /**
+ * Gets whether there is a last value.
+ *
+ *
Note: the last value is local to this class instance and updated only with update(), not
+ * append().
+ *
+ * @return True if last value exists, false otherwise.
+ */
+ public synchronized boolean hasLastValue() {
+ return m_lastValue != null;
+ }
+
+ /**
+ * Gets the last value.
+ *
+ *
Note: the last value is local to this class instance and updated only with update(), not
+ * append().
+ *
+ * @return Last value, or false if none.
+ */
+ @SuppressWarnings("PMD.ReturnEmptyCollectionRatherThanNull")
+ public synchronized double[] getLastValue() {
+ if (m_lastValue == null) {
+ return null;
+ }
+ return Arrays.copyOf(m_lastValue, m_lastValueLen);
+ }
+
+ private boolean equalsLast(double[] value) {
+ if (m_lastValue == null || m_lastValueLen != value.length) {
+ return false;
+ }
+ return Arrays.equals(m_lastValue, 0, value.length, value, 0, value.length);
+ }
+
+ private void copyToLast(double[] value) {
+ if (m_lastValue == null || m_lastValue.length < value.length) {
+ m_lastValue = Arrays.copyOf(value, value.length);
+ } else {
+ System.arraycopy(value, 0, m_lastValue, 0, value.length);
+ }
+ m_lastValueLen = value.length;
+ }
+
+ private double[] m_lastValue;
+ private int m_lastValueLen;
}
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/DoubleLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/DoubleLogEntry.java
index a089df2cd8..2484063aba 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/DoubleLogEntry.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/DoubleLogEntry.java
@@ -71,4 +71,60 @@ public class DoubleLogEntry extends DataLogEntry {
public void append(double value) {
m_log.appendDouble(m_entry, value, 0);
}
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ *
Note: the last value is local to this class instance; using update() with two instances
+ * pointing to the same underlying log entry name will likely result in unexpected results.
+ *
+ * @param value Value to record
+ * @param timestamp Time stamp (0 to indicate now)
+ */
+ public synchronized void update(double value, long timestamp) {
+ if (!m_hasLastValue || m_lastValue != value) {
+ m_lastValue = value;
+ m_hasLastValue = true;
+ append(value, timestamp);
+ }
+ }
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ *
Note: the last value is local to this class instance; using update() with two instances
+ * pointing to the same underlying log entry name will likely result in unexpected results.
+ *
+ * @param value Value to record
+ */
+ public void update(double value) {
+ update(value, 0);
+ }
+
+ /**
+ * Gets whether there is a last value.
+ *
+ *
Note: the last value is local to this class instance and updated only with update(), not
+ * append().
+ *
+ * @return True if last value exists, false otherwise.
+ */
+ public synchronized boolean hasLastValue() {
+ return m_hasLastValue;
+ }
+
+ /**
+ * Gets the last value.
+ *
+ *
Note: the last value is local to this class instance and updated only with update(), not
+ * append().
+ *
+ * @return Last value, or 0 if none.
+ */
+ public synchronized double getLastValue() {
+ return m_lastValue;
+ }
+
+ boolean m_hasLastValue;
+ double m_lastValue;
}
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/FloatArrayLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/FloatArrayLogEntry.java
index be25970ffc..136b7e5b42 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/FloatArrayLogEntry.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/FloatArrayLogEntry.java
@@ -4,6 +4,8 @@
package edu.wpi.first.util.datalog;
+import java.util.Arrays;
+
/** Log array of float values. */
public class FloatArrayLogEntry extends DataLogEntry {
/** The data type for float array values. */
@@ -71,4 +73,79 @@ public class FloatArrayLogEntry extends DataLogEntry {
public void append(float[] value) {
m_log.appendFloatArray(m_entry, value, 0);
}
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ *
Note: the last value is local to this class instance; using update() with two instances
+ * pointing to the same underlying log entry name will likely result in unexpected results.
+ *
+ * @param value Value to record
+ * @param timestamp Time stamp (0 to indicate now)
+ */
+ public synchronized void update(float[] value, long timestamp) {
+ if (!equalsLast(value)) {
+ copyToLast(value);
+ append(value, timestamp);
+ }
+ }
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ *
Note: the last value is local to this class instance; using update() with two instances
+ * pointing to the same underlying log entry name will likely result in unexpected results.
+ *
+ * @param value Value to record
+ */
+ public void update(float[] value) {
+ update(value, 0);
+ }
+
+ /**
+ * Gets whether there is a last value.
+ *
+ *
Note: the last value is local to this class instance and updated only with update(), not
+ * append().
+ *
+ * @return True if last value exists, false otherwise.
+ */
+ public synchronized boolean hasLastValue() {
+ return m_lastValue != null;
+ }
+
+ /**
+ * Gets the last value.
+ *
+ *
Note: the last value is local to this class instance and updated only with update(), not
+ * append().
+ *
+ * @return Last value, or false if none.
+ */
+ @SuppressWarnings("PMD.ReturnEmptyCollectionRatherThanNull")
+ public synchronized float[] getLastValue() {
+ if (m_lastValue == null) {
+ return null;
+ }
+ return Arrays.copyOf(m_lastValue, m_lastValueLen);
+ }
+
+ private boolean equalsLast(float[] value) {
+ if (m_lastValue == null || m_lastValueLen != value.length) {
+ return false;
+ }
+ return Arrays.equals(m_lastValue, 0, value.length, value, 0, value.length);
+ }
+
+ private void copyToLast(float[] value) {
+ if (m_lastValue == null || m_lastValue.length < value.length) {
+ m_lastValue = Arrays.copyOf(value, value.length);
+ } else {
+ System.arraycopy(value, 0, m_lastValue, 0, value.length);
+ }
+ m_lastValueLen = value.length;
+ }
+
+ private float[] m_lastValue;
+ private int m_lastValueLen;
}
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/FloatLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/FloatLogEntry.java
index 28f83cb6a6..8cb02cfafd 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/FloatLogEntry.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/FloatLogEntry.java
@@ -71,4 +71,60 @@ public class FloatLogEntry extends DataLogEntry {
public void append(float value) {
m_log.appendFloat(m_entry, value, 0);
}
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ *
Note: the last value is local to this class instance; using update() with two instances
+ * pointing to the same underlying log entry name will likely result in unexpected results.
+ *
+ * @param value Value to record
+ * @param timestamp Time stamp (0 to indicate now)
+ */
+ public synchronized void update(float value, long timestamp) {
+ if (!m_hasLastValue || m_lastValue != value) {
+ m_lastValue = value;
+ m_hasLastValue = true;
+ append(value, timestamp);
+ }
+ }
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ *
Note: the last value is local to this class instance; using update() with two instances
+ * pointing to the same underlying log entry name will likely result in unexpected results.
+ *
+ * @param value Value to record
+ */
+ public void update(float value) {
+ update(value, 0);
+ }
+
+ /**
+ * Gets whether there is a last value.
+ *
+ *
Note: the last value is local to this class instance and updated only with update(), not
+ * append().
+ *
+ * @return True if last value exists, false otherwise.
+ */
+ public synchronized boolean hasLastValue() {
+ return m_hasLastValue;
+ }
+
+ /**
+ * Gets the last value.
+ *
+ *
Note: the last value is local to this class instance and updated only with update(), not
+ * append().
+ *
+ * @return Last value, or 0 if none.
+ */
+ public synchronized float getLastValue() {
+ return m_lastValue;
+ }
+
+ boolean m_hasLastValue;
+ float m_lastValue;
}
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/IntegerArrayLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/IntegerArrayLogEntry.java
index d2f8f0ea52..80e2be36d9 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/IntegerArrayLogEntry.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/IntegerArrayLogEntry.java
@@ -4,6 +4,8 @@
package edu.wpi.first.util.datalog;
+import java.util.Arrays;
+
/** Log array of integer values. */
public class IntegerArrayLogEntry extends DataLogEntry {
/** The data type for integer array values. */
@@ -71,4 +73,79 @@ public class IntegerArrayLogEntry extends DataLogEntry {
public void append(long[] value) {
m_log.appendIntegerArray(m_entry, value, 0);
}
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ *
Note: the last value is local to this class instance; using update() with two instances
+ * pointing to the same underlying log entry name will likely result in unexpected results.
+ *
+ * @param value Value to record
+ * @param timestamp Time stamp (0 to indicate now)
+ */
+ public synchronized void update(long[] value, long timestamp) {
+ if (!equalsLast(value)) {
+ copyToLast(value);
+ append(value, timestamp);
+ }
+ }
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ *
Note: the last value is local to this class instance; using update() with two instances
+ * pointing to the same underlying log entry name will likely result in unexpected results.
+ *
+ * @param value Value to record
+ */
+ public void update(long[] value) {
+ update(value, 0);
+ }
+
+ /**
+ * Gets whether there is a last value.
+ *
+ *
Note: the last value is local to this class instance and updated only with update(), not
+ * append().
+ *
+ * @return True if last value exists, false otherwise.
+ */
+ public synchronized boolean hasLastValue() {
+ return m_lastValue != null;
+ }
+
+ /**
+ * Gets the last value.
+ *
+ *
Note: the last value is local to this class instance and updated only with update(), not
+ * append().
+ *
+ * @return Last value, or false if none.
+ */
+ @SuppressWarnings("PMD.ReturnEmptyCollectionRatherThanNull")
+ public synchronized long[] getLastValue() {
+ if (m_lastValue == null) {
+ return null;
+ }
+ return Arrays.copyOf(m_lastValue, m_lastValueLen);
+ }
+
+ private boolean equalsLast(long[] value) {
+ if (m_lastValue == null || m_lastValueLen != value.length) {
+ return false;
+ }
+ return Arrays.equals(m_lastValue, 0, value.length, value, 0, value.length);
+ }
+
+ private void copyToLast(long[] value) {
+ if (m_lastValue == null || m_lastValue.length < value.length) {
+ m_lastValue = Arrays.copyOf(value, value.length);
+ } else {
+ System.arraycopy(value, 0, m_lastValue, 0, value.length);
+ }
+ m_lastValueLen = value.length;
+ }
+
+ private long[] m_lastValue;
+ private int m_lastValueLen;
}
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/IntegerLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/IntegerLogEntry.java
index 395a208f74..25f491cc03 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/IntegerLogEntry.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/IntegerLogEntry.java
@@ -71,4 +71,60 @@ public class IntegerLogEntry extends DataLogEntry {
public void append(long value) {
m_log.appendInteger(m_entry, value, 0);
}
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ *
Note: the last value is local to this class instance; using update() with two instances
+ * pointing to the same underlying log entry name will likely result in unexpected results.
+ *
+ * @param value Value to record
+ * @param timestamp Time stamp (0 to indicate now)
+ */
+ public synchronized void update(long value, long timestamp) {
+ if (!m_hasLastValue || m_lastValue != value) {
+ m_lastValue = value;
+ m_hasLastValue = true;
+ append(value, timestamp);
+ }
+ }
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ *
Note: the last value is local to this class instance; using update() with two instances
+ * pointing to the same underlying log entry name will likely result in unexpected results.
+ *
+ * @param value Value to record
+ */
+ public void update(long value) {
+ update(value, 0);
+ }
+
+ /**
+ * Gets whether there is a last value.
+ *
+ *
Note: the last value is local to this class instance and updated only with update(), not
+ * append().
+ *
+ * @return True if last value exists, false otherwise.
+ */
+ public synchronized boolean hasLastValue() {
+ return m_hasLastValue;
+ }
+
+ /**
+ * Gets the last value.
+ *
+ *
Note: the last value is local to this class instance and updated only with update(), not
+ * append().
+ *
+ * @return Last value, or 0 if none.
+ */
+ public synchronized long getLastValue() {
+ return m_lastValue;
+ }
+
+ boolean m_hasLastValue;
+ long m_lastValue;
}
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/ProtobufLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/ProtobufLogEntry.java
index 9e7fa43b34..9108e56bbf 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/ProtobufLogEntry.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/ProtobufLogEntry.java
@@ -20,6 +20,8 @@ public final class ProtobufLogEntry extends DataLogEntry {
DataLog log, String name, Protobuf proto, String metadata, long timestamp) {
super(log, name, proto.getTypeString(), metadata, timestamp);
m_buf = ProtobufBuffer.create(proto);
+ m_immutable = proto.isImmutable();
+ m_cloneable = proto.isCloneable();
log.addSchema(proto, timestamp);
}
@@ -113,5 +115,126 @@ public final class ProtobufLogEntry extends DataLogEntry {
append(value, 0);
}
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ * Note: the last value is local to this class instance; using update() with two instances
+ * pointing to the same underlying log entry name will likely result in unexpected results.
+ *
+ * @param value Value to record
+ * @param timestamp Time stamp (0 to indicate now)
+ */
+ public void update(T value, long timestamp) {
+ try {
+ synchronized (m_buf) {
+ if (m_immutable || m_cloneable) {
+ if (value.equals(m_lastValue)) {
+ return;
+ }
+ try {
+ if (m_immutable) {
+ m_lastValue = value;
+ } else {
+ m_lastValue = m_buf.getProto().clone(value);
+ }
+ ByteBuffer bb = m_buf.write(value);
+ m_log.appendRaw(m_entry, bb, 0, bb.position(), timestamp);
+ return;
+ } catch (CloneNotSupportedException e) {
+ // fall through
+ }
+ }
+ doUpdate(m_buf.write(value), timestamp);
+ }
+ } catch (IOException e) {
+ // ignore
+ }
+ }
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ *
Note: the last value is local to this class instance; using update() with two instances
+ * pointing to the same underlying log entry name will likely result in unexpected results.
+ *
+ * @param value Value to record
+ */
+ public void update(T value) {
+ update(value, 0);
+ }
+
+ /**
+ * Gets whether there is a last value.
+ *
+ *
Note: the last value is local to this class instance and updated only with update(), not
+ * append().
+ *
+ * @return True if last value exists, false otherwise.
+ */
+ public boolean hasLastValue() {
+ synchronized (m_buf) {
+ if (m_immutable) {
+ return m_lastValue != null;
+ } else if (m_cloneable && m_lastValue != null) {
+ return true;
+ }
+ return m_lastValueBuf != null;
+ }
+ }
+
+ /**
+ * Gets the last value.
+ *
+ *
Note: the last value is local to this class instance and updated only with update(), not
+ * append().
+ *
+ * @return Last value, or null if none.
+ */
+ public T getLastValue() {
+ synchronized (m_buf) {
+ if (m_immutable) {
+ return m_lastValue;
+ } else if (m_cloneable && m_lastValue != null) {
+ try {
+ return m_buf.getProto().clone(m_lastValue);
+ } catch (CloneNotSupportedException e) {
+ // fall through
+ }
+ }
+ if (m_lastValueBuf == null) {
+ return null;
+ }
+ try {
+ T val = m_buf.read(m_lastValueBuf);
+ m_lastValueBuf.position(0);
+ return val;
+ } catch (IOException e) {
+ return null;
+ }
+ }
+ }
+
+ private void doUpdate(ByteBuffer bb, long timestamp) {
+ int len = bb.position();
+ bb.limit(len);
+ bb.position(0);
+ if (m_lastValueBuf == null || !bb.equals(m_lastValueBuf)) {
+ // update last value
+ if (m_lastValueBuf == null || m_lastValueBuf.limit() < len) {
+ m_lastValueBuf = ByteBuffer.allocate(len);
+ }
+ bb.get(m_lastValueBuf.array(), 0, len);
+ bb.position(0);
+ m_lastValueBuf.limit(len);
+
+ // append to log
+ m_log.appendRaw(m_entry, bb, 0, len, timestamp);
+ }
+ }
+
private final ProtobufBuffer m_buf;
+ private ByteBuffer m_lastValueBuf;
+ private final boolean m_immutable;
+ private final boolean m_cloneable;
+ private T m_lastValue;
}
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/RawLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/RawLogEntry.java
index a9e3373335..9ab59e9659 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/RawLogEntry.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/RawLogEntry.java
@@ -5,6 +5,7 @@
package edu.wpi.first.util.datalog;
import java.nio.ByteBuffer;
+import java.util.Arrays;
/** Log raw byte array values. */
public class RawLogEntry extends DataLogEntry {
@@ -125,7 +126,7 @@ public class RawLogEntry extends DataLogEntry {
/**
* Appends a record to the log.
*
- * @param value Data to record; will send from value.position() to value.capacity()
+ * @param value Data to record; will send from value.position() to value.limit()
* @param timestamp Time stamp (0 to indicate now)
*/
public void append(ByteBuffer value, long timestamp) {
@@ -135,7 +136,7 @@ public class RawLogEntry extends DataLogEntry {
/**
* Appends a record to the log.
*
- * @param value Data to record; will send from value.position() to value.capacity()
+ * @param value Data to record; will send from value.position() to value.limit()
*/
public void append(ByteBuffer value) {
append(value, 0);
@@ -163,4 +164,204 @@ public class RawLogEntry extends DataLogEntry {
public void append(ByteBuffer value, int start, int len) {
append(value, start, len, 0);
}
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ * Note: the last value is local to this class instance; using update() with two instances
+ * pointing to the same underlying log entry name will likely result in unexpected results.
+ *
+ * @param value Value to record; will send entire array contents
+ * @param timestamp Time stamp (0 to indicate now)
+ */
+ public synchronized void update(byte[] value, long timestamp) {
+ if (!equalsLast(value, 0, value.length)) {
+ copyToLast(value, 0, value.length);
+ append(value, timestamp);
+ }
+ }
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ *
Note: the last value is local to this class instance; using update() with two instances
+ * pointing to the same underlying log entry name will likely result in unexpected results.
+ *
+ * @param value Value to record; will send entire array contents
+ */
+ public void update(byte[] value) {
+ update(value, 0);
+ }
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ *
Note: the last value is local to this class instance; using update() with two instances
+ * pointing to the same underlying log entry name will likely result in unexpected results.
+ *
+ * @param value Data to record
+ * @param start Start position of data (in byte array)
+ * @param len Length of data (must be less than or equal to value.length - offset)
+ * @param timestamp Time stamp (0 to indicate now)
+ */
+ public synchronized void update(byte[] value, int start, int len, long timestamp) {
+ if (!equalsLast(value, start, len)) {
+ copyToLast(value, start, len);
+ append(value, start, len, timestamp);
+ }
+ }
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ *
Note: the last value is local to this class instance; using update() with two instances
+ * pointing to the same underlying log entry name will likely result in unexpected results.
+ *
+ * @param value Data to record
+ * @param start Start position of data (in byte array)
+ * @param len Length of data (must be less than or equal to value.length - offset)
+ */
+ public void update(byte[] value, int start, int len) {
+ update(value, start, len, 0);
+ }
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ *
Note: the last value is local to this class instance; using update() with two instances
+ * pointing to the same underlying log entry name will likely result in unexpected results.
+ *
+ * @param value Data to record; will send from value.position() to value.limit()
+ * @param timestamp Time stamp (0 to indicate now)
+ */
+ public synchronized void update(ByteBuffer value, long timestamp) {
+ if (!equalsLast(value)) {
+ int start = value.position();
+ int len = value.limit() - start;
+ copyToLast(value, start, len);
+ append(value, start, len, timestamp);
+ }
+ }
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ *
Note: the last value is local to this class instance; using update() with two instances
+ * pointing to the same underlying log entry name will likely result in unexpected results.
+ *
+ * @param value Data to record; will send from value.position() to value.limit()
+ */
+ public void update(ByteBuffer value) {
+ update(value, 0);
+ }
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ *
Note: the last value is local to this class instance; using update() with two instances
+ * pointing to the same underlying log entry name will likely result in unexpected results.
+ *
+ * @param value Data to record
+ * @param start Start position of data (in value buffer)
+ * @param len Length of data (must be less than or equal to value.length - offset)
+ * @param timestamp Time stamp (0 to indicate now)
+ */
+ public synchronized void update(ByteBuffer value, int start, int len, long timestamp) {
+ if (!equalsLast(value, start, len)) {
+ copyToLast(value, start, len);
+ append(value, start, len, timestamp);
+ }
+ }
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ *
Note: the last value is local to this class instance; using update() with two instances
+ * pointing to the same underlying log entry name will likely result in unexpected results.
+ *
+ * @param value Data to record
+ * @param start Start position of data (in value buffer)
+ * @param len Length of data (must be less than or equal to value.length - offset)
+ */
+ public void update(ByteBuffer value, int start, int len) {
+ update(value, start, len, 0);
+ }
+
+ /**
+ * Gets whether there is a last value.
+ *
+ *
Note: the last value is local to this class instance and updated only with update(), not
+ * append().
+ *
+ * @return True if last value exists, false otherwise.
+ */
+ public synchronized boolean hasLastValue() {
+ return m_lastValue != null;
+ }
+
+ /**
+ * Gets the last value.
+ *
+ *
Note: the last value is local to this class instance and updated only with update(), not
+ * append().
+ *
+ * @return Last value, or null if none.
+ */
+ @SuppressWarnings("PMD.ReturnEmptyCollectionRatherThanNull")
+ public synchronized byte[] getLastValue() {
+ if (m_lastValue == null) {
+ return null;
+ }
+ return Arrays.copyOf(m_lastValue.array(), m_lastValue.limit());
+ }
+
+ private boolean equalsLast(byte[] value, int start, int len) {
+ if (m_lastValue == null || m_lastValue.limit() != len) {
+ return false;
+ }
+ return Arrays.equals(m_lastValue.array(), 0, len, value, start, start + len);
+ }
+
+ @SuppressWarnings("PMD.SimplifyBooleanReturns")
+ private boolean equalsLast(ByteBuffer value) {
+ if (m_lastValue == null) {
+ return false;
+ }
+ return value.equals(m_lastValue);
+ }
+
+ private boolean equalsLast(ByteBuffer value, int start, int len) {
+ if (m_lastValue == null || m_lastValue.limit() != len) {
+ return false;
+ }
+ int origpos = value.position();
+ value.position(start);
+ int origlimit = value.limit();
+ value.limit(start + len);
+ boolean eq = value.equals(m_lastValue);
+ value.position(origpos);
+ value.limit(origlimit);
+ return eq;
+ }
+
+ private void copyToLast(byte[] value, int start, int len) {
+ if (m_lastValue == null || m_lastValue.limit() < len) {
+ m_lastValue = ByteBuffer.allocate(len);
+ }
+ System.arraycopy(value, start, m_lastValue.array(), 0, len);
+ m_lastValue.limit(len);
+ }
+
+ private void copyToLast(ByteBuffer value, int start, int len) {
+ if (m_lastValue == null || m_lastValue.limit() < len) {
+ m_lastValue = ByteBuffer.allocate(len);
+ }
+ int origpos = value.position();
+ value.position(start);
+ value.get(m_lastValue.array(), 0, len);
+ value.position(origpos);
+ m_lastValue.limit(len);
+ }
+
+ private ByteBuffer m_lastValue;
}
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/StringArrayLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/StringArrayLogEntry.java
index f0a6dde737..0218502fc0 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/StringArrayLogEntry.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/StringArrayLogEntry.java
@@ -4,6 +4,8 @@
package edu.wpi.first.util.datalog;
+import java.util.Arrays;
+
/** Log array of string values. */
public class StringArrayLogEntry extends DataLogEntry {
/** The data type for string array values. */
@@ -71,4 +73,79 @@ public class StringArrayLogEntry extends DataLogEntry {
public void append(String[] value) {
m_log.appendStringArray(m_entry, value, 0);
}
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ *
Note: the last value is local to this class instance; using update() with two instances
+ * pointing to the same underlying log entry name will likely result in unexpected results.
+ *
+ * @param value Value to record
+ * @param timestamp Time stamp (0 to indicate now)
+ */
+ public synchronized void update(String[] value, long timestamp) {
+ if (!equalsLast(value)) {
+ copyToLast(value);
+ append(value, timestamp);
+ }
+ }
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ *
Note: the last value is local to this class instance; using update() with two instances
+ * pointing to the same underlying log entry name will likely result in unexpected results.
+ *
+ * @param value Value to record
+ */
+ public void update(String[] value) {
+ update(value, 0);
+ }
+
+ /**
+ * Gets whether there is a last value.
+ *
+ *
Note: the last value is local to this class instance and updated only with update(), not
+ * append().
+ *
+ * @return True if last value exists, false otherwise.
+ */
+ public synchronized boolean hasLastValue() {
+ return m_lastValue != null;
+ }
+
+ /**
+ * Gets the last value.
+ *
+ *
Note: the last value is local to this class instance and updated only with update(), not
+ * append().
+ *
+ * @return Last value, or false if none.
+ */
+ @SuppressWarnings("PMD.ReturnEmptyCollectionRatherThanNull")
+ public synchronized String[] getLastValue() {
+ if (m_lastValue == null) {
+ return null;
+ }
+ return Arrays.copyOf(m_lastValue, m_lastValueLen);
+ }
+
+ private boolean equalsLast(String[] value) {
+ if (m_lastValue == null || m_lastValueLen != value.length) {
+ return false;
+ }
+ return Arrays.equals(m_lastValue, 0, value.length, value, 0, value.length);
+ }
+
+ private void copyToLast(String[] value) {
+ if (m_lastValue == null || m_lastValue.length < value.length) {
+ m_lastValue = Arrays.copyOf(value, value.length);
+ } else {
+ System.arraycopy(value, 0, m_lastValue, 0, value.length);
+ }
+ m_lastValueLen = value.length;
+ }
+
+ private String[] m_lastValue;
+ private int m_lastValueLen;
}
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/StringLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/StringLogEntry.java
index 27c8aefef5..523abd6646 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/StringLogEntry.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/StringLogEntry.java
@@ -96,4 +96,58 @@ public class StringLogEntry extends DataLogEntry {
public void append(String value) {
m_log.appendString(m_entry, value, 0);
}
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ *
Note: the last value is local to this class instance; using update() with two instances
+ * pointing to the same underlying log entry name will likely result in unexpected results.
+ *
+ * @param value Value to record
+ * @param timestamp Time stamp (0 to indicate now)
+ */
+ public synchronized void update(String value, long timestamp) {
+ if (m_lastValue == null || !value.equals(m_lastValue)) {
+ m_lastValue = value;
+ append(value, timestamp);
+ }
+ }
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ *
Note: the last value is local to this class instance; using update() with two instances
+ * pointing to the same underlying log entry name will likely result in unexpected results.
+ *
+ * @param value Value to record
+ */
+ public void update(String value) {
+ update(value, 0);
+ }
+
+ /**
+ * Gets whether there is a last value.
+ *
+ *
Note: the last value is local to this class instance and updated only with update(), not
+ * append().
+ *
+ * @return True if last value exists, false otherwise.
+ */
+ public synchronized boolean hasLastValue() {
+ return m_lastValue != null;
+ }
+
+ /**
+ * Gets the last value.
+ *
+ *
Note: the last value is local to this class instance and updated only with update(), not
+ * append().
+ *
+ * @return Last value, or null if none.
+ */
+ public synchronized String getLastValue() {
+ return m_lastValue;
+ }
+
+ private String m_lastValue;
}
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/StructArrayLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/StructArrayLogEntry.java
index e208f2c0d0..43d69b10f1 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/StructArrayLogEntry.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/StructArrayLogEntry.java
@@ -6,7 +6,9 @@ package edu.wpi.first.util.datalog;
import edu.wpi.first.util.struct.Struct;
import edu.wpi.first.util.struct.StructBuffer;
+import java.lang.reflect.Array;
import java.nio.ByteBuffer;
+import java.util.Arrays;
import java.util.Collection;
/**
@@ -19,6 +21,8 @@ public final class StructArrayLogEntry extends DataLogEntry {
DataLog log, String name, Struct struct, String metadata, long timestamp) {
super(log, name, struct.getTypeString() + "[]", metadata, timestamp);
m_buf = StructBuffer.create(struct);
+ m_immutable = struct.isImmutable();
+ m_cloneable = struct.isCloneable();
log.addSchema(struct, timestamp);
}
@@ -87,7 +91,7 @@ public final class StructArrayLogEntry extends DataLogEntry {
* @param nelem number of elements
*/
public void reserve(int nelem) {
- synchronized (this) {
+ synchronized (m_buf) {
m_buf.reserve(nelem);
}
}
@@ -99,7 +103,7 @@ public final class StructArrayLogEntry extends DataLogEntry {
* @param timestamp Time stamp (0 to indicate now)
*/
public void append(T[] value, long timestamp) {
- synchronized (this) {
+ synchronized (m_buf) {
ByteBuffer bb = m_buf.writeArray(value);
m_log.appendRaw(m_entry, bb, 0, bb.position(), timestamp);
}
@@ -121,7 +125,7 @@ public final class StructArrayLogEntry extends DataLogEntry {
* @param timestamp Time stamp (0 to indicate now)
*/
public void append(Collection value, long timestamp) {
- synchronized (this) {
+ synchronized (m_buf) {
ByteBuffer bb = m_buf.writeArray(value);
m_log.appendRaw(m_entry, bb, 0, bb.position(), timestamp);
}
@@ -136,5 +140,242 @@ public final class StructArrayLogEntry extends DataLogEntry {
append(value, 0);
}
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ * Note: the last value is local to this class instance; using update() with two instances
+ * pointing to the same underlying log entry name will likely result in unexpected results.
+ *
+ * @param value Value to record
+ * @param timestamp Time stamp (0 to indicate now)
+ */
+ public void update(T[] value, long timestamp) {
+ synchronized (m_buf) {
+ if (m_immutable || m_cloneable) {
+ if (m_lastValue != null
+ && m_lastValueLen == value.length
+ && Arrays.equals(m_lastValue, 0, value.length, value, 0, value.length)) {
+ return;
+ }
+ try {
+ copyToLast(value);
+ ByteBuffer bb = m_buf.writeArray(value);
+ m_log.appendRaw(m_entry, bb, 0, bb.position(), timestamp);
+ return;
+ } catch (CloneNotSupportedException e) {
+ // fall through
+ }
+ }
+ doUpdate(m_buf.writeArray(value), timestamp);
+ }
+ }
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ *
Note: the last value is local to this class instance; using update() with two instances
+ * pointing to the same underlying log entry name will likely result in unexpected results.
+ *
+ * @param value Value to record
+ */
+ public void update(T[] value) {
+ update(value, 0);
+ }
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ *
Note: the last value is local to this class instance; using update() with two instances
+ * pointing to the same underlying log entry name will likely result in unexpected results.
+ *
+ * @param value Value to record
+ * @param timestamp Time stamp (0 to indicate now)
+ */
+ public void update(Collection value, long timestamp) {
+ synchronized (m_buf) {
+ if (m_immutable || m_cloneable) {
+ if (equalsLast(value)) {
+ return;
+ }
+ try {
+ copyToLast(value);
+ ByteBuffer bb = m_buf.writeArray(value);
+ m_log.appendRaw(m_entry, bb, 0, bb.position(), timestamp);
+ return;
+ } catch (CloneNotSupportedException e) {
+ // fall through
+ }
+ }
+ doUpdate(m_buf.writeArray(value), timestamp);
+ }
+ }
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ * Note: the last value is local to this class instance; using update() with two instances
+ * pointing to the same underlying log entry name will likely result in unexpected results.
+ *
+ * @param value Value to record
+ */
+ public void update(Collection value) {
+ update(value, 0);
+ }
+
+ /**
+ * Gets whether there is a last value.
+ *
+ * Note: the last value is local to this class instance and updated only with update(), not
+ * append().
+ *
+ * @return True if last value exists, false otherwise.
+ */
+ public boolean hasLastValue() {
+ synchronized (m_buf) {
+ if (m_immutable) {
+ return m_lastValue != null;
+ } else if (m_cloneable && m_lastValue != null) {
+ return true;
+ }
+ return m_lastValueBuf != null;
+ }
+ }
+
+ /**
+ * Gets the last value.
+ *
+ *
Note: the last value is local to this class instance and updated only with update(), not
+ * append().
+ *
+ * @return Last value, or null if none.
+ */
+ @SuppressWarnings("PMD.ReturnEmptyCollectionRatherThanNull")
+ public T[] getLastValue() {
+ synchronized (m_buf) {
+ if (m_immutable) {
+ if (m_lastValue == null) {
+ return null;
+ }
+ return Arrays.copyOf(m_lastValue, m_lastValueLen);
+ } else if (m_cloneable && m_lastValue != null) {
+ try {
+ return cloneArray(m_lastValue, m_lastValueLen);
+ } catch (CloneNotSupportedException e) {
+ // fall through
+ }
+ }
+ if (m_lastValueBuf == null) {
+ return null;
+ }
+ T[] val = m_buf.readArray(m_lastValueBuf);
+ m_lastValueBuf.position(0);
+ return val;
+ }
+ }
+
+ private void doUpdate(ByteBuffer bb, long timestamp) {
+ int len = bb.position();
+ bb.limit(len);
+ bb.position(0);
+ if (m_lastValueBuf == null || !bb.equals(m_lastValueBuf)) {
+ // update last value
+ if (m_lastValueBuf == null || m_lastValueBuf.limit() < len) {
+ m_lastValueBuf = ByteBuffer.allocate(len);
+ }
+ bb.get(m_lastValueBuf.array(), 0, len);
+ bb.position(0);
+ m_lastValueBuf.limit(len);
+
+ // append to log
+ m_log.appendRaw(m_entry, bb, 0, len, timestamp);
+ }
+ }
+
+ private boolean equalsLast(Collection arr) {
+ if (m_lastValue == null) {
+ return false;
+ }
+ if (m_lastValueLen != arr.size()) {
+ return false;
+ }
+ int i = 0;
+ for (T elem : arr) {
+ if (!m_lastValue[i].equals(elem)) {
+ return false;
+ }
+ i++;
+ }
+ return true;
+ }
+
+ private T[] cloneArray(T[] in, int len) throws CloneNotSupportedException {
+ Struct s = m_buf.getStruct();
+ @SuppressWarnings("unchecked")
+ T[] arr = (T[]) Array.newInstance(s.getTypeClass(), len);
+ for (int i = 0; i < len; i++) {
+ arr[i] = s.clone(in[i]);
+ }
+ return arr;
+ }
+
+ private T[] cloneArray(Collection in) throws CloneNotSupportedException {
+ Struct s = m_buf.getStruct();
+ @SuppressWarnings("unchecked")
+ T[] arr = (T[]) Array.newInstance(s.getTypeClass(), in.size());
+ int i = 0;
+ for (T elem : in) {
+ arr[i++] = s.clone(elem);
+ }
+ return arr;
+ }
+
+ private void copyToLast(T[] value) throws CloneNotSupportedException {
+ if (m_lastValue == null || m_lastValue.length < value.length) {
+ if (m_immutable) {
+ m_lastValue = Arrays.copyOf(value, value.length);
+ } else {
+ m_lastValue = cloneArray(value, value.length);
+ }
+ } else {
+ if (m_immutable) {
+ System.arraycopy(value, 0, m_lastValue, 0, value.length);
+ } else {
+ Struct s = m_buf.getStruct();
+ for (int i = 0; i < value.length; i++) {
+ m_lastValue[i] = s.clone(value[i]);
+ }
+ }
+ }
+ m_lastValueLen = value.length;
+ }
+
+ private void copyToLast(Collection value) throws CloneNotSupportedException {
+ if (m_lastValue == null || m_lastValue.length < value.size()) {
+ if (m_immutable) {
+ Struct s = m_buf.getStruct();
+ @SuppressWarnings("unchecked")
+ T[] arr = (T[]) Array.newInstance(s.getTypeClass(), value.size());
+ int i = 0;
+ for (T elem : value) {
+ arr[i++] = elem;
+ }
+ } else {
+ m_lastValue = cloneArray(value);
+ }
+ } else {
+ Struct s = m_buf.getStruct();
+ int i = 0;
+ for (T elem : value) {
+ m_lastValue[i++] = m_immutable ? elem : s.clone(elem);
+ }
+ }
+ m_lastValueLen = value.size();
+ }
+
private final StructBuffer m_buf;
+ private ByteBuffer m_lastValueBuf;
+ private final boolean m_immutable;
+ private final boolean m_cloneable;
+ private T[] m_lastValue;
+ private int m_lastValueLen;
}
diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/StructLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/StructLogEntry.java
index 0d09182315..c7bdf0a858 100644
--- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/StructLogEntry.java
+++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/StructLogEntry.java
@@ -18,6 +18,8 @@ public final class StructLogEntry extends DataLogEntry {
DataLog log, String name, Struct struct, String metadata, long timestamp) {
super(log, name, struct.getTypeString(), metadata, timestamp);
m_buf = StructBuffer.create(struct);
+ m_immutable = struct.isImmutable();
+ m_cloneable = struct.isCloneable();
log.addSchema(struct, timestamp);
}
@@ -102,5 +104,118 @@ public final class StructLogEntry extends DataLogEntry {
append(value, 0);
}
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ * Note: the last value is local to this class instance; using update() with two instances
+ * pointing to the same underlying log entry name will likely result in unexpected results.
+ *
+ * @param value Value to record
+ * @param timestamp Time stamp (0 to indicate now)
+ */
+ public void update(T value, long timestamp) {
+ synchronized (m_buf) {
+ if (m_immutable || m_cloneable) {
+ if (value.equals(m_lastValue)) {
+ return;
+ }
+ try {
+ if (m_immutable) {
+ m_lastValue = value;
+ } else {
+ m_lastValue = m_buf.getStruct().clone(value);
+ }
+ ByteBuffer bb = m_buf.write(value);
+ m_log.appendRaw(m_entry, bb, 0, bb.position(), timestamp);
+ return;
+ } catch (CloneNotSupportedException e) {
+ // fall through
+ }
+ }
+ doUpdate(m_buf.write(value), timestamp);
+ }
+ }
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ *
Note: the last value is local to this class instance; using update() with two instances
+ * pointing to the same underlying log entry name will likely result in unexpected results.
+ *
+ * @param value Value to record
+ */
+ public void update(T value) {
+ update(value, 0);
+ }
+
+ /**
+ * Gets whether there is a last value.
+ *
+ *
Note: the last value is local to this class instance and updated only with update(), not
+ * append().
+ *
+ * @return True if last value exists, false otherwise.
+ */
+ public boolean hasLastValue() {
+ synchronized (m_buf) {
+ if (m_immutable) {
+ return m_lastValue != null;
+ } else if (m_cloneable && m_lastValue != null) {
+ return true;
+ }
+ return m_lastValueBuf != null;
+ }
+ }
+
+ /**
+ * Gets the last value.
+ *
+ *
Note: the last value is local to this class instance and updated only with update(), not
+ * append().
+ *
+ * @return Last value, or null if none.
+ */
+ public T getLastValue() {
+ synchronized (m_buf) {
+ if (m_immutable) {
+ return m_lastValue;
+ } else if (m_cloneable && m_lastValue != null) {
+ try {
+ return m_buf.getStruct().clone(m_lastValue);
+ } catch (CloneNotSupportedException e) {
+ // fall through
+ }
+ }
+ if (m_lastValueBuf == null) {
+ return null;
+ }
+ T val = m_buf.read(m_lastValueBuf);
+ m_lastValueBuf.position(0);
+ return val;
+ }
+ }
+
+ private void doUpdate(ByteBuffer bb, long timestamp) {
+ int len = bb.position();
+ bb.limit(len);
+ bb.position(0);
+ if (m_lastValueBuf == null || !bb.equals(m_lastValueBuf)) {
+ // update last value
+ if (m_lastValueBuf == null || m_lastValueBuf.limit() < len) {
+ m_lastValueBuf = ByteBuffer.allocate(len);
+ }
+ bb.get(m_lastValueBuf.array(), 0, len);
+ bb.position(0);
+ m_lastValueBuf.limit(len);
+
+ // append to log
+ m_log.appendRaw(m_entry, bb, 0, len, timestamp);
+ }
+ }
+
private final StructBuffer m_buf;
+ private ByteBuffer m_lastValueBuf;
+ private final boolean m_immutable;
+ private final boolean m_cloneable;
+ private T m_lastValue;
}
diff --git a/wpiutil/src/main/native/cpp/DataLog.cpp b/wpiutil/src/main/native/cpp/DataLog.cpp
index 5234c8e705..dd717cc6c0 100644
--- a/wpiutil/src/main/native/cpp/DataLog.cpp
+++ b/wpiutil/src/main/native/cpp/DataLog.cpp
@@ -4,6 +4,7 @@
#include "wpi/DataLog.h"
+#include
#include
#include
#include
@@ -618,6 +619,107 @@ void DataLog::AppendStringArray(int entry,
}
}
+template
+inline bool UpdateImpl(std::optional>& lastValue,
+ std::span data) {
+ if (!lastValue || !std::equal(data.begin(), data.end(), lastValue->begin(),
+ lastValue->end())) {
+ if (lastValue) {
+ lastValue->assign(data.begin(), data.end());
+ } else {
+ lastValue = std::vector{data.begin(), data.end()};
+ }
+ return true;
+ }
+ return false;
+}
+
+template
+inline bool UpdateImpl(std::optional>& lastValue,
+ std::span data) {
+ if (!lastValue || !std::equal(data.begin(), data.end(), lastValue->begin(),
+ lastValue->end(), [](auto a, auto b) {
+ return a == static_cast(b);
+ })) {
+ if (lastValue) {
+ lastValue->assign(data.begin(), data.end());
+ } else {
+ lastValue = std::vector{data.begin(), data.end()};
+ }
+ return true;
+ }
+ return false;
+}
+
+void RawLogEntry::Update(std::span data, int64_t timestamp) {
+ std::scoped_lock lock{m_mutex};
+ if (UpdateImpl(m_lastValue, data)) {
+ Append(data, timestamp);
+ }
+}
+
+void BooleanArrayLogEntry::Update(std::span arr,
+ int64_t timestamp) {
+ std::scoped_lock lock{m_mutex};
+ if (UpdateImpl(m_lastValue, arr)) {
+ Append(arr, timestamp);
+ }
+}
+
+void BooleanArrayLogEntry::Update(std::span arr, int64_t timestamp) {
+ std::scoped_lock lock{m_mutex};
+ if (UpdateImpl(m_lastValue, arr)) {
+ Append(arr, timestamp);
+ }
+}
+
+void BooleanArrayLogEntry::Update(std::span arr,
+ int64_t timestamp) {
+ std::scoped_lock lock{m_mutex};
+ if (UpdateImpl(m_lastValue, arr)) {
+ Append(arr, timestamp);
+ }
+}
+
+void IntegerArrayLogEntry::Update(std::span arr,
+ int64_t timestamp) {
+ std::scoped_lock lock{m_mutex};
+ if (UpdateImpl(m_lastValue, arr)) {
+ Append(arr, timestamp);
+ }
+}
+
+void FloatArrayLogEntry::Update(std::span arr, int64_t timestamp) {
+ std::scoped_lock lock{m_mutex};
+ if (UpdateImpl(m_lastValue, arr)) {
+ Append(arr, timestamp);
+ }
+}
+
+void DoubleArrayLogEntry::Update(std::span arr,
+ int64_t timestamp) {
+ std::scoped_lock lock{m_mutex};
+ if (UpdateImpl(m_lastValue, arr)) {
+ Append(arr, timestamp);
+ }
+}
+
+void StringArrayLogEntry::Update(std::span arr,
+ int64_t timestamp) {
+ std::scoped_lock lock{m_mutex};
+ if (UpdateImpl(m_lastValue, arr)) {
+ Append(arr, timestamp);
+ }
+}
+
+void StringArrayLogEntry::Update(std::span arr,
+ int64_t timestamp) {
+ std::scoped_lock lock{m_mutex};
+ if (UpdateImpl(m_lastValue, arr)) {
+ Append(arr, timestamp);
+ }
+}
+
extern "C" {
void WPI_DataLog_Release(struct WPI_DataLog* datalog) {
diff --git a/wpiutil/src/main/native/cpp/jni/DataLogJNI.cpp b/wpiutil/src/main/native/cpp/jni/DataLogJNI.cpp
index 9ac6b3b58a..e98c13f8df 100644
--- a/wpiutil/src/main/native/cpp/jni/DataLogJNI.cpp
+++ b/wpiutil/src/main/native/cpp/jni/DataLogJNI.cpp
@@ -4,6 +4,7 @@
#include
+#include
#include
#include
@@ -18,6 +19,18 @@
using namespace wpi::java;
using namespace wpi::log;
+namespace {
+class buf_ostream : public wpi::raw_uvector_ostream {
+ private:
+ std::vector data;
+
+ public:
+ buf_ostream() : raw_uvector_ostream{data} {}
+
+ void clear() { data.clear(); }
+};
+} // namespace
+
extern "C" {
/*
@@ -96,6 +109,24 @@ Java_edu_wpi_first_util_datalog_DataLogJNI_fgCreate
return reinterpret_cast(writer);
}
+/*
+ * Class: edu_wpi_first_util_datalog_DataLogJNI
+ * Method: fgCreateMemory
+ * Signature: (Ljava/lang/String;)J
+ */
+JNIEXPORT jlong JNICALL
+Java_edu_wpi_first_util_datalog_DataLogJNI_fgCreateMemory
+ (JNIEnv* env, jclass, jstring extraHeader)
+{
+ if (!extraHeader) {
+ wpi::ThrowNullPointerException(env, "extraHeader is null");
+ return 0;
+ }
+ auto writer = new DataLogWriter{std::make_unique(),
+ JStringRef{env, extraHeader}};
+ return reinterpret_cast(writer);
+}
+
/*
* Class: edu_wpi_first_util_datalog_DataLogJNI
* Method: flush
@@ -109,7 +140,34 @@ Java_edu_wpi_first_util_datalog_DataLogJNI_flush
wpi::ThrowNullPointerException(env, "impl is null");
return;
}
- reinterpret_cast(impl)->Flush();
+ reinterpret_cast(impl)->Flush();
+}
+
+/*
+ * Class: edu_wpi_first_util_datalog_DataLogJNI
+ * Method: copyWriteBuffer
+ * Signature: (J[BI)I
+ */
+JNIEXPORT jint JNICALL
+Java_edu_wpi_first_util_datalog_DataLogJNI_copyWriteBuffer
+ (JNIEnv* env, jclass, jlong impl, jbyteArray buf, jint start)
+{
+ if (impl == 0) {
+ wpi::ThrowNullPointerException(env, "impl is null");
+ return 0;
+ }
+ auto writer = reinterpret_cast(impl);
+ writer->Flush();
+ auto& stream = static_cast(writer->GetStream());
+ JSpan jbuf{env, buf};
+ auto arr = stream.array();
+ if (start < 0 || static_cast(start) >= arr.size()) {
+ stream.clear();
+ return 0;
+ }
+ size_t qty = (std::min)(jbuf.size(), arr.size() - start);
+ std::copy(arr.begin(), arr.begin() + qty, jbuf.begin());
+ return qty;
}
/*
@@ -125,7 +183,7 @@ Java_edu_wpi_first_util_datalog_DataLogJNI_pause
wpi::ThrowNullPointerException(env, "impl is null");
return;
}
- reinterpret_cast(impl)->Pause();
+ reinterpret_cast(impl)->Pause();
}
/*
@@ -141,7 +199,7 @@ Java_edu_wpi_first_util_datalog_DataLogJNI_resume
wpi::ThrowNullPointerException(env, "impl is null");
return;
}
- reinterpret_cast(impl)->Resume();
+ reinterpret_cast(impl)->Resume();
}
/*
@@ -157,7 +215,7 @@ Java_edu_wpi_first_util_datalog_DataLogJNI_stop
wpi::ThrowNullPointerException(env, "impl is null");
return;
}
- reinterpret_cast(impl)->Stop();
+ reinterpret_cast(impl)->Stop();
}
/*
diff --git a/wpiutil/src/main/native/include/wpi/DataLog.h b/wpiutil/src/main/native/include/wpi/DataLog.h
index 9fa209b244..4834b65dd0 100644
--- a/wpiutil/src/main/native/include/wpi/DataLog.h
+++ b/wpiutil/src/main/native/include/wpi/DataLog.h
@@ -6,8 +6,10 @@
#include
+#include
#include
#include
+#include
#include
#include
#include
@@ -575,10 +577,60 @@ class DataLogEntry {
int m_entry = 0;
};
+template
+class DataLogValueEntryImpl : public DataLogEntry {
+ protected:
+ DataLogValueEntryImpl() = default;
+ DataLogValueEntryImpl(DataLog& log, std::string_view name,
+ std::string_view type, std::string_view metadata = {},
+ int64_t timestamp = 0)
+ : DataLogEntry{log, name, type, metadata, timestamp} {}
+
+ public:
+ DataLogValueEntryImpl(DataLogValueEntryImpl&& rhs)
+ : DataLogEntry{std::move(rhs)} {
+ std::scoped_lock lock{rhs.m_mutex};
+ m_lastValue = std::move(rhs.m_lastValue);
+ }
+ DataLogValueEntryImpl& operator=(DataLogValueEntryImpl&& rhs) {
+ DataLogEntry::operator=(std::move(rhs));
+ std::scoped_lock lock{m_mutex, rhs.m_mutex};
+ m_lastValue = std::move(rhs.m_lastValue);
+ return *this;
+ }
+
+ /**
+ * Gets whether there is a last value.
+ *
+ * @note The last value is local to this class instance and updated only with
+ * Update(), not Append().
+ *
+ * @return True if last value exists, false otherwise.
+ */
+ bool HasLastValue() const { return m_lastValue.has_value(); }
+
+ /**
+ * Gets the last value.
+ *
+ * @note The last value is local to this class instance and updated only with
+ * Update(), not Append().
+ *
+ * @return Last value (empty if no last value)
+ */
+ std::optional GetLastValue() const {
+ std::scoped_lock lock{m_mutex};
+ return m_lastValue;
+ }
+
+ protected:
+ mutable wpi::mutex m_mutex;
+ std::optional m_lastValue;
+};
+
/**
* Log arbitrary byte data.
*/
-class RawLogEntry : public DataLogEntry {
+class RawLogEntry : public DataLogValueEntryImpl> {
public:
static constexpr std::string_view kDataType = "raw";
@@ -590,7 +642,7 @@ class RawLogEntry : public DataLogEntry {
: RawLogEntry{log, name, metadata, kDataType, timestamp} {}
RawLogEntry(DataLog& log, std::string_view name, std::string_view metadata,
std::string_view type, int64_t timestamp = 0)
- : DataLogEntry{log, name, type, metadata, timestamp} {}
+ : DataLogValueEntryImpl{log, name, type, metadata, timestamp} {}
/**
* Appends a record to the log.
@@ -601,12 +653,24 @@ class RawLogEntry : public DataLogEntry {
void Append(std::span data, int64_t timestamp = 0) {
m_log->AppendRaw(m_entry, data, timestamp);
}
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ * @note The last value is local to this class instance; using Update() with
+ * two instances pointing to the same underlying log entry name will likely
+ * result in unexpected results.
+ *
+ * @param data Data to record
+ * @param timestamp Time stamp (may be 0 to indicate now)
+ */
+ void Update(std::span data, int64_t timestamp = 0);
};
/**
* Log boolean values.
*/
-class BooleanLogEntry : public DataLogEntry {
+class BooleanLogEntry : public DataLogValueEntryImpl {
public:
static constexpr std::string_view kDataType = "boolean";
@@ -615,7 +679,7 @@ class BooleanLogEntry : public DataLogEntry {
: BooleanLogEntry{log, name, {}, timestamp} {}
BooleanLogEntry(DataLog& log, std::string_view name,
std::string_view metadata, int64_t timestamp = 0)
- : DataLogEntry{log, name, kDataType, metadata, timestamp} {}
+ : DataLogValueEntryImpl{log, name, kDataType, metadata, timestamp} {}
/**
* Appends a record to the log.
@@ -626,12 +690,30 @@ class BooleanLogEntry : public DataLogEntry {
void Append(bool value, int64_t timestamp = 0) {
m_log->AppendBoolean(m_entry, value, timestamp);
}
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ * @note The last value is local to this class instance; using Update() with
+ * two instances pointing to the same underlying log entry name will likely
+ * result in unexpected results.
+ *
+ * @param value Value to record
+ * @param timestamp Time stamp (may be 0 to indicate now)
+ */
+ void Update(bool value, int64_t timestamp = 0) {
+ std::scoped_lock lock{m_mutex};
+ if (m_lastValue != value) {
+ m_lastValue = value;
+ Append(value, timestamp);
+ }
+ }
};
/**
* Log integer values.
*/
-class IntegerLogEntry : public DataLogEntry {
+class IntegerLogEntry : public DataLogValueEntryImpl {
public:
static constexpr std::string_view kDataType = "int64";
@@ -640,7 +722,7 @@ class IntegerLogEntry : public DataLogEntry {
: IntegerLogEntry{log, name, {}, timestamp} {}
IntegerLogEntry(DataLog& log, std::string_view name,
std::string_view metadata, int64_t timestamp = 0)
- : DataLogEntry{log, name, kDataType, metadata, timestamp} {}
+ : DataLogValueEntryImpl{log, name, kDataType, metadata, timestamp} {}
/**
* Appends a record to the log.
@@ -651,12 +733,30 @@ class IntegerLogEntry : public DataLogEntry {
void Append(int64_t value, int64_t timestamp = 0) {
m_log->AppendInteger(m_entry, value, timestamp);
}
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ * @note The last value is local to this class instance; using Update() with
+ * two instances pointing to the same underlying log entry name will likely
+ * result in unexpected results.
+ *
+ * @param value Value to record
+ * @param timestamp Time stamp (may be 0 to indicate now)
+ */
+ void Update(int64_t value, int64_t timestamp = 0) {
+ std::scoped_lock lock{m_mutex};
+ if (m_lastValue != value) {
+ m_lastValue = value;
+ Append(value, timestamp);
+ }
+ }
};
/**
* Log float values.
*/
-class FloatLogEntry : public DataLogEntry {
+class FloatLogEntry : public DataLogValueEntryImpl {
public:
static constexpr std::string_view kDataType = "float";
@@ -665,7 +765,7 @@ class FloatLogEntry : public DataLogEntry {
: FloatLogEntry{log, name, {}, timestamp} {}
FloatLogEntry(DataLog& log, std::string_view name, std::string_view metadata,
int64_t timestamp = 0)
- : DataLogEntry{log, name, kDataType, metadata, timestamp} {}
+ : DataLogValueEntryImpl{log, name, kDataType, metadata, timestamp} {}
/**
* Appends a record to the log.
@@ -676,12 +776,30 @@ class FloatLogEntry : public DataLogEntry {
void Append(float value, int64_t timestamp = 0) {
m_log->AppendFloat(m_entry, value, timestamp);
}
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ * @note The last value is local to this class instance; using Update() with
+ * two instances pointing to the same underlying log entry name will likely
+ * result in unexpected results.
+ *
+ * @param value Value to record
+ * @param timestamp Time stamp (may be 0 to indicate now)
+ */
+ void Update(float value, int64_t timestamp = 0) {
+ std::scoped_lock lock{m_mutex};
+ if (m_lastValue != value) {
+ m_lastValue = value;
+ Append(value, timestamp);
+ }
+ }
};
/**
* Log double values.
*/
-class DoubleLogEntry : public DataLogEntry {
+class DoubleLogEntry : public DataLogValueEntryImpl {
public:
static constexpr std::string_view kDataType = "double";
@@ -690,7 +808,7 @@ class DoubleLogEntry : public DataLogEntry {
: DoubleLogEntry{log, name, {}, timestamp} {}
DoubleLogEntry(DataLog& log, std::string_view name, std::string_view metadata,
int64_t timestamp = 0)
- : DataLogEntry{log, name, kDataType, metadata, timestamp} {}
+ : DataLogValueEntryImpl{log, name, kDataType, metadata, timestamp} {}
/**
* Appends a record to the log.
@@ -701,12 +819,30 @@ class DoubleLogEntry : public DataLogEntry {
void Append(double value, int64_t timestamp = 0) {
m_log->AppendDouble(m_entry, value, timestamp);
}
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ * @note The last value is local to this class instance; using Update() with
+ * two instances pointing to the same underlying log entry name will likely
+ * result in unexpected results.
+ *
+ * @param value Value to record
+ * @param timestamp Time stamp (may be 0 to indicate now)
+ */
+ void Update(double value, int64_t timestamp = 0) {
+ std::scoped_lock lock{m_mutex};
+ if (m_lastValue != value) {
+ m_lastValue = value;
+ Append(value, timestamp);
+ }
+ }
};
/**
* Log string values.
*/
-class StringLogEntry : public DataLogEntry {
+class StringLogEntry : public DataLogValueEntryImpl {
public:
static constexpr const char* kDataType = "string";
@@ -718,7 +854,7 @@ class StringLogEntry : public DataLogEntry {
: StringLogEntry{log, name, metadata, kDataType, timestamp} {}
StringLogEntry(DataLog& log, std::string_view name, std::string_view metadata,
std::string_view type, int64_t timestamp = 0)
- : DataLogEntry{log, name, type, metadata, timestamp} {}
+ : DataLogValueEntryImpl{log, name, type, metadata, timestamp} {}
/**
* Appends a record to the log.
@@ -729,12 +865,30 @@ class StringLogEntry : public DataLogEntry {
void Append(std::string_view value, int64_t timestamp = 0) {
m_log->AppendString(m_entry, value, timestamp);
}
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ * @note The last value is local to this class instance; using Update() with
+ * two instances pointing to the same underlying log entry name will likely
+ * result in unexpected results.
+ *
+ * @param value Value to record
+ * @param timestamp Time stamp (may be 0 to indicate now)
+ */
+ void Update(std::string value, int64_t timestamp = 0) {
+ std::scoped_lock lock{m_mutex};
+ if (m_lastValue != value) {
+ m_lastValue = value;
+ Append(value, timestamp);
+ }
+ }
};
/**
* Log array of boolean values.
*/
-class BooleanArrayLogEntry : public DataLogEntry {
+class BooleanArrayLogEntry : public DataLogValueEntryImpl> {
public:
static constexpr const char* kDataType = "boolean[]";
@@ -744,7 +898,7 @@ class BooleanArrayLogEntry : public DataLogEntry {
: BooleanArrayLogEntry{log, name, {}, timestamp} {}
BooleanArrayLogEntry(DataLog& log, std::string_view name,
std::string_view metadata, int64_t timestamp = 0)
- : DataLogEntry{log, name, kDataType, metadata, timestamp} {}
+ : DataLogValueEntryImpl{log, name, kDataType, metadata, timestamp} {}
/**
* Appends a record to the log. For find functions to work, timestamp
@@ -796,12 +950,77 @@ class BooleanArrayLogEntry : public DataLogEntry {
void Append(std::span arr, int64_t timestamp = 0) {
m_log->AppendBooleanArray(m_entry, arr, timestamp);
}
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ * @note The last value is local to this class instance; using Update() with
+ * two instances pointing to the same underlying log entry name will likely
+ * result in unexpected results.
+ *
+ * @param arr Values to record
+ * @param timestamp Time stamp (may be 0 to indicate now)
+ */
+ void Update(std::span arr, int64_t timestamp = 0);
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ * @note The last value is local to this class instance; using Update() with
+ * two instances pointing to the same underlying log entry name will likely
+ * result in unexpected results.
+ *
+ * @param arr Values to record
+ * @param timestamp Time stamp (may be 0 to indicate now)
+ */
+ void Update(std::initializer_list arr, int64_t timestamp = 0) {
+ Update(std::span{arr.begin(), arr.end()}, timestamp);
+ }
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ * @note The last value is local to this class instance; using Update() with
+ * two instances pointing to the same underlying log entry name will likely
+ * result in unexpected results.
+ *
+ * @param arr Values to record
+ * @param timestamp Time stamp (may be 0 to indicate now)
+ */
+ void Update(std::span arr, int64_t timestamp = 0);
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ * @note The last value is local to this class instance; using Update() with
+ * two instances pointing to the same underlying log entry name will likely
+ * result in unexpected results.
+ *
+ * @param arr Values to record
+ * @param timestamp Time stamp (may be 0 to indicate now)
+ */
+ void Update(std::initializer_list arr, int64_t timestamp = 0) {
+ Update(std::span{arr.begin(), arr.end()}, timestamp);
+ }
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ * @note The last value is local to this class instance; using Update() with
+ * two instances pointing to the same underlying log entry name will likely
+ * result in unexpected results.
+ *
+ * @param arr Values to record
+ * @param timestamp Time stamp (may be 0 to indicate now)
+ */
+ void Update(std::span arr, int64_t timestamp = 0);
};
/**
* Log array of integer values.
*/
-class IntegerArrayLogEntry : public DataLogEntry {
+class IntegerArrayLogEntry
+ : public DataLogValueEntryImpl> {
public:
static constexpr const char* kDataType = "int64[]";
@@ -811,7 +1030,7 @@ class IntegerArrayLogEntry : public DataLogEntry {
: IntegerArrayLogEntry{log, name, {}, timestamp} {}
IntegerArrayLogEntry(DataLog& log, std::string_view name,
std::string_view metadata, int64_t timestamp = 0)
- : DataLogEntry{log, name, kDataType, metadata, timestamp} {}
+ : DataLogValueEntryImpl{log, name, kDataType, metadata, timestamp} {}
/**
* Appends a record to the log.
@@ -832,12 +1051,38 @@ class IntegerArrayLogEntry : public DataLogEntry {
void Append(std::initializer_list arr, int64_t timestamp = 0) {
Append({arr.begin(), arr.end()}, timestamp);
}
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ * @note The last value is local to this class instance; using Update() with
+ * two instances pointing to the same underlying log entry name will likely
+ * result in unexpected results.
+ *
+ * @param arr Values to record
+ * @param timestamp Time stamp (may be 0 to indicate now)
+ */
+ void Update(std::span arr, int64_t timestamp = 0);
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ * @note The last value is local to this class instance; using Update() with
+ * two instances pointing to the same underlying log entry name will likely
+ * result in unexpected results.
+ *
+ * @param arr Values to record
+ * @param timestamp Time stamp (may be 0 to indicate now)
+ */
+ void Update(std::initializer_list arr, int64_t timestamp = 0) {
+ Update({arr.begin(), arr.end()}, timestamp);
+ }
};
/**
* Log array of float values.
*/
-class FloatArrayLogEntry : public DataLogEntry {
+class FloatArrayLogEntry : public DataLogValueEntryImpl> {
public:
static constexpr const char* kDataType = "float[]";
@@ -846,7 +1091,7 @@ class FloatArrayLogEntry : public DataLogEntry {
: FloatArrayLogEntry{log, name, {}, timestamp} {}
FloatArrayLogEntry(DataLog& log, std::string_view name,
std::string_view metadata, int64_t timestamp = 0)
- : DataLogEntry{log, name, kDataType, metadata, timestamp} {}
+ : DataLogValueEntryImpl{log, name, kDataType, metadata, timestamp} {}
/**
* Appends a record to the log.
@@ -867,12 +1112,38 @@ class FloatArrayLogEntry : public DataLogEntry {
void Append(std::initializer_list arr, int64_t timestamp = 0) {
Append({arr.begin(), arr.end()}, timestamp);
}
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ * @note The last value is local to this class instance; using Update() with
+ * two instances pointing to the same underlying log entry name will likely
+ * result in unexpected results.
+ *
+ * @param arr Values to record
+ * @param timestamp Time stamp (may be 0 to indicate now)
+ */
+ void Update(std::span arr, int64_t timestamp = 0);
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ * @note The last value is local to this class instance; using Update() with
+ * two instances pointing to the same underlying log entry name will likely
+ * result in unexpected results.
+ *
+ * @param arr Values to record
+ * @param timestamp Time stamp (may be 0 to indicate now)
+ */
+ void Update(std::initializer_list arr, int64_t timestamp = 0) {
+ Update({arr.begin(), arr.end()}, timestamp);
+ }
};
/**
* Log array of double values.
*/
-class DoubleArrayLogEntry : public DataLogEntry {
+class DoubleArrayLogEntry : public DataLogValueEntryImpl> {
public:
static constexpr const char* kDataType = "double[]";
@@ -882,7 +1153,7 @@ class DoubleArrayLogEntry : public DataLogEntry {
: DoubleArrayLogEntry{log, name, {}, timestamp} {}
DoubleArrayLogEntry(DataLog& log, std::string_view name,
std::string_view metadata, int64_t timestamp = 0)
- : DataLogEntry{log, name, kDataType, metadata, timestamp} {}
+ : DataLogValueEntryImpl{log, name, kDataType, metadata, timestamp} {}
/**
* Appends a record to the log.
@@ -903,12 +1174,39 @@ class DoubleArrayLogEntry : public DataLogEntry {
void Append(std::initializer_list arr, int64_t timestamp = 0) {
Append({arr.begin(), arr.end()}, timestamp);
}
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ * @note The last value is local to this class instance; using Update() with
+ * two instances pointing to the same underlying log entry name will likely
+ * result in unexpected results.
+ *
+ * @param arr Values to record
+ * @param timestamp Time stamp (may be 0 to indicate now)
+ */
+ void Update(std::span arr, int64_t timestamp = 0);
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ * @note The last value is local to this class instance; using Update() with
+ * two instances pointing to the same underlying log entry name will likely
+ * result in unexpected results.
+ *
+ * @param arr Values to record
+ * @param timestamp Time stamp (may be 0 to indicate now)
+ */
+ void Update(std::initializer_list arr, int64_t timestamp = 0) {
+ Update({arr.begin(), arr.end()}, timestamp);
+ }
};
/**
* Log array of string values.
*/
-class StringArrayLogEntry : public DataLogEntry {
+class StringArrayLogEntry
+ : public DataLogValueEntryImpl> {
public:
static constexpr const char* kDataType = "string[]";
@@ -918,7 +1216,7 @@ class StringArrayLogEntry : public DataLogEntry {
: StringArrayLogEntry{log, name, {}, timestamp} {}
StringArrayLogEntry(DataLog& log, std::string_view name,
std::string_view metadata, int64_t timestamp = 0)
- : DataLogEntry{log, name, kDataType, metadata, timestamp} {}
+ : DataLogValueEntryImpl{log, name, kDataType, metadata, timestamp} {}
/**
* Appends a record to the log.
@@ -951,6 +1249,46 @@ class StringArrayLogEntry : public DataLogEntry {
Append(std::span{arr.begin(), arr.end()},
timestamp);
}
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ * @note The last value is local to this class instance; using Update() with
+ * two instances pointing to the same underlying log entry name will likely
+ * result in unexpected results.
+ *
+ * @param arr Values to record
+ * @param timestamp Time stamp (may be 0 to indicate now)
+ */
+ void Update(std::span arr, int64_t timestamp = 0);
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ * @note The last value is local to this class instance; using Update() with
+ * two instances pointing to the same underlying log entry name will likely
+ * result in unexpected results.
+ *
+ * @param arr Values to record
+ * @param timestamp Time stamp (may be 0 to indicate now)
+ */
+ void Update(std::span arr, int64_t timestamp = 0);
+
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ * @note The last value is local to this class instance; using Update() with
+ * two instances pointing to the same underlying log entry name will likely
+ * result in unexpected results.
+ *
+ * @param arr Values to record
+ * @param timestamp Time stamp (may be 0 to indicate now)
+ */
+ void Update(std::initializer_list arr,
+ int64_t timestamp = 0) {
+ Update(std::span{arr.begin(), arr.end()},
+ timestamp);
+ }
};
/**
@@ -974,6 +1312,19 @@ class StructLogEntry : public DataLogEntry {
m_entry = log.Start(name, S::GetTypeString(info...), metadata, timestamp);
}
+ StructLogEntry(StructLogEntry&& rhs)
+ : DataLogEntry{std::move(rhs)}, m_info{std::move(rhs.m_info)} {
+ std::scoped_lock lock{rhs.m_mutex};
+ m_lastValue = std::move(rhs.m_lastValue);
+ }
+ StructLogEntry& operator=(StructLogEntry&& rhs) {
+ DataLogEntry::operator=(std::move(rhs));
+ m_info = std::move(rhs.m_info);
+ std::scoped_lock lock{m_mutex, rhs.m_mutex};
+ m_lastValue = std::move(rhs.m_lastValue);
+ return *this;
+ }
+
/**
* Appends a record to the log.
*
@@ -995,7 +1346,74 @@ class StructLogEntry : public DataLogEntry {
m_log->AppendRaw(m_entry, buf, timestamp);
}
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ * @note The last value is local to this class instance; using Update() with
+ * two instances pointing to the same underlying log entry name will likely
+ * result in unexpected results.
+ *
+ * @param data Data to record
+ * @param timestamp Time stamp (may be 0 to indicate now)
+ */
+ void Update(const T& data, int64_t timestamp = 0) {
+ if constexpr (sizeof...(I) == 0) {
+ if constexpr (wpi::is_constexpr([] { S::GetSize(); })) {
+ uint8_t buf[S::GetSize()];
+ S::Pack(buf, data);
+ std::scoped_lock lock{m_mutex};
+ if (m_lastValue.empty() ||
+ !std::equal(buf, buf + S::GetSize(), m_lastValue.begin(),
+ m_lastValue.end())) {
+ m_lastValue.assign(buf, buf + S::GetSize());
+ m_log->AppendRaw(m_entry, buf, timestamp);
+ }
+ return;
+ }
+ }
+ wpi::SmallVector buf;
+ buf.resize_for_overwrite(std::apply(S::GetSize, m_info));
+ std::apply([&](const I&... info) { S::Pack(buf, data, info...); }, m_info);
+ std::scoped_lock lock{m_mutex};
+ if (m_lastValue.empty() ||
+ !std::equal(buf.begin(), buf.end(), m_lastValue.begin(),
+ m_lastValue.end())) {
+ m_lastValue.assign(buf.begin(), buf.end());
+ m_log->AppendRaw(m_entry, buf, timestamp);
+ }
+ }
+
+ /**
+ * Gets whether there is a last value.
+ *
+ * @note The last value is local to this class instance and updated only with
+ * Update(), not Append().
+ *
+ * @return True if last value exists, false otherwise.
+ */
+ bool HasLastValue() const { return !m_lastValue.empty(); }
+
+ /**
+ * Gets the last value.
+ *
+ * @note The last value is local to this class instance and updated only with
+ * Update(), not Append().
+ *
+ * @return Last value (empty if no last value)
+ */
+ std::optional GetLastValue() const {
+ std::scoped_lock lock{m_mutex};
+ if (m_lastValue.empty()) {
+ return std::nullopt;
+ }
+ return std::apply(
+ [&](const I&... info) { return S::Unpack(m_lastValue, info...); },
+ m_info);
+ }
+
private:
+ mutable wpi::mutex m_mutex;
+ std::vector m_lastValue;
[[no_unique_address]]
std::tuple m_info;
};
@@ -1024,6 +1442,22 @@ class StructArrayLogEntry : public DataLogEntry {
metadata, timestamp);
}
+ StructArrayLogEntry(StructArrayLogEntry&& rhs)
+ : DataLogEntry{std::move(rhs)},
+ m_buf{std::move(rhs.m_buf)},
+ m_info{std::move(rhs.m_info)} {
+ std::scoped_lock lock{rhs.m_mutex};
+ m_lastValue = std::move(rhs.m_lastValue);
+ }
+ StructArrayLogEntry& operator=(StructArrayLogEntry&& rhs) {
+ DataLogEntry::operator=(std::move(rhs));
+ m_buf = std::move(rhs.m_buf);
+ m_info = std::move(rhs.m_info);
+ std::scoped_lock lock{m_mutex, rhs.m_mutex};
+ m_lastValue = std::move(rhs.m_lastValue);
+ return *this;
+ }
+
/**
* Appends a record to the log.
*
@@ -1063,8 +1497,81 @@ class StructArrayLogEntry : public DataLogEntry {
m_info);
}
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ * @note The last value is local to this class instance; using Update() with
+ * two instances pointing to the same underlying log entry name will likely
+ * result in unexpected results.
+ *
+ * @param data Data to record
+ * @param timestamp Time stamp (may be 0 to indicate now)
+ */
+ void Update(std::span data, int64_t timestamp = 0) {
+ std::apply(
+ [&](const I&... info) {
+ m_buf.Write(
+ data,
+ [&](auto bytes) {
+ std::scoped_lock lock{m_mutex};
+ if (!m_lastValue.has_value()) {
+ m_lastValue = std::vector(bytes.begin(), bytes.end());
+ m_log->AppendRaw(m_entry, bytes, timestamp);
+ } else if (!std::equal(bytes.begin(), bytes.end(),
+ m_lastValue->begin(),
+ m_lastValue->end())) {
+ m_lastValue->assign(bytes.begin(), bytes.end());
+ m_log->AppendRaw(m_entry, bytes, timestamp);
+ }
+ },
+ info...);
+ },
+ m_info);
+ }
+
+ /**
+ * Gets whether there is a last value.
+ *
+ * @note The last value is local to this class instance and updated only with
+ * Update(), not Append().
+ *
+ * @return True if last value exists, false otherwise.
+ */
+ bool HasLastValue() const { return m_lastValue.has_value(); }
+
+ /**
+ * Gets the last value.
+ *
+ * @note The last value is local to this class instance and updated only with
+ * Update(), not Append().
+ *
+ * @return Last value (empty if no last value)
+ */
+ std::optional> GetLastValue() const {
+ std::scoped_lock lock{m_mutex};
+ if (!m_lastValue.has_value()) {
+ return std::nullopt;
+ }
+ auto& lastValue = m_lastValue.value();
+ size_t size = std::apply(S::GetSize, m_info);
+ std::vector rv;
+ rv.reserve(lastValue.size() / size);
+ for (auto in = lastValue.begin(), end = lastValue.end(); in < end;
+ in += size) {
+ std::apply(
+ [&](const I&... info) {
+ rv.emplace_back(S::Unpack(
+ std::span{std::to_address(in), size}, info...));
+ },
+ m_info);
+ }
+ return rv;
+ }
+
private:
+ mutable wpi::mutex m_mutex;
StructArrayBuffer m_buf;
+ std::optional> m_lastValue;
[[no_unique_address]]
std::tuple m_info;
};
@@ -1102,9 +1609,60 @@ class ProtobufLogEntry : public DataLogEntry {
m_log->AppendRaw(m_entry, buf, timestamp);
}
+ /**
+ * Updates the last value and appends a record to the log if it has changed.
+ *
+ * @note The last value is local to this class instance; using Update() with
+ * two instances pointing to the same underlying log entry name will likely
+ * result in unexpected results.
+ *
+ * @param data Data to record
+ * @param timestamp Time stamp (may be 0 to indicate now)
+ */
+ void Update(const T& data, int64_t timestamp = 0) {
+ std::scoped_lock lock{m_mutex};
+ wpi::SmallVector buf;
+ m_msg.Pack(buf, data);
+ if (!m_lastValue.has_value()) {
+ m_lastValue = std::vector(buf.begin(), buf.end());
+ m_log->AppendRaw(m_entry, buf, timestamp);
+ } else if (!std::equal(buf.begin(), buf.end(), m_lastValue->begin(),
+ m_lastValue->end())) {
+ m_lastValue->assign(buf.begin(), buf.end());
+ m_log->AppendRaw(m_entry, buf, timestamp);
+ }
+ }
+
+ /**
+ * Gets whether there is a last value.
+ *
+ * @note The last value is local to this class instance and updated only with
+ * Update(), not Append().
+ *
+ * @return True if last value exists, false otherwise.
+ */
+ bool HasLastValue() const { return m_lastValue.has_value(); }
+
+ /**
+ * Gets the last value.
+ *
+ * @note The last value is local to this class instance and updated only with
+ * Update(), not Append().
+ *
+ * @return Last value (empty if no last value)
+ */
+ std::optional GetLastValue() const {
+ std::scoped_lock lock{m_mutex};
+ if (!m_lastValue.has_value()) {
+ return std::nullopt;
+ }
+ return m_msg.Unpack(m_lastValue);
+ }
+
private:
- wpi::mutex m_mutex;
+ mutable wpi::mutex m_mutex;
ProtobufMessage m_msg;
+ std::optional> m_lastValue;
};
} // namespace wpi::log
diff --git a/wpiutil/src/main/native/include/wpi/DataLogWriter.h b/wpiutil/src/main/native/include/wpi/DataLogWriter.h
index 15e106e94f..f69c7b5b25 100644
--- a/wpiutil/src/main/native/include/wpi/DataLogWriter.h
+++ b/wpiutil/src/main/native/include/wpi/DataLogWriter.h
@@ -81,6 +81,13 @@ class DataLogWriter final : public DataLog {
*/
void Stop() final;
+ /**
+ * Gets the output stream.
+ *
+ * @return output stream
+ */
+ wpi::raw_ostream& GetStream() { return *m_os; }
+
private:
bool BufferFull() final;
diff --git a/wpiutil/src/test/java/edu/wpi/first/util/datalog/DataLogTest.java b/wpiutil/src/test/java/edu/wpi/first/util/datalog/DataLogTest.java
new file mode 100644
index 0000000000..c84d2846c4
--- /dev/null
+++ b/wpiutil/src/test/java/edu/wpi/first/util/datalog/DataLogTest.java
@@ -0,0 +1,799 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.util.datalog;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import edu.wpi.first.util.struct.Struct;
+import edu.wpi.first.util.struct.StructSerializable;
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.util.Objects;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+@SuppressWarnings("AvoidEscapedUnicodeCharacters")
+class DataLogTest {
+ static class ImmutableThingStruct implements Struct {
+ @Override
+ public Class getTypeClass() {
+ return ImmutableThing.class;
+ }
+
+ @Override
+ public String getTypeString() {
+ return "struct:Thing";
+ }
+
+ @Override
+ public int getSize() {
+ return 1;
+ }
+
+ @Override
+ public String getSchema() {
+ return "uint8 value";
+ }
+
+ @Override
+ public ImmutableThing unpack(ByteBuffer bb) {
+ return new ImmutableThing(bb.get());
+ }
+
+ @Override
+ public void pack(ByteBuffer bb, ImmutableThing value) {
+ bb.put(value.m_x);
+ }
+
+ @Override
+ public boolean isImmutable() {
+ return true;
+ }
+ }
+
+ static class ImmutableThing implements StructSerializable {
+ byte m_x;
+
+ ImmutableThing(int x) {
+ m_x = (byte) x;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return obj instanceof ImmutableThing other && other.m_x == m_x;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(m_x);
+ }
+
+ public static final ImmutableThingStruct struct = new ImmutableThingStruct();
+ }
+
+ static class CloneableThingStruct implements Struct {
+ @Override
+ public Class getTypeClass() {
+ return CloneableThing.class;
+ }
+
+ @Override
+ public String getTypeString() {
+ return "struct:Thing";
+ }
+
+ @Override
+ public int getSize() {
+ return 1;
+ }
+
+ @Override
+ public String getSchema() {
+ return "uint8 value";
+ }
+
+ @Override
+ public CloneableThing unpack(ByteBuffer bb) {
+ return new CloneableThing(bb.get());
+ }
+
+ @Override
+ public void pack(ByteBuffer bb, CloneableThing value) {
+ bb.put(value.m_x);
+ }
+
+ @Override
+ public boolean isCloneable() {
+ return true;
+ }
+
+ @Override
+ public CloneableThing clone(CloneableThing obj) throws CloneNotSupportedException {
+ return obj.clone();
+ }
+ }
+
+ @SuppressWarnings("MemberName")
+ private static int cloneCalls;
+
+ static class CloneableThing implements StructSerializable, Cloneable {
+ byte m_x;
+
+ CloneableThing(int x) {
+ m_x = (byte) x;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return obj instanceof CloneableThing other && other.m_x == m_x;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(m_x);
+ }
+
+ @Override
+ public CloneableThing clone() throws CloneNotSupportedException {
+ CloneableThing thing = (CloneableThing) super.clone();
+ cloneCalls++;
+ return thing;
+ }
+
+ public static final CloneableThingStruct struct = new CloneableThingStruct();
+ }
+
+ static class ThingStruct implements Struct {
+ @Override
+ public Class getTypeClass() {
+ return Thing.class;
+ }
+
+ @Override
+ public String getTypeString() {
+ return "struct:Thing";
+ }
+
+ @Override
+ public int getSize() {
+ return 1;
+ }
+
+ @Override
+ public String getSchema() {
+ return "uint8 value";
+ }
+
+ @Override
+ public Thing unpack(ByteBuffer bb) {
+ return new Thing(bb.get());
+ }
+
+ @Override
+ public void pack(ByteBuffer bb, Thing value) {
+ bb.put(value.m_x);
+ }
+ }
+
+ static class Thing implements StructSerializable {
+ byte m_x;
+
+ Thing(int x) {
+ m_x = (byte) x;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return obj instanceof Thing other && other.m_x == m_x;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(m_x);
+ }
+
+ public static final ThingStruct struct = new ThingStruct();
+ }
+
+ @SuppressWarnings("MemberName")
+ private ByteArrayOutputStream data;
+
+ @SuppressWarnings("MemberName")
+ private DataLog log;
+
+ @BeforeEach
+ public void init() {
+ data = new ByteArrayOutputStream();
+ log = new DataLogWriter(data);
+ cloneCalls = 0;
+ }
+
+ @AfterEach
+ public void shutdown() {
+ log.close();
+ }
+
+ @Test
+ void testSimpleInt() {
+ int entry = log.start("test", "int64", "", 1);
+ log.appendInteger(entry, 1, 2);
+ log.flush();
+ assertEquals(54, data.size());
+ }
+
+ @Test
+ void testBooleanAppend() {
+ BooleanLogEntry entry = new BooleanLogEntry(log, "a", 5);
+ entry.append(false, 7);
+ log.flush();
+ assertEquals(46, data.size());
+ }
+
+ @Test
+ void testBooleanUpdate() {
+ BooleanLogEntry entry = new BooleanLogEntry(log, "a", 5);
+ assertFalse(entry.hasLastValue());
+ entry.update(false, 7);
+ log.flush();
+ assertEquals(46, data.size());
+ assertTrue(entry.hasLastValue());
+ assertFalse(entry.getLastValue());
+ entry.update(false, 8);
+ log.flush();
+ assertEquals(46, data.size());
+ entry.update(true, 9);
+ log.flush();
+ assertEquals(51, data.size());
+ assertTrue(entry.hasLastValue());
+ assertTrue(entry.getLastValue());
+ }
+
+ @Test
+ void testIntegerAppend() {
+ IntegerLogEntry entry = new IntegerLogEntry(log, "a", 5);
+ entry.append(5, 7);
+ log.flush();
+ assertEquals(51, data.size());
+ }
+
+ @Test
+ void testIntegerUpdate() {
+ IntegerLogEntry entry = new IntegerLogEntry(log, "a", 5);
+ assertFalse(entry.hasLastValue());
+ entry.update(0, 7);
+ log.flush();
+ assertEquals(51, data.size());
+ assertTrue(entry.hasLastValue());
+ assertEquals(0, entry.getLastValue());
+ entry.update(0, 8);
+ log.flush();
+ assertEquals(51, data.size());
+ entry.update(2, 9);
+ log.flush();
+ assertEquals(63, data.size());
+ assertTrue(entry.hasLastValue());
+ assertEquals(2, entry.getLastValue());
+ }
+
+ @Test
+ void testFloatAppend() {
+ FloatLogEntry entry = new FloatLogEntry(log, "a", 5);
+ entry.append(5.0f, 7);
+ log.flush();
+ assertEquals(47, data.size());
+ }
+
+ @Test
+ void testFloatUpdate() {
+ FloatLogEntry entry = new FloatLogEntry(log, "a", 5);
+ assertFalse(entry.hasLastValue());
+ entry.update(0.0f, 7);
+ log.flush();
+ assertEquals(47, data.size());
+ assertTrue(entry.hasLastValue());
+ assertEquals(0.0f, entry.getLastValue());
+ entry.update(0.0f, 8);
+ log.flush();
+ assertEquals(47, data.size());
+ entry.update(0.1f, 9);
+ log.flush();
+ assertEquals(55, data.size());
+ assertTrue(entry.hasLastValue());
+ assertEquals(0.1f, entry.getLastValue());
+ }
+
+ @Test
+ void testDoubleAppend() {
+ DoubleLogEntry entry = new DoubleLogEntry(log, "a", 5);
+ entry.append(5.0, 7);
+ log.flush();
+ assertEquals(52, data.size());
+ }
+
+ @Test
+ void testDoubleUpdate() {
+ DoubleLogEntry entry = new DoubleLogEntry(log, "a", 5);
+ assertFalse(entry.hasLastValue());
+ entry.update(0.0, 7);
+ log.flush();
+ assertEquals(52, data.size());
+ assertTrue(entry.hasLastValue());
+ assertEquals(0.0, entry.getLastValue());
+ entry.update(0.0, 8);
+ log.flush();
+ assertEquals(52, data.size());
+ entry.update(0.1, 9);
+ log.flush();
+ assertEquals(64, data.size());
+ assertTrue(entry.hasLastValue());
+ assertEquals(0.1, entry.getLastValue());
+ }
+
+ @Test
+ void testStringAppend() {
+ StringLogEntry entry = new StringLogEntry(log, "a", 5);
+ entry.append("x", 7);
+ log.flush();
+ assertEquals(45, data.size());
+ }
+
+ @Test
+ void testStringUpdate() {
+ StringLogEntry entry = new StringLogEntry(log, "a", 5);
+ assertFalse(entry.hasLastValue());
+
+ entry.update("x", 7);
+ log.flush();
+ assertEquals(45, data.size());
+ assertTrue(entry.hasLastValue());
+ assertEquals("x", entry.getLastValue());
+
+ entry.update("x", 8);
+ log.flush();
+ assertEquals(45, data.size());
+
+ entry.update("y", 9);
+ log.flush();
+ assertEquals(50, data.size());
+ assertTrue(entry.hasLastValue());
+ assertEquals("y", entry.getLastValue());
+
+ entry.update("yy", 10);
+ log.flush();
+ assertEquals(56, data.size());
+ assertTrue(entry.hasLastValue());
+ assertEquals("yy", entry.getLastValue());
+
+ entry.update("", 11);
+ log.flush();
+ assertEquals(60, data.size());
+ assertTrue(entry.hasLastValue());
+ assertEquals("", entry.getLastValue());
+ }
+
+ @Test
+ void testRawAppend() {
+ RawLogEntry entry = new RawLogEntry(log, "a", 5);
+ entry.append(new byte[] {5}, 7);
+ log.flush();
+ assertEquals(42, data.size());
+ }
+
+ @Test
+ void testRawUpdate() {
+ RawLogEntry entry = new RawLogEntry(log, "a", 5);
+ assertFalse(entry.hasLastValue());
+
+ entry.update(new byte[] {5}, 7);
+ log.flush();
+ assertEquals(42, data.size());
+ assertTrue(entry.hasLastValue());
+ assertArrayEquals(new byte[] {5}, entry.getLastValue());
+
+ entry.update(new byte[] {5}, 8);
+ log.flush();
+ assertEquals(42, data.size());
+
+ entry.update(new byte[] {6}, 9);
+ log.flush();
+ assertEquals(47, data.size());
+ assertTrue(entry.hasLastValue());
+ assertArrayEquals(new byte[] {6}, entry.getLastValue());
+
+ entry.update(new byte[] {6, 6}, 10);
+ log.flush();
+ assertEquals(53, data.size());
+ assertTrue(entry.hasLastValue());
+ assertArrayEquals(new byte[] {6, 6}, entry.getLastValue());
+
+ entry.update(new byte[] {}, 11);
+ log.flush();
+ assertEquals(57, data.size());
+ assertTrue(entry.hasLastValue());
+ assertArrayEquals(new byte[] {}, entry.getLastValue());
+ }
+
+ @Test
+ void testBooleanArrayAppendEmpty() {
+ BooleanArrayLogEntry entry = new BooleanArrayLogEntry(log, "a", 5);
+ entry.append(new boolean[] {}, 7);
+ log.flush();
+ assertEquals(47, data.size());
+ }
+
+ @Test
+ void testBooleanArrayAppend() {
+ BooleanArrayLogEntry entry = new BooleanArrayLogEntry(log, "a", 5);
+ entry.append(new boolean[] {false}, 7);
+ log.flush();
+ assertEquals(48, data.size());
+ }
+
+ @Test
+ void testBooleanArrayUpdate() {
+ BooleanArrayLogEntry entry = new BooleanArrayLogEntry(log, "a", 5);
+ assertFalse(entry.hasLastValue());
+ entry.update(new boolean[] {false}, 7);
+ log.flush();
+ assertEquals(48, data.size());
+ assertTrue(entry.hasLastValue());
+ assertArrayEquals(new boolean[] {false}, entry.getLastValue());
+ entry.update(new boolean[] {false}, 8);
+ log.flush();
+ assertEquals(48, data.size());
+ entry.update(new boolean[] {true}, 9);
+ log.flush();
+ assertEquals(53, data.size());
+ assertTrue(entry.hasLastValue());
+ assertArrayEquals(new boolean[] {true}, entry.getLastValue());
+ entry.update(new boolean[] {}, 10);
+ log.flush();
+ assertEquals(57, data.size());
+ assertTrue(entry.hasLastValue());
+ assertArrayEquals(new boolean[] {}, entry.getLastValue());
+ }
+
+ @Test
+ void testIntegerArrayAppendEmpty() {
+ IntegerArrayLogEntry entry = new IntegerArrayLogEntry(log, "a", 5);
+ entry.append(new long[] {}, 7);
+ log.flush();
+ assertEquals(45, data.size());
+ }
+
+ @Test
+ void testIntegerArrayAppend() {
+ IntegerArrayLogEntry entry = new IntegerArrayLogEntry(log, "a", 5);
+ entry.append(new long[] {1}, 7);
+ log.flush();
+ assertEquals(53, data.size());
+ }
+
+ @Test
+ void testIntegerArrayUpdate() {
+ IntegerArrayLogEntry entry = new IntegerArrayLogEntry(log, "a", 5);
+ assertFalse(entry.hasLastValue());
+ entry.update(new long[] {1}, 7);
+ log.flush();
+ assertEquals(53, data.size());
+ assertTrue(entry.hasLastValue());
+ assertArrayEquals(new long[] {1}, entry.getLastValue());
+ entry.update(new long[] {1}, 8);
+ log.flush();
+ assertEquals(53, data.size());
+ entry.update(new long[] {2}, 9);
+ log.flush();
+ assertEquals(65, data.size());
+ assertTrue(entry.hasLastValue());
+ assertArrayEquals(new long[] {2}, entry.getLastValue());
+ entry.update(new long[] {}, 10);
+ log.flush();
+ assertEquals(69, data.size());
+ assertTrue(entry.hasLastValue());
+ assertArrayEquals(new long[] {}, entry.getLastValue());
+ }
+
+ @Test
+ void testDoubleArrayAppendEmpty() {
+ DoubleArrayLogEntry entry = new DoubleArrayLogEntry(log, "a", 5);
+ entry.append(new double[] {}, 7);
+ log.flush();
+ assertEquals(46, data.size());
+ }
+
+ @Test
+ void testDoubleArrayAppend() {
+ DoubleArrayLogEntry entry = new DoubleArrayLogEntry(log, "a", 5);
+ entry.append(new double[] {1.0}, 7);
+ log.flush();
+ assertEquals(54, data.size());
+ }
+
+ @Test
+ void testDoubleArrayUpdate() {
+ DoubleArrayLogEntry entry = new DoubleArrayLogEntry(log, "a", 5);
+ assertFalse(entry.hasLastValue());
+ entry.update(new double[] {1.0}, 7);
+ log.flush();
+ assertEquals(54, data.size());
+ assertTrue(entry.hasLastValue());
+ assertArrayEquals(new double[] {1.0}, entry.getLastValue());
+ entry.update(new double[] {1.0}, 8);
+ log.flush();
+ assertEquals(54, data.size());
+ entry.update(new double[] {2.0}, 9);
+ log.flush();
+ assertEquals(66, data.size());
+ assertTrue(entry.hasLastValue());
+ assertArrayEquals(new double[] {2}, entry.getLastValue());
+ entry.update(new double[] {}, 10);
+ log.flush();
+ assertEquals(70, data.size());
+ assertTrue(entry.hasLastValue());
+ assertArrayEquals(new double[] {}, entry.getLastValue());
+ }
+
+ @Test
+ void testFloatArrayAppendEmpty() {
+ FloatArrayLogEntry entry = new FloatArrayLogEntry(log, "a", 5);
+ entry.append(new float[] {}, 7);
+ log.flush();
+ assertEquals(45, data.size());
+ }
+
+ @Test
+ void testFloatArrayAppend() {
+ FloatArrayLogEntry entry = new FloatArrayLogEntry(log, "a", 5);
+ entry.append(new float[] {1.0f}, 7);
+ log.flush();
+ assertEquals(49, data.size());
+ }
+
+ @Test
+ void testFloatArrayUpdate() {
+ FloatArrayLogEntry entry = new FloatArrayLogEntry(log, "a", 5);
+ assertFalse(entry.hasLastValue());
+ entry.update(new float[] {1.0f}, 7);
+ log.flush();
+ assertEquals(49, data.size());
+ assertTrue(entry.hasLastValue());
+ assertArrayEquals(new float[] {1.0f}, entry.getLastValue());
+ entry.update(new float[] {1.0f}, 8);
+ log.flush();
+ assertEquals(49, data.size());
+ entry.update(new float[] {2.0f}, 9);
+ log.flush();
+ assertEquals(57, data.size());
+ assertTrue(entry.hasLastValue());
+ assertArrayEquals(new float[] {2.0f}, entry.getLastValue());
+ entry.update(new float[] {}, 10);
+ log.flush();
+ assertEquals(61, data.size());
+ assertTrue(entry.hasLastValue());
+ assertArrayEquals(new float[] {}, entry.getLastValue());
+ }
+
+ @Test
+ void testStringArrayAppendEmpty() {
+ StringArrayLogEntry entry = new StringArrayLogEntry(log, "a", 5);
+ entry.append(new String[] {}, 7);
+ entry.append(new String[] {}, 7);
+ log.flush();
+ assertEquals(58, data.size());
+ }
+
+ @Test
+ void testStringArrayAppend() {
+ StringArrayLogEntry entry = new StringArrayLogEntry(log, "a", 5);
+ entry.append(new String[] {"x"}, 7);
+ log.flush();
+ assertEquals(55, data.size());
+ }
+
+ @Test
+ void testStringArrayUpdate() {
+ StringArrayLogEntry entry = new StringArrayLogEntry(log, "a", 5);
+ assertFalse(entry.hasLastValue());
+ entry.update(new String[] {"x"}, 7);
+ log.flush();
+ assertEquals(55, data.size());
+ assertTrue(entry.hasLastValue());
+ assertArrayEquals(new String[] {"x"}, entry.getLastValue());
+ entry.update(new String[] {"x"}, 8);
+ log.flush();
+ assertEquals(55, data.size());
+ entry.update(new String[] {"y"}, 9);
+ log.flush();
+ assertEquals(68, data.size());
+ assertTrue(entry.hasLastValue());
+ assertArrayEquals(new String[] {"y"}, entry.getLastValue());
+ entry.update(new String[] {}, 10);
+ log.flush();
+ assertEquals(76, data.size());
+ assertTrue(entry.hasLastValue());
+ assertArrayEquals(new String[] {}, entry.getLastValue());
+ }
+
+ @Test
+ void testStruct() {
+ StructLogEntry entry = StructLogEntry.create(log, "a", Thing.struct, 5);
+ entry.append(new Thing((byte) 1), 6);
+ entry.append(new Thing((byte) 0), 7);
+ }
+
+ @Test
+ void testStructUpdate() {
+ StructLogEntry entry = StructLogEntry.create(log, "a", Thing.struct, 5);
+ assertFalse(entry.hasLastValue());
+
+ entry.update(new Thing(1), 7);
+ log.flush();
+ assertEquals(120, data.size());
+ assertTrue(entry.hasLastValue());
+ assertEquals(new Thing(1), entry.getLastValue());
+
+ entry.update(new Thing(1), 8);
+ log.flush();
+ assertEquals(120, data.size());
+
+ entry.update(new Thing(2), 9);
+ log.flush();
+ assertEquals(125, data.size());
+ assertTrue(entry.hasLastValue());
+ assertEquals(new Thing(2), entry.getLastValue());
+ }
+
+ @Test
+ void testCloneableStructUpdate() {
+ StructLogEntry entry =
+ StructLogEntry.create(log, "a", CloneableThing.struct, 5);
+ assertFalse(entry.hasLastValue());
+
+ entry.update(new CloneableThing(1), 7);
+ assertEquals(1, cloneCalls);
+ log.flush();
+ assertEquals(120, data.size());
+ assertTrue(entry.hasLastValue());
+ assertEquals(new CloneableThing(1), entry.getLastValue());
+ assertEquals(2, cloneCalls);
+
+ entry.update(new CloneableThing(1), 8);
+ assertEquals(2, cloneCalls);
+ log.flush();
+ assertEquals(120, data.size());
+
+ entry.update(new CloneableThing(2), 9);
+ assertEquals(3, cloneCalls);
+ log.flush();
+ assertEquals(125, data.size());
+ assertTrue(entry.hasLastValue());
+ assertEquals(new CloneableThing(2), entry.getLastValue());
+ assertEquals(4, cloneCalls);
+ }
+
+ @Test
+ void testImmutableStructUpdate() {
+ StructLogEntry entry =
+ StructLogEntry.create(log, "a", ImmutableThing.struct, 5);
+ assertFalse(entry.hasLastValue());
+
+ entry.update(new ImmutableThing(1), 7);
+ log.flush();
+ assertEquals(120, data.size());
+ assertTrue(entry.hasLastValue());
+ assertEquals(new ImmutableThing(1), entry.getLastValue());
+
+ entry.update(new ImmutableThing(1), 8);
+ log.flush();
+ assertEquals(120, data.size());
+
+ entry.update(new ImmutableThing(2), 9);
+ log.flush();
+ assertEquals(125, data.size());
+ assertTrue(entry.hasLastValue());
+ assertEquals(new ImmutableThing(2), entry.getLastValue());
+ }
+
+ @Test
+ void testStructArrayUpdate() {
+ StructArrayLogEntry entry = StructArrayLogEntry.create(log, "a", Thing.struct, 5);
+ assertFalse(entry.hasLastValue());
+
+ entry.update(new Thing[] {new Thing(1), new Thing(2)}, 7);
+ log.flush();
+ assertEquals(123, data.size());
+ assertTrue(entry.hasLastValue());
+ assertArrayEquals(new Thing[] {new Thing(1), new Thing(2)}, entry.getLastValue());
+
+ entry.update(new Thing[] {new Thing(1), new Thing(2)}, 8);
+ log.flush();
+ assertEquals(123, data.size());
+
+ entry.update(new Thing[] {new Thing(1), new Thing(3)}, 9);
+ log.flush();
+ assertEquals(129, data.size());
+ assertTrue(entry.hasLastValue());
+ assertArrayEquals(new Thing[] {new Thing(1), new Thing(3)}, entry.getLastValue());
+
+ entry.update(new Thing[] {}, 10);
+ log.flush();
+ assertEquals(133, data.size());
+ assertTrue(entry.hasLastValue());
+ assertArrayEquals(new Thing[] {}, entry.getLastValue());
+ }
+
+ @Test
+ void testCloneableStructArrayUpdate() {
+ StructArrayLogEntry entry =
+ StructArrayLogEntry.create(log, "a", CloneableThing.struct, 5);
+ assertFalse(entry.hasLastValue());
+
+ entry.update(new CloneableThing[] {new CloneableThing(1), new CloneableThing(2)}, 7);
+ assertEquals(2, cloneCalls);
+ log.flush();
+ assertEquals(123, data.size());
+ assertTrue(entry.hasLastValue());
+ assertArrayEquals(
+ new CloneableThing[] {new CloneableThing(1), new CloneableThing(2)}, entry.getLastValue());
+ assertEquals(4, cloneCalls);
+
+ entry.update(new CloneableThing[] {new CloneableThing(1), new CloneableThing(2)}, 8);
+ assertEquals(4, cloneCalls);
+ log.flush();
+ assertEquals(123, data.size());
+
+ entry.update(new CloneableThing[] {new CloneableThing(1), new CloneableThing(3)}, 9);
+ assertEquals(6, cloneCalls);
+ log.flush();
+ assertEquals(129, data.size());
+ assertTrue(entry.hasLastValue());
+ assertArrayEquals(
+ new CloneableThing[] {new CloneableThing(1), new CloneableThing(3)}, entry.getLastValue());
+ assertEquals(8, cloneCalls);
+
+ entry.update(new CloneableThing[] {}, 10);
+ assertEquals(8, cloneCalls);
+ log.flush();
+ assertEquals(133, data.size());
+ assertTrue(entry.hasLastValue());
+ assertArrayEquals(new CloneableThing[] {}, entry.getLastValue());
+ assertEquals(8, cloneCalls);
+ }
+
+ @Test
+ void testImmutableStructArrayUpdate() {
+ StructArrayLogEntry entry =
+ StructArrayLogEntry.create(log, "a", ImmutableThing.struct, 5);
+ assertFalse(entry.hasLastValue());
+
+ entry.update(new ImmutableThing[] {new ImmutableThing(1), new ImmutableThing(2)}, 7);
+ log.flush();
+ assertEquals(123, data.size());
+ assertTrue(entry.hasLastValue());
+ assertArrayEquals(
+ new ImmutableThing[] {new ImmutableThing(1), new ImmutableThing(2)}, entry.getLastValue());
+
+ entry.update(new ImmutableThing[] {new ImmutableThing(1), new ImmutableThing(2)}, 8);
+ log.flush();
+ assertEquals(123, data.size());
+
+ entry.update(new ImmutableThing[] {new ImmutableThing(1), new ImmutableThing(3)}, 9);
+ log.flush();
+ assertEquals(129, data.size());
+ assertTrue(entry.hasLastValue());
+ assertArrayEquals(
+ new ImmutableThing[] {new ImmutableThing(1), new ImmutableThing(3)}, entry.getLastValue());
+
+ entry.update(new ImmutableThing[] {}, 10);
+ log.flush();
+ assertEquals(133, data.size());
+ assertTrue(entry.hasLastValue());
+ assertArrayEquals(new ImmutableThing[] {}, entry.getLastValue());
+ }
+}
diff --git a/wpiutil/src/test/native/cpp/DataLogTest.cpp b/wpiutil/src/test/native/cpp/DataLogTest.cpp
index f4389abcb0..aa0a79188f 100644
--- a/wpiutil/src/test/native/cpp/DataLogTest.cpp
+++ b/wpiutil/src/test/native/cpp/DataLogTest.cpp
@@ -16,6 +16,10 @@ struct ThingA {
int x = 0;
};
+inline bool operator==(const ThingA& a, const ThingA& b) {
+ return a.x == b.x;
+}
+
struct ThingB {
int x = 0;
};
@@ -130,10 +134,376 @@ class DataLogTest : public ::testing::Test {
};
TEST_F(DataLogTest, SimpleInt) {
- int entry = log.Start("test", "int64");
- log.AppendInteger(entry, 1, 0);
+ int entry = log.Start("test", "int64", "", 1);
+ log.AppendInteger(entry, 1, 2);
+ log.Flush();
+ ASSERT_EQ(data.size(), 54u);
+}
+
+TEST_F(DataLogTest, BooleanAppend) {
+ wpi::log::BooleanLogEntry entry{log, "a", 5};
+ entry.Append(false, 7);
+ log.Flush();
+ ASSERT_EQ(data.size(), 46u);
+}
+
+TEST_F(DataLogTest, BooleanUpdate) {
+ wpi::log::BooleanLogEntry entry{log, "a", 5};
+ ASSERT_FALSE(entry.GetLastValue().has_value());
+ entry.Update(false, 7);
+ log.Flush();
+ ASSERT_EQ(data.size(), 46u);
+ ASSERT_TRUE(entry.GetLastValue().has_value());
+ ASSERT_EQ(entry.GetLastValue().value(), false);
+ entry.Update(false, 8);
+ log.Flush();
+ ASSERT_EQ(data.size(), 46u);
+ entry.Update(true, 9);
+ log.Flush();
+ ASSERT_EQ(data.size(), 51u);
+ ASSERT_TRUE(entry.GetLastValue().has_value());
+ ASSERT_EQ(entry.GetLastValue().value(), true);
+}
+
+TEST_F(DataLogTest, IntegerAppend) {
+ wpi::log::IntegerLogEntry entry{log, "a", 5};
+ entry.Append(5, 7);
+ log.Flush();
+ ASSERT_EQ(data.size(), 51u);
+}
+
+TEST_F(DataLogTest, IntegerUpdate) {
+ wpi::log::IntegerLogEntry entry{log, "a", 5};
+ ASSERT_FALSE(entry.GetLastValue().has_value());
+ entry.Update(0, 7);
+ log.Flush();
+ ASSERT_EQ(data.size(), 51u);
+ ASSERT_TRUE(entry.GetLastValue().has_value());
+ ASSERT_EQ(entry.GetLastValue().value(), 0);
+ entry.Update(0, 8);
+ log.Flush();
+ ASSERT_EQ(data.size(), 51u);
+ entry.Update(2, 9);
+ log.Flush();
+ ASSERT_EQ(data.size(), 63u);
+ ASSERT_TRUE(entry.GetLastValue().has_value());
+ ASSERT_EQ(entry.GetLastValue().value(), 2);
+}
+
+TEST_F(DataLogTest, FloatAppend) {
+ wpi::log::FloatLogEntry entry{log, "a", 5};
+ entry.Append(5.0, 7);
+ log.Flush();
+ ASSERT_EQ(data.size(), 47u);
+}
+
+TEST_F(DataLogTest, FloatUpdate) {
+ wpi::log::FloatLogEntry entry{log, "a", 5};
+ ASSERT_FALSE(entry.GetLastValue().has_value());
+ entry.Update(0.0f, 7);
+ log.Flush();
+ ASSERT_EQ(data.size(), 47u);
+ ASSERT_TRUE(entry.GetLastValue().has_value());
+ ASSERT_EQ(entry.GetLastValue().value(), 0.0f);
+ entry.Update(0.0f, 8);
+ log.Flush();
+ ASSERT_EQ(data.size(), 47u);
+ entry.Update(0.1f, 9);
+ log.Flush();
+ ASSERT_EQ(data.size(), 55u);
+ ASSERT_TRUE(entry.GetLastValue().has_value());
+ ASSERT_EQ(entry.GetLastValue().value(), 0.1f);
+}
+
+TEST_F(DataLogTest, DoubleAppend) {
+ wpi::log::DoubleLogEntry entry{log, "a", 5};
+ entry.Append(5.0, 7);
+ log.Flush();
+ ASSERT_EQ(data.size(), 52u);
+}
+
+TEST_F(DataLogTest, DoubleUpdate) {
+ wpi::log::DoubleLogEntry entry{log, "a", 5};
+ ASSERT_FALSE(entry.GetLastValue().has_value());
+ entry.Update(0.0, 7);
+ log.Flush();
+ ASSERT_EQ(data.size(), 52u);
+ ASSERT_TRUE(entry.GetLastValue().has_value());
+ ASSERT_EQ(entry.GetLastValue().value(), 0.0);
+ entry.Update(0.0, 8);
+ log.Flush();
+ ASSERT_EQ(data.size(), 52u);
+ entry.Update(0.1, 9);
+ log.Flush();
+ ASSERT_EQ(data.size(), 64u);
+ ASSERT_TRUE(entry.GetLastValue().has_value());
+ ASSERT_EQ(entry.GetLastValue().value(), 0.1);
+}
+
+TEST_F(DataLogTest, StringAppend) {
+ wpi::log::StringLogEntry entry{log, "a", 5};
+ entry.Append("x", 7);
+ log.Flush();
+ ASSERT_EQ(data.size(), 45u);
+}
+
+TEST_F(DataLogTest, StringUpdate) {
+ wpi::log::StringLogEntry entry{log, "a", 5};
+ ASSERT_FALSE(entry.HasLastValue());
+
+ entry.Update("x", 7);
+ log.Flush();
+ ASSERT_EQ(data.size(), 45u);
+ ASSERT_TRUE(entry.GetLastValue().has_value());
+ ASSERT_EQ(entry.GetLastValue().value(), "x");
+
+ entry.Update("x", 8);
+ log.Flush();
+ ASSERT_EQ(data.size(), 45u);
+
+ entry.Update("y", 9);
+ log.Flush();
+ ASSERT_EQ(data.size(), 50u);
+ ASSERT_TRUE(entry.GetLastValue().has_value());
+ ASSERT_EQ(entry.GetLastValue().value(), "y");
+
+ entry.Update("yy", 10);
+ log.Flush();
+ ASSERT_EQ(data.size(), 56u);
+ ASSERT_TRUE(entry.GetLastValue().has_value());
+ ASSERT_EQ(entry.GetLastValue().value(), "yy");
+
+ entry.Update("", 11);
+ log.Flush();
+ ASSERT_EQ(data.size(), 60u);
+ ASSERT_TRUE(entry.GetLastValue().has_value());
+ ASSERT_EQ(entry.GetLastValue().value(), "");
+}
+
+TEST_F(DataLogTest, RawAppend) {
+ wpi::log::RawLogEntry entry{log, "a", 5};
+ entry.Append({{5}}, 7);
+ log.Flush();
+ ASSERT_EQ(data.size(), 42u);
+}
+
+TEST_F(DataLogTest, RawUpdate) {
+ wpi::log::RawLogEntry entry{log, "a", 5};
+ ASSERT_FALSE(entry.HasLastValue());
+
+ entry.Update({{5}}, 7);
+ log.Flush();
+ ASSERT_EQ(data.size(), 42u);
+ ASSERT_TRUE(entry.GetLastValue().has_value());
+ ASSERT_EQ(entry.GetLastValue().value(), std::vector{5});
+
+ entry.Update({{5}}, 8);
+ log.Flush();
+ ASSERT_EQ(data.size(), 42u);
+
+ entry.Update({{6}}, 9);
+ log.Flush();
+ ASSERT_EQ(data.size(), 47u);
+ ASSERT_TRUE(entry.GetLastValue().has_value());
+ ASSERT_EQ(entry.GetLastValue().value(), std::vector{6});
+
+ entry.Update({{6, 6}}, 9);
+ log.Flush();
+ ASSERT_EQ(data.size(), 53u);
+ ASSERT_TRUE(entry.GetLastValue().has_value());
+ ASSERT_EQ(entry.GetLastValue().value(), (std::vector{6, 6}));
+
+ entry.Update(std::span{}, 10);
+ log.Flush();
+ ASSERT_EQ(data.size(), 57u);
+ ASSERT_TRUE(entry.GetLastValue().has_value());
+ ASSERT_EQ(entry.GetLastValue().value(), std::vector{});
+}
+
+TEST_F(DataLogTest, BooleanArrayAppendEmpty) {
+ wpi::log::BooleanArrayLogEntry entry{log, "a", 5};
+ entry.Append(std::span{}, 7);
+ log.Flush();
+ ASSERT_EQ(data.size(), 47u);
+}
+
+TEST_F(DataLogTest, BooleanArrayAppend) {
+ wpi::log::BooleanArrayLogEntry entry{log, "a", 5};
+ entry.Append({false}, 7);
+ log.Flush();
+ ASSERT_EQ(data.size(), 48u);
+}
+
+TEST_F(DataLogTest, BooleanArrayUpdate) {
+ wpi::log::BooleanArrayLogEntry entry{log, "a", 5};
+ ASSERT_FALSE(entry.GetLastValue().has_value());
+ entry.Update({false}, 7);
+ log.Flush();
+ ASSERT_EQ(data.size(), 48u);
+ ASSERT_TRUE(entry.GetLastValue().has_value());
+ ASSERT_EQ(entry.GetLastValue().value(), std::vector{false});
+ entry.Update({false}, 8);
+ log.Flush();
+ ASSERT_EQ(data.size(), 48u);
+ entry.Update({true}, 9);
+ log.Flush();
+ ASSERT_EQ(data.size(), 53u);
+ ASSERT_TRUE(entry.GetLastValue().has_value());
+ ASSERT_EQ(entry.GetLastValue().value(), std::vector{true});
+ entry.Update(std::span{}, 10);
+ log.Flush();
+ ASSERT_EQ(data.size(), 57u);
+ ASSERT_TRUE(entry.GetLastValue().has_value());
+ ASSERT_EQ(entry.GetLastValue().value(), std::vector{});
+}
+
+TEST_F(DataLogTest, IntegerArrayAppendEmpty) {
+ wpi::log::IntegerArrayLogEntry entry{log, "a", 5};
+ entry.Append(std::span{}, 7);
+ log.Flush();
+ ASSERT_EQ(data.size(), 45u);
+}
+
+TEST_F(DataLogTest, IntegerArrayAppend) {
+ wpi::log::IntegerArrayLogEntry entry{log, "a", 5};
+ entry.Append({1}, 7);
+ log.Flush();
+ ASSERT_EQ(data.size(), 53u);
+}
+
+TEST_F(DataLogTest, IntegerArrayUpdate) {
+ wpi::log::IntegerArrayLogEntry entry{log, "a", 5};
+ ASSERT_FALSE(entry.GetLastValue().has_value());
+ entry.Update({1}, 7);
+ log.Flush();
+ ASSERT_EQ(data.size(), 53u);
+ ASSERT_TRUE(entry.GetLastValue().has_value());
+ ASSERT_EQ(entry.GetLastValue().value(), std::vector{1});
+ entry.Update({1}, 8);
+ log.Flush();
+ ASSERT_EQ(data.size(), 53u);
+ entry.Update({2}, 9);
+ log.Flush();
+ ASSERT_EQ(data.size(), 65u);
+ ASSERT_TRUE(entry.GetLastValue().has_value());
+ ASSERT_EQ(entry.GetLastValue().value(), std::vector{2});
+ entry.Update(std::span{}, 10);
+ log.Flush();
+ ASSERT_EQ(data.size(), 69u);
+ ASSERT_TRUE(entry.GetLastValue().has_value());
+ ASSERT_EQ(entry.GetLastValue().value(), std::vector{});
+}
+
+TEST_F(DataLogTest, DoubleArrayAppendEmpty) {
+ wpi::log::DoubleArrayLogEntry entry{log, "a", 5};
+ entry.Append(std::span{}, 7);
+ log.Flush();
+ ASSERT_EQ(data.size(), 46u);
+}
+
+TEST_F(DataLogTest, DoubleArrayAppend) {
+ wpi::log::DoubleArrayLogEntry entry{log, "a", 5};
+ entry.Append({1.0}, 7);
+ log.Flush();
+ ASSERT_EQ(data.size(), 54u);
+}
+
+TEST_F(DataLogTest, DoubleArrayUpdate) {
+ wpi::log::DoubleArrayLogEntry entry{log, "a", 5};
+ ASSERT_FALSE(entry.GetLastValue().has_value());
+ entry.Update({1.0}, 7);
+ log.Flush();
+ ASSERT_EQ(data.size(), 54u);
+ ASSERT_TRUE(entry.GetLastValue().has_value());
+ ASSERT_EQ(entry.GetLastValue().value(), std::vector{1.0});
+ entry.Update({1.0}, 8);
+ log.Flush();
+ ASSERT_EQ(data.size(), 54u);
+ entry.Update({2.0}, 9);
log.Flush();
ASSERT_EQ(data.size(), 66u);
+ ASSERT_TRUE(entry.GetLastValue().has_value());
+ ASSERT_EQ(entry.GetLastValue().value(), std::vector{2});
+ entry.Update(std::span{}, 10);
+ log.Flush();
+ ASSERT_EQ(data.size(), 70u);
+ ASSERT_TRUE(entry.GetLastValue().has_value());
+ ASSERT_EQ(entry.GetLastValue().value(), std::vector{});
+}
+
+TEST_F(DataLogTest, FloatArrayAppendEmpty) {
+ wpi::log::FloatArrayLogEntry entry{log, "a", 5};
+ entry.Append(std::span{}, 7);
+ log.Flush();
+ ASSERT_EQ(data.size(), 45u);
+}
+
+TEST_F(DataLogTest, FloatArrayAppend) {
+ wpi::log::FloatArrayLogEntry entry{log, "a", 5};
+ entry.Append({1.0f}, 7);
+ log.Flush();
+ ASSERT_EQ(data.size(), 49u);
+}
+
+TEST_F(DataLogTest, FloatArrayUpdate) {
+ wpi::log::FloatArrayLogEntry entry{log, "a", 5};
+ ASSERT_FALSE(entry.GetLastValue().has_value());
+ entry.Update({1.0f}, 7);
+ log.Flush();
+ ASSERT_EQ(data.size(), 49u);
+ ASSERT_TRUE(entry.GetLastValue().has_value());
+ ASSERT_EQ(entry.GetLastValue().value(), std::vector{1.0f});
+ entry.Update({1.0f}, 8);
+ log.Flush();
+ ASSERT_EQ(data.size(), 49u);
+ entry.Update({2.0f}, 9);
+ log.Flush();
+ ASSERT_EQ(data.size(), 57u);
+ ASSERT_TRUE(entry.GetLastValue().has_value());
+ ASSERT_EQ(entry.GetLastValue().value(), std::vector{2.0f});
+ entry.Update(std::span{}, 10);
+ log.Flush();
+ ASSERT_EQ(data.size(), 61u);
+ ASSERT_TRUE(entry.GetLastValue().has_value());
+ ASSERT_EQ(entry.GetLastValue().value(), std::vector{});
+}
+
+TEST_F(DataLogTest, StringArrayAppendEmpty) {
+ wpi::log::StringArrayLogEntry entry{log, "a", 5};
+ entry.Append(std::span{}, 7);
+ entry.Append(std::span{}, 7);
+ log.Flush();
+ ASSERT_EQ(data.size(), 58u);
+}
+
+TEST_F(DataLogTest, StringArrayAppend) {
+ wpi::log::StringArrayLogEntry entry{log, "a", 5};
+ entry.Append({"x"}, 7);
+ log.Flush();
+ ASSERT_EQ(data.size(), 55u);
+}
+
+TEST_F(DataLogTest, StringArrayUpdate) {
+ wpi::log::StringArrayLogEntry entry{log, "a", 5};
+ ASSERT_FALSE(entry.GetLastValue().has_value());
+ entry.Update({"x"}, 7);
+ log.Flush();
+ ASSERT_EQ(data.size(), 55u);
+ ASSERT_TRUE(entry.GetLastValue().has_value());
+ ASSERT_EQ(entry.GetLastValue().value(), std::vector{"x"});
+ entry.Update({"x"}, 8);
+ log.Flush();
+ ASSERT_EQ(data.size(), 55u);
+ entry.Update({"y"}, 9);
+ log.Flush();
+ ASSERT_EQ(data.size(), 68u);
+ ASSERT_TRUE(entry.GetLastValue().has_value());
+ ASSERT_EQ(entry.GetLastValue().value(), std::vector{"y"});
+ entry.Update(std::span{}, 10);
+ log.Flush();
+ ASSERT_EQ(data.size(), 76u);
+ ASSERT_TRUE(entry.GetLastValue().has_value());
+ ASSERT_EQ(entry.GetLastValue().value(), std::vector{});
}
TEST_F(DataLogTest, StructA) {
@@ -144,6 +514,27 @@ TEST_F(DataLogTest, StructA) {
entry.Append(ThingA{}, 7);
}
+TEST_F(DataLogTest, StructUpdate) {
+ wpi::log::StructLogEntry entry{log, "a", 5};
+ ASSERT_FALSE(entry.GetLastValue().has_value());
+
+ entry.Update(ThingA{}, 7);
+ log.Flush();
+ ASSERT_EQ(data.size(), 122u);
+ ASSERT_TRUE(entry.GetLastValue().has_value());
+ ASSERT_EQ(entry.GetLastValue().value(), ThingA{});
+
+ entry.Update(ThingA{}, 8);
+ log.Flush();
+ ASSERT_EQ(data.size(), 122u);
+
+ entry.Update(ThingA{.x = 1}, 9);
+ log.Flush();
+ ASSERT_EQ(data.size(), 127u);
+ ASSERT_TRUE(entry.GetLastValue().has_value());
+ ASSERT_EQ(entry.GetLastValue().value(), ThingA{.x = 1});
+}
+
TEST_F(DataLogTest, StructArrayA) {
[[maybe_unused]]
wpi::log::StructArrayLogEntry entry0;
@@ -152,6 +543,35 @@ TEST_F(DataLogTest, StructArrayA) {
entry.Append({{ThingA{}, ThingA{}}}, 7);
}
+TEST_F(DataLogTest, StructArrayUpdate) {
+ wpi::log::StructArrayLogEntry entry{log, "a", 5};
+ ASSERT_FALSE(entry.GetLastValue().has_value());
+
+ entry.Update({{ThingA{}, ThingA{.x = 1}}}, 7);
+ log.Flush();
+ ASSERT_EQ(data.size(), 125u);
+ ASSERT_TRUE(entry.GetLastValue().has_value());
+ ASSERT_EQ(entry.GetLastValue().value(),
+ (std::vector{ThingA{}, ThingA{.x = 1}}));
+
+ entry.Update({{ThingA{}, ThingA{.x = 1}}}, 8);
+ log.Flush();
+ ASSERT_EQ(data.size(), 125u);
+
+ entry.Update({{ThingA{}, ThingA{.x = 2}}}, 9);
+ log.Flush();
+ ASSERT_EQ(data.size(), 131u);
+ ASSERT_TRUE(entry.GetLastValue().has_value());
+ ASSERT_EQ(entry.GetLastValue().value(),
+ (std::vector{ThingA{}, ThingA{.x = 2}}));
+
+ entry.Update(std::span{}, 10);
+ log.Flush();
+ ASSERT_EQ(data.size(), 135u);
+ ASSERT_TRUE(entry.GetLastValue().has_value());
+ ASSERT_EQ(entry.GetLastValue().value(), std::vector{});
+}
+
TEST_F(DataLogTest, StructFixedArrayA) {
[[maybe_unused]]
wpi::log::StructArrayLogEntry> entry0;