[datalog] Move all DataLog functionality to new datalog library (#7641)

Currently the major DataLog backend API (reading and writing) is split between wpiutil and glass. In the interest of allowing code that wants to use these APIs to not need to link to glass and declutter wpiutil, all of those APIs are moved to a new library named "datalog".

Signed-off-by: Jade Turner <spacey-sooty@proton.me>
Co-authored-by: Jade Turner <spacey-sooty@proton.me>
Co-authored-by: Gold856 <117957790+Gold856@users.noreply.github.com>
This commit is contained in:
DeltaDizzy
2025-02-19 23:08:17 -06:00
committed by GitHub
parent ac1705ae2b
commit da47f06d70
99 changed files with 778 additions and 330 deletions

View File

@@ -0,0 +1,12 @@
// 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.datalog;
public final class DevMain {
/** Main entry point. */
public static void main(String[] args) {}
private DevMain() {}
}

View File

@@ -0,0 +1,5 @@
// 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.
int main() {}

View File

@@ -0,0 +1,151 @@
// 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.datalog;
import java.util.Arrays;
/** Log array of boolean values. */
public class BooleanArrayLogEntry extends DataLogEntry {
/** The data type for boolean array values. */
public static final String kDataType = "boolean[]";
/**
* Constructs a boolean array log entry.
*
* @param log datalog
* @param name name of the entry
* @param metadata metadata
* @param timestamp entry creation timestamp (0=now)
*/
public BooleanArrayLogEntry(DataLog log, String name, String metadata, long timestamp) {
super(log, name, kDataType, metadata, timestamp);
}
/**
* Constructs a boolean array log entry.
*
* @param log datalog
* @param name name of the entry
* @param metadata metadata
*/
public BooleanArrayLogEntry(DataLog log, String name, String metadata) {
this(log, name, metadata, 0);
}
/**
* Constructs a boolean array log entry.
*
* @param log datalog
* @param name name of the entry
* @param timestamp entry creation timestamp (0=now)
*/
public BooleanArrayLogEntry(DataLog log, String name, long timestamp) {
this(log, name, "", timestamp);
}
/**
* Constructs a boolean array log entry.
*
* @param log datalog
* @param name name of the entry
*/
public BooleanArrayLogEntry(DataLog log, String name) {
this(log, name, 0);
}
/**
* Appends a record to the log.
*
* @param value Value to record
* @param timestamp Time stamp (0 to indicate now)
*/
public void append(boolean[] value, long timestamp) {
m_log.appendBooleanArray(m_entry, value, timestamp);
}
/**
* Appends a record to the log.
*
* @param value Value to record
*/
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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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;
}

View File

@@ -0,0 +1,130 @@
// 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.datalog;
/** Log boolean values. */
public class BooleanLogEntry extends DataLogEntry {
/** The data type for boolean values. */
public static final String kDataType = "boolean";
/**
* Constructs a boolean log entry.
*
* @param log datalog
* @param name name of the entry
* @param metadata metadata
* @param timestamp entry creation timestamp (0=now)
*/
public BooleanLogEntry(DataLog log, String name, String metadata, long timestamp) {
super(log, name, kDataType, metadata, timestamp);
}
/**
* Constructs a boolean log entry.
*
* @param log datalog
* @param name name of the entry
* @param metadata metadata
*/
public BooleanLogEntry(DataLog log, String name, String metadata) {
this(log, name, metadata, 0);
}
/**
* Constructs a boolean log entry.
*
* @param log datalog
* @param name name of the entry
* @param timestamp entry creation timestamp (0=now)
*/
public BooleanLogEntry(DataLog log, String name, long timestamp) {
this(log, name, "", timestamp);
}
/**
* Constructs a boolean log entry.
*
* @param log datalog
* @param name name of the entry
*/
public BooleanLogEntry(DataLog log, String name) {
this(log, name, 0);
}
/**
* Appends a record to the log.
*
* @param value Value to record
* @param timestamp Time stamp (0 to indicate now)
*/
public void append(boolean value, long timestamp) {
m_log.appendBoolean(m_entry, value, timestamp);
}
/**
* Appends a record to the log.
*
* @param value Value to record
*/
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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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;
}

View File

@@ -0,0 +1,465 @@
// 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.datalog;
import edu.wpi.first.util.WPIUtilJNI;
import edu.wpi.first.util.protobuf.Protobuf;
import edu.wpi.first.util.struct.Struct;
import java.nio.ByteBuffer;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* A data log for high-speed writing of data values.
*
* <p>The finish() function is needed only to indicate in the log that a particular entry is no
* longer being used (it releases the name to ID mapping). The finish() function is not required to
* be called for data to be flushed to disk; entries in the log are written as append() calls are
* being made. In fact, finish() does not need to be called at all.
*
* <p>DataLog calls are thread safe. DataLog uses a typical multiple-supplier, single-consumer
* setup. Writes to the log are atomic, but there is no guaranteed order in the log when multiple
* threads are writing to it; whichever thread grabs the write mutex first will get written first.
* For this reason (as well as the fact that timestamps can be set to arbitrary values), records in
* the log are not guaranteed to be sorted by timestamp.
*/
public class DataLog implements AutoCloseable {
/**
* Constructs.
*
* @param impl implementation handle
*/
protected DataLog(long impl) {
m_impl = impl;
}
/** Explicitly flushes the log data to disk. */
public void flush() {
DataLogJNI.flush(m_impl);
}
/**
* 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.
*/
public void pause() {
DataLogJNI.pause(m_impl);
}
/** Resumes appending of data records to the log. */
public void resume() {
DataLogJNI.resume(m_impl);
}
/** Stops appending all records to the log, and closes the log file. */
public void stop() {
DataLogJNI.stop(m_impl);
}
/**
* Returns whether there is a data schema already registered with the given name.
*
* @param name Name (the string passed as the data type for records using this schema)
* @return True if schema already registered
*/
public boolean hasSchema(String name) {
return m_schemaMap.containsKey(name);
}
/**
* Registers a data schema. Data schemas provide information for how a certain data type string
* can be decoded. The type string of a data schema indicates the type of the schema itself (e.g.
* "protobuf" for protobuf schemas, "struct" for struct schemas, etc). In the data log, schemas
* are saved just like normal records, with the name being generated from the provided name:
* "/.schema/name". Duplicate calls to this function with the same name are silently ignored.
*
* @param name Name (the string passed as the data type for records using this schema)
* @param type Type of schema (e.g. "protobuf", "struct", etc)
* @param schema Schema data
* @param timestamp Time stamp (may be 0 to indicate now)
*/
public void addSchema(String name, String type, byte[] schema, long timestamp) {
if (m_schemaMap.putIfAbsent(name, 1) != null) {
return;
}
DataLogJNI.addSchema(m_impl, name, type, schema, timestamp);
}
/**
* Registers a data schema. Data schemas provide information for how a certain data type string
* can be decoded. The type string of a data schema indicates the type of the schema itself (e.g.
* "protobuf" for protobuf schemas, "struct" for struct schemas, etc). In the data log, schemas
* are saved just like normal records, with the name being generated from the provided name:
* "/.schema/name". Duplicate calls to this function with the same name are silently ignored.
*
* @param name Name (the string passed as the data type for records using this schema)
* @param type Type of schema (e.g. "protobuf", "struct", etc)
* @param schema Schema data
*/
public void addSchema(String name, String type, byte[] schema) {
addSchema(name, type, schema, 0);
}
/**
* Registers a data schema. Data schemas provide information for how a certain data type string
* can be decoded. The type string of a data schema indicates the type of the schema itself (e.g.
* "protobuf" for protobuf schemas, "struct" for struct schemas, etc). In the data log, schemas
* are saved just like normal records, with the name being generated from the provided name:
* "/.schema/name". Duplicate calls to this function with the same name are silently ignored.
*
* @param name Name (the string passed as the data type for records using this schema)
* @param type Type of schema (e.g. "protobuf", "struct", etc)
* @param schema Schema data
* @param timestamp Time stamp (may be 0 to indicate now)
*/
public void addSchema(String name, String type, String schema, long timestamp) {
if (m_schemaMap.putIfAbsent(name, 1) != null) {
return;
}
DataLogJNI.addSchemaString(m_impl, name, type, schema, timestamp);
}
/**
* Registers a data schema. Data schemas provide information for how a certain data type string
* can be decoded. The type string of a data schema indicates the type of the schema itself (e.g.
* "protobuf" for protobuf schemas, "struct" for struct schemas, etc). In the data log, schemas
* are saved just like normal records, with the name being generated from the provided name:
* "/.schema/name". Duplicate calls to this function with the same name are silently ignored.
*
* @param name Name (the string passed as the data type for records using this schema)
* @param type Type of schema (e.g. "protobuf", "struct", etc)
* @param schema Schema data
*/
public void addSchema(String name, String type, String schema) {
addSchema(name, type, schema, 0);
}
/**
* Registers a protobuf schema. Duplicate calls to this function with the same name are silently
* ignored.
*
* @param proto protobuf serialization object
* @param timestamp Time stamp (0 to indicate now)
*/
public void addSchema(Protobuf<?, ?> proto, long timestamp) {
final long actualTimestamp = timestamp == 0 ? WPIUtilJNI.now() : timestamp;
proto.forEachDescriptor(
this::hasSchema,
(typeString, schema) ->
addSchema(typeString, "proto:FileDescriptorProto", schema, actualTimestamp));
}
/**
* Registers a protobuf schema. Duplicate calls to this function with the same name are silently
* ignored.
*
* @param proto protobuf serialization object
*/
public void addSchema(Protobuf<?, ?> proto) {
addSchema(proto, 0);
}
/**
* Registers a struct schema. Duplicate calls to this function with the same name are silently
* ignored.
*
* @param struct struct serialization object
* @param timestamp Time stamp (0 to indicate now)
*/
public void addSchema(Struct<?> struct, long timestamp) {
addSchemaImpl(struct, timestamp == 0 ? WPIUtilJNI.now() : timestamp, new HashSet<>());
}
/**
* Registers a struct schema. Duplicate calls to this function with the same name are silently
* ignored.
*
* @param struct struct serialization object
*/
public void addSchema(Struct<?> struct) {
addSchema(struct, 0);
}
/**
* Start an entry. Duplicate names are allowed (with the same type), and result in the same index
* being returned (start/finish are reference counted). A duplicate name with a different type
* will result in an error message being printed to the console and 0 being returned (which will
* be ignored by the append functions).
*
* @param name Name
* @param type Data type
* @param metadata Initial metadata (e.g. data properties)
* @param timestamp Time stamp (0 to indicate now)
* @return Entry index
*/
public int start(String name, String type, String metadata, long timestamp) {
return DataLogJNI.start(m_impl, name, type, metadata, timestamp);
}
/**
* Start an entry. Duplicate names are allowed (with the same type), and result in the same index
* being returned (start/finish are reference counted). A duplicate name with a different type
* will result in an error message being printed to the console and 0 being returned (which will
* be ignored by the append functions).
*
* @param name Name
* @param type Data type
* @param metadata Initial metadata (e.g. data properties)
* @return Entry index
*/
public int start(String name, String type, String metadata) {
return start(name, type, metadata, 0);
}
/**
* Start an entry. Duplicate names are allowed (with the same type), and result in the same index
* being returned (start/finish are reference counted). A duplicate name with a different type
* will result in an error message being printed to the console and 0 being returned (which will
* be ignored by the append functions).
*
* @param name Name
* @param type Data type
* @return Entry index
*/
public int start(String name, String type) {
return start(name, type, "");
}
/**
* Finish an entry.
*
* @param entry Entry index
* @param timestamp Time stamp (0 to indicate now)
*/
public void finish(int entry, long timestamp) {
DataLogJNI.finish(m_impl, entry, timestamp);
}
/**
* Finish an entry.
*
* @param entry Entry index
*/
public void finish(int entry) {
finish(entry, 0);
}
/**
* Updates the metadata for an entry.
*
* @param entry Entry index
* @param metadata New metadata for the entry
* @param timestamp Time stamp (0 to indicate now)
*/
public void setMetadata(int entry, String metadata, long timestamp) {
DataLogJNI.setMetadata(m_impl, entry, metadata, timestamp);
}
/**
* Updates the metadata for an entry.
*
* @param entry Entry index
* @param metadata New metadata for the entry
*/
public void setMetadata(int entry, String metadata) {
setMetadata(entry, metadata, 0);
}
@Override
public void close() {
DataLogJNI.close(m_impl);
m_impl = 0;
}
/**
* Appends a raw record to the log.
*
* @param entry Entry index, as returned by start()
* @param data Byte array to record; will send entire array contents
* @param timestamp Time stamp (0 to indicate now)
*/
public void appendRaw(int entry, byte[] data, long timestamp) {
appendRaw(entry, data, 0, data.length, timestamp);
}
/**
* Appends a record to the log.
*
* @param entry Entry index, as returned by start()
* @param data Byte array to record
* @param start Start position of data (in byte array)
* @param len Length of data (must be less than or equal to data.length - start)
* @param timestamp Time stamp (0 to indicate now)
*/
public void appendRaw(int entry, byte[] data, int start, int len, long timestamp) {
DataLogJNI.appendRaw(m_impl, entry, data, start, len, timestamp);
}
/**
* Appends a record to the log.
*
* @param entry Entry index, as returned by start()
* @param data Buffer to record; will send from data.position() to data.limit()
* @param timestamp Time stamp (0 to indicate now)
*/
public void appendRaw(int entry, ByteBuffer data, long timestamp) {
int pos = data.position();
appendRaw(entry, data, pos, data.limit() - pos, timestamp);
}
/**
* Appends a record to the log.
*
* @param entry Entry index, as returned by start()
* @param data Buffer to record
* @param start Start position of data (in buffer)
* @param len Length of data (must be less than or equal to data.capacity() - start)
* @param timestamp Time stamp (0 to indicate now)
*/
public void appendRaw(int entry, ByteBuffer data, int start, int len, long timestamp) {
DataLogJNI.appendRaw(m_impl, entry, data, start, len, timestamp);
}
/**
* Appends a boolean record to the log.
*
* @param entry Entry index, as returned by start()
* @param value Boolean value to record
* @param timestamp Time stamp (0 to indicate now)
*/
public void appendBoolean(int entry, boolean value, long timestamp) {
DataLogJNI.appendBoolean(m_impl, entry, value, timestamp);
}
/**
* Appends an integer record to the log.
*
* @param entry Entry index, as returned by start()
* @param value Integer value to record
* @param timestamp Time stamp (0 to indicate now)
*/
public void appendInteger(int entry, long value, long timestamp) {
DataLogJNI.appendInteger(m_impl, entry, value, timestamp);
}
/**
* Appends a float record to the log.
*
* @param entry Entry index, as returned by start()
* @param value Float value to record
* @param timestamp Time stamp (0 to indicate now)
*/
public void appendFloat(int entry, float value, long timestamp) {
DataLogJNI.appendFloat(m_impl, entry, value, timestamp);
}
/**
* Appends a double record to the log.
*
* @param entry Entry index, as returned by start()
* @param value Double value to record
* @param timestamp Time stamp (0 to indicate now)
*/
public void appendDouble(int entry, double value, long timestamp) {
DataLogJNI.appendDouble(m_impl, entry, value, timestamp);
}
/**
* Appends a string record to the log.
*
* @param entry Entry index, as returned by start()
* @param value String value to record
* @param timestamp Time stamp (0 to indicate now)
*/
public void appendString(int entry, String value, long timestamp) {
DataLogJNI.appendString(m_impl, entry, value, timestamp);
}
/**
* Appends a boolean array record to the log.
*
* @param entry Entry index, as returned by start()
* @param arr Boolean array to record
* @param timestamp Time stamp (0 to indicate now)
*/
public void appendBooleanArray(int entry, boolean[] arr, long timestamp) {
DataLogJNI.appendBooleanArray(m_impl, entry, arr, timestamp);
}
/**
* Appends an integer array record to the log.
*
* @param entry Entry index, as returned by start()
* @param arr Integer array to record
* @param timestamp Time stamp (0 to indicate now)
*/
public void appendIntegerArray(int entry, long[] arr, long timestamp) {
DataLogJNI.appendIntegerArray(m_impl, entry, arr, timestamp);
}
/**
* Appends a float array record to the log.
*
* @param entry Entry index, as returned by start()
* @param arr Float array to record
* @param timestamp Time stamp (0 to indicate now)
*/
public void appendFloatArray(int entry, float[] arr, long timestamp) {
DataLogJNI.appendFloatArray(m_impl, entry, arr, timestamp);
}
/**
* Appends a double array record to the log.
*
* @param entry Entry index, as returned by start()
* @param arr Double array to record
* @param timestamp Time stamp (0 to indicate now)
*/
public void appendDoubleArray(int entry, double[] arr, long timestamp) {
DataLogJNI.appendDoubleArray(m_impl, entry, arr, timestamp);
}
/**
* Appends a string array record to the log.
*
* @param entry Entry index, as returned by start()
* @param arr String array to record
* @param timestamp Time stamp (0 to indicate now)
*/
public void appendStringArray(int entry, String[] arr, long timestamp) {
DataLogJNI.appendStringArray(m_impl, entry, arr, timestamp);
}
/**
* Gets the JNI implementation handle.
*
* @return data log handle.
*/
public long getImpl() {
return m_impl;
}
private void addSchemaImpl(Struct<?> struct, long timestamp, Set<String> seen) {
String typeString = struct.getTypeString();
if (hasSchema(typeString)) {
return;
}
if (!seen.add(typeString)) {
throw new UnsupportedOperationException(typeString + ": circular reference with " + seen);
}
addSchema(typeString, "structschema", struct.getSchema(), timestamp);
for (Struct<?> inner : struct.getNested()) {
addSchemaImpl(inner, timestamp, seen);
}
seen.remove(typeString);
}
/** Implementation handle. */
protected long m_impl;
private final ConcurrentMap<String, Integer> m_schemaMap = new ConcurrentHashMap<>();
}

View File

@@ -0,0 +1,86 @@
// 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.datalog;
/**
* A data log background writer that periodically flushes the data log on a background thread. The
* data log file is created immediately upon construction with a temporary filename. The file may be
* renamed at any time using the setFilename() function.
*
* <p>The data log is periodically flushed to disk. It can also be explicitly flushed to disk by
* using the flush() function. This operation is, however, non-blocking.
*/
public final class DataLogBackgroundWriter extends DataLog {
/**
* Construct a new Data Log. The log will be initially created with a temporary filename.
*
* @param dir directory to store the log
* @param filename filename to use; if none provided, a random filename is generated of the form
* "wpilog_{}.wpilog"
* @param period time between automatic flushes to disk, in seconds; this is a time/storage
* tradeoff
* @param extraHeader extra header data
*/
public DataLogBackgroundWriter(String dir, String filename, double period, String extraHeader) {
super(DataLogJNI.bgCreate(dir, filename, period, extraHeader));
}
/**
* Construct a new Data Log. The log will be initially created with a temporary filename.
*
* @param dir directory to store the log
* @param filename filename to use; if none provided, a random filename is generated of the form
* "wpilog_{}.wpilog"
* @param period time between automatic flushes to disk, in seconds; this is a time/storage
* tradeoff
*/
public DataLogBackgroundWriter(String dir, String filename, double period) {
this(dir, filename, period, "");
}
/**
* Construct a new Data Log. The log will be initially created with a temporary filename.
*
* @param dir directory to store the log
* @param filename filename to use; if none provided, a random filename is generated of the form
* "wpilog_{}.wpilog"
*/
public DataLogBackgroundWriter(String dir, String filename) {
this(dir, filename, 0.25);
}
/**
* Construct a new Data Log. The log will be initially created with a temporary filename.
*
* @param dir directory to store the log
*/
public DataLogBackgroundWriter(String dir) {
this(dir, "", 0.25);
}
/** Construct a new Data Log. The log will be initially created with a temporary filename. */
public DataLogBackgroundWriter() {
this("");
}
/**
* Change log filename.
*
* @param filename filename
*/
public void setFilename(String filename) {
DataLogJNI.bgSetFilename(m_impl, filename);
}
/**
* Resumes appending of data records to the log. If called after stop(), opens a new file (with
* random name if SetFilename was not called after stop()) and appends Start records and schema
* data values for all previously started entries and schemas.
*/
@Override
public void resume() {
DataLogJNI.resume(m_impl);
}
}

View File

@@ -0,0 +1,84 @@
// 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.datalog;
/** Log entry base class. */
public class DataLogEntry {
/**
* Constructs a data log entry.
*
* @param log datalog
* @param name name of the entry
* @param type Data type
* @param metadata metadata
* @param timestamp entry creation timestamp (0=now)
*/
protected DataLogEntry(DataLog log, String name, String type, String metadata, long timestamp) {
m_log = log;
m_entry = log.start(name, type, metadata, timestamp);
}
/**
* Constructs a data log entry.
*
* @param log datalog
* @param name name of the entry
* @param type Data type
* @param metadata metadata
*/
protected DataLogEntry(DataLog log, String name, String type, String metadata) {
this(log, name, type, metadata, 0);
}
/**
* Constructs a data log entry.
*
* @param log datalog
* @param name name of the entry
* @param type Data type
*/
protected DataLogEntry(DataLog log, String name, String type) {
this(log, name, type, "");
}
/**
* Updates the metadata for the entry.
*
* @param metadata New metadata for the entry
* @param timestamp Time stamp (0 to indicate now)
*/
public void setMetadata(String metadata, long timestamp) {
m_log.setMetadata(m_entry, metadata, timestamp);
}
/**
* Updates the metadata for the entry.
*
* @param metadata New metadata for the entry
*/
public void setMetadata(String metadata) {
setMetadata(metadata, 0);
}
/**
* Finishes the entry.
*
* @param timestamp Time stamp (0 to indicate now)
*/
public void finish(long timestamp) {
m_log.finish(m_entry, timestamp);
}
/** Finishes the entry. */
public void finish() {
finish(0);
}
/** The data log instance associated with the entry. */
protected final DataLog m_log;
/** The data log entry index. */
protected final int m_entry;
}

View File

@@ -0,0 +1,46 @@
// 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.datalog;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.function.Consumer;
/** DataLogReader iterator. */
public class DataLogIterator implements Iterator<DataLogRecord> {
DataLogIterator(DataLogReader reader, int pos) {
m_reader = reader;
m_pos = pos;
}
@Override
public void forEachRemaining(Consumer<? super DataLogRecord> action) {
int size = m_reader.size();
for (; m_pos < size; m_pos = m_reader.getNextRecord(m_pos)) {
DataLogRecord record;
try {
record = m_reader.getRecord(m_pos);
} catch (NoSuchElementException ex) {
break;
}
action.accept(record);
}
}
@Override
public boolean hasNext() {
return (m_pos + 16) <= m_reader.size();
}
@Override
public DataLogRecord next() {
DataLogRecord record = m_reader.getRecord(m_pos);
m_pos = m_reader.getNextRecord(m_pos);
return record;
}
private final DataLogReader m_reader;
private int m_pos;
}

View File

@@ -0,0 +1,375 @@
// 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.datalog;
import edu.wpi.first.util.RuntimeLoader;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* DataLog JNI Functions.
*
* @see "datalog/DataLog.h"
*/
public class DataLogJNI {
static boolean libraryLoaded = false;
/** Sets whether JNI should be loaded in the static block. */
public static class Helper {
private static AtomicBoolean extractOnStaticLoad = new AtomicBoolean(true);
/**
* Returns true if the JNI should be loaded in the static block.
*
* @return True if the JNI should be loaded in the static block.
*/
public static boolean getExtractOnStaticLoad() {
return extractOnStaticLoad.get();
}
/**
* Sets whether the JNI should be loaded in the static block.
*
* @param load Whether the JNI should be loaded in the static block.
*/
public static void setExtractOnStaticLoad(boolean load) {
extractOnStaticLoad.set(load);
}
/** Utility class. */
private Helper() {}
}
static {
if (Helper.getExtractOnStaticLoad()) {
try {
RuntimeLoader.loadLibrary("datalogjni");
} catch (Exception ex) {
ex.printStackTrace();
System.exit(1);
}
libraryLoaded = true;
}
}
/**
* Force load the library.
*
* @throws IOException if the library failed to load
*/
public static synchronized void forceLoad() throws IOException {
if (libraryLoaded) {
return;
}
RuntimeLoader.loadLibrary("datalogjni");
libraryLoaded = true;
}
/**
* Create a new Data Log background writer. The log will be initially created with a temporary
* filename.
*
* @param dir directory to store the log
* @param filename filename to use; if none provided, a random filename is generated of the form
* "wpilog_{}.wpilog"
* @param period time between automatic flushes to disk, in seconds; this is a time/storage
* tradeoff
* @param extraHeader extra header data
* @return data log background writer implementation handle
*/
static native long bgCreate(String dir, String filename, double period, String extraHeader);
/**
* Change log filename.
*
* @param impl data log background writer implementation handle
* @param filename filename
*/
static native void bgSetFilename(long impl, String filename);
/**
* Create a new Data Log foreground writer.
*
* @param filename filename to use
* @param extraHeader extra header data
* @return data log writer implementation handle
* @throws IOException if file cannot be opened
*/
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.
*
* @param impl data log background writer implementation handle
*/
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.
*
* @param impl data log background writer implementation handle
*/
static native void pause(long impl);
/**
* Resumes appending of data records to the log. If called after Stop(), opens a new file (with
* random name if SetFilename was not called after Stop()) and appends Start records and schema
* data values for all previously started entries and schemas.
*
* @param impl data log background writer implementation handle
*/
static native void resume(long impl);
/**
* Stops appending all records to the log, and closes the log file.
*
* @param impl data log background writer implementation handle
*/
static native void stop(long impl);
/**
* Registers a data schema. Data schemas provide information for how a certain data type string
* can be decoded. The type string of a data schema indicates the type of the schema itself (e.g.
* "protobuf" for protobuf schemas, "struct" for struct schemas, etc). In the data log, schemas
* are saved just like normal records, with the name being generated from the provided name:
* "/.schema/&lt;name&gt;". Duplicate calls to this function with the same name are silently
* ignored.
*
* @param impl data log implementation handle
* @param name Name (the string passed as the data type for records using this schema)
* @param type Type of schema (e.g. "protobuf", "struct", etc)
* @param schema Schema data
* @param timestamp Time stamp (may be 0 to indicate now)
*/
static native void addSchema(long impl, String name, String type, byte[] schema, long timestamp);
static native void addSchemaString(
long impl, String name, String type, String schema, long timestamp);
/**
* Start an entry. Duplicate names are allowed (with the same type), and result in the same index
* being returned (Start/Finish are reference counted). A duplicate name with a different type
* will result in an error message being printed to the console and 0 being returned (which will
* be ignored by the Append functions).
*
* @param impl data log implementation handle
* @param name Name
* @param type Data type
* @param metadata Initial metadata (e.g. data properties)
* @param timestamp Time stamp (may be 0 to indicate now)
* @return Entry index
*/
static native int start(long impl, String name, String type, String metadata, long timestamp);
/**
* Finish an entry.
*
* @param impl data log implementation handle
* @param entry Entry index
* @param timestamp Time stamp (may be 0 to indicate now)
*/
static native void finish(long impl, int entry, long timestamp);
/**
* Updates the metadata for an entry.
*
* @param impl data log implementation handle
* @param entry Entry index
* @param metadata New metadata for the entry
* @param timestamp Time stamp (may be 0 to indicate now)
*/
static native void setMetadata(long impl, int entry, String metadata, long timestamp);
/**
* Closes the data log implementation handle.
*
* @param impl data log implementation handle
*/
static native void close(long impl);
/**
* Appends a raw record to the log.
*
* @param impl data log implementation handle
* @param entry Entry index, as returned by WPI_DataLog_Start()
* @param data Byte array to record
* @param len Length of byte array
* @param timestamp Time stamp (may be 0 to indicate now)
*/
static native void appendRaw(
long impl, int entry, byte[] data, int start, int len, long timestamp);
/**
* Appends a raw record to the log.
*
* @param impl data log implementation handle
* @param entry Entry index, as returned by WPI_DataLog_Start()
* @param data ByteBuffer to record
* @param len Length of byte array
* @param timestamp Time stamp (may be 0 to indicate now)
*/
static void appendRaw(long impl, int entry, ByteBuffer data, int start, int len, long timestamp) {
if (data.isDirect()) {
if (start < 0) {
throw new IndexOutOfBoundsException("start must be >= 0");
}
if (len < 0) {
throw new IndexOutOfBoundsException("len must be >= 0");
}
if ((start + len) > data.capacity()) {
throw new IndexOutOfBoundsException("start + len must be smaller than buffer capacity");
}
appendRawBuffer(impl, entry, data, start, len, timestamp);
} else if (data.hasArray()) {
appendRaw(impl, entry, data.array(), data.arrayOffset() + start, len, timestamp);
} else {
throw new UnsupportedOperationException("ByteBuffer must be direct or have a backing array");
}
}
private static native void appendRawBuffer(
long impl, int entry, ByteBuffer data, int start, int len, long timestamp);
/**
* Appends a boolean record to the log.
*
* @param impl data log implementation handle
* @param entry Entry index, as returned by Start()
* @param value Boolean value to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
static native void appendBoolean(long impl, int entry, boolean value, long timestamp);
/**
* Appends an integer record to the log.
*
* @param impl data log implementation handle
* @param entry Entry index, as returned by Start()
* @param value Integer value to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
static native void appendInteger(long impl, int entry, long value, long timestamp);
/**
* Appends a float record to the log.
*
* @param impl data log implementation handle
* @param entry Entry index, as returned by Start()
* @param value Float value to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
static native void appendFloat(long impl, int entry, float value, long timestamp);
/**
* Appends a double record to the log.
*
* @param impl data log implementation handle
* @param entry Entry index, as returned by Start()
* @param value Double value to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
static native void appendDouble(long impl, int entry, double value, long timestamp);
/**
* Appends a string record to the log.
*
* @param impl data log implementation handle
* @param entry Entry index, as returned by Start()
* @param value String value to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
static native void appendString(long impl, int entry, String value, long timestamp);
/**
* Appends a boolean array record to the log.
*
* @param impl data log implementation handle
* @param entry Entry index, as returned by Start()
* @param value Boolean array to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
static native void appendBooleanArray(long impl, int entry, boolean[] value, long timestamp);
/**
* Appends an integer array record to the log.
*
* @param impl data log implementation handle
* @param entry Entry index, as returned by Start()
* @param value Integer array to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
static native void appendIntegerArray(long impl, int entry, long[] value, long timestamp);
/**
* Appends a float array record to the log.
*
* @param impl data log implementation handle
* @param entry Entry index, as returned by Start()
* @param value Float array to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
static native void appendFloatArray(long impl, int entry, float[] value, long timestamp);
/**
* Appends a double array record to the log.
*
* @param impl data log implementation handle
* @param entry Entry index, as returned by Start()
* @param value Double array to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
static native void appendDoubleArray(long impl, int entry, double[] value, long timestamp);
/**
* Appends a string array record to the log.
*
* @param impl data log implementation handle
* @param entry Entry index, as returned by Start()
* @param value String array to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
static native void appendStringArray(long impl, int entry, String[] value, long timestamp);
/**
* Create a native FileLogger. When the specified file is modified, appended data will be appended
* to the specified data log.
*
* @param file path to the file
* @param log data log implementation handle
* @param key log key to append data to
* @return The FileLogger handle.
*/
public static native long createFileLogger(String file, long log, String key);
/**
* Free a native FileLogger. This causes the FileLogger to stop appending data to the log.
*
* @param fileTail The FileLogger handle.
*/
public static native void freeFileLogger(long fileTail);
/** Utility class. */
private DataLogJNI() {}
}

View File

@@ -0,0 +1,154 @@
// 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.datalog;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.util.NoSuchElementException;
import java.util.function.Consumer;
/** Data log reader (reads logs written by the DataLog class). */
public class DataLogReader implements Iterable<DataLogRecord> {
/**
* Constructs from a byte buffer.
*
* @param buffer byte buffer
*/
public DataLogReader(ByteBuffer buffer) {
m_buf = buffer;
m_buf.order(ByteOrder.LITTLE_ENDIAN);
}
/**
* Constructs from a file.
*
* @param filename filename
* @throws IOException if unable to open/read file
*/
public DataLogReader(String filename) throws IOException {
RandomAccessFile f = new RandomAccessFile(filename, "r");
FileChannel channel = f.getChannel();
m_buf = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size());
m_buf.order(ByteOrder.LITTLE_ENDIAN);
channel.close();
f.close();
}
/**
* Returns true if the data log is valid (e.g. has a valid header).
*
* @return True if valid, false otherwise
*/
public boolean isValid() {
return m_buf.remaining() >= 12
&& m_buf.get(0) == 'W'
&& m_buf.get(1) == 'P'
&& m_buf.get(2) == 'I'
&& m_buf.get(3) == 'L'
&& m_buf.get(4) == 'O'
&& m_buf.get(5) == 'G'
&& m_buf.getShort(6) >= 0x0100;
}
/**
* Gets the data log version. Returns 0 if data log is invalid.
*
* @return Version number; most significant byte is major, least significant is minor (so version
* 1.0 will be 0x0100)
*/
public short getVersion() {
if (m_buf.remaining() < 12) {
return 0;
}
return m_buf.getShort(6);
}
/**
* Gets the extra header data.
*
* @return Extra header data
*/
public String getExtraHeader() {
ByteBuffer buf = m_buf.duplicate();
buf.order(ByteOrder.LITTLE_ENDIAN);
buf.position(8);
int size = buf.getInt();
byte[] arr = new byte[size];
buf.get(arr);
return new String(arr, StandardCharsets.UTF_8);
}
@Override
public void forEach(Consumer<? super DataLogRecord> action) {
int size = m_buf.remaining();
for (int pos = 12 + m_buf.getInt(8); pos < size; pos = getNextRecord(pos)) {
DataLogRecord record;
try {
record = getRecord(pos);
} catch (NoSuchElementException ex) {
break;
}
action.accept(record);
}
}
@Override
public DataLogIterator iterator() {
return new DataLogIterator(this, 12 + m_buf.getInt(8));
}
private long readVarInt(int pos, int len) {
long val = 0;
for (int i = 0; i < len; i++) {
val |= ((long) (m_buf.get(pos + i) & 0xff)) << (i * 8);
}
return val;
}
DataLogRecord getRecord(int pos) {
try {
int lenbyte = m_buf.get(pos) & 0xff;
int entryLen = (lenbyte & 0x3) + 1;
int sizeLen = ((lenbyte >> 2) & 0x3) + 1;
int timestampLen = ((lenbyte >> 4) & 0x7) + 1;
int headerLen = 1 + entryLen + sizeLen + timestampLen;
int entry = (int) readVarInt(pos + 1, entryLen);
int size = (int) readVarInt(pos + 1 + entryLen, sizeLen);
long timestamp = readVarInt(pos + 1 + entryLen + sizeLen, timestampLen);
// build a slice of the data contents
ByteBuffer data = m_buf.duplicate();
data.position(pos + headerLen);
data.limit(pos + headerLen + size);
return new DataLogRecord(entry, timestamp, data.slice());
} catch (BufferUnderflowException | IndexOutOfBoundsException ex) {
throw new NoSuchElementException();
}
}
int getNextRecord(int pos) {
int lenbyte = m_buf.get(pos) & 0xff;
int entryLen = (lenbyte & 0x3) + 1;
int sizeLen = ((lenbyte >> 2) & 0x3) + 1;
int timestampLen = ((lenbyte >> 4) & 0x7) + 1;
int headerLen = 1 + entryLen + sizeLen + timestampLen;
int size = 0;
for (int i = 0; i < sizeLen; i++) {
size |= (m_buf.get(pos + 1 + entryLen + i) & 0xff) << (i * 8);
}
return pos + headerLen + size;
}
int size() {
return m_buf.remaining();
}
private final ByteBuffer m_buf;
}

View File

@@ -0,0 +1,430 @@
// 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.datalog;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.DoubleBuffer;
import java.nio.FloatBuffer;
import java.nio.LongBuffer;
import java.nio.charset.StandardCharsets;
import java.util.InputMismatchException;
/**
* A record in the data log. May represent either a control record (entry == 0) or a data record.
* Used only for reading (e.g. with DataLogReader).
*/
public class DataLogRecord {
private static final int kControlStart = 0;
private static final int kControlFinish = 1;
private static final int kControlSetMetadata = 2;
DataLogRecord(int entry, long timestamp, ByteBuffer data) {
m_entry = entry;
m_timestamp = timestamp;
m_data = data;
m_data.order(ByteOrder.LITTLE_ENDIAN);
}
/**
* Gets the entry ID.
*
* @return entry ID
*/
public int getEntry() {
return m_entry;
}
/**
* Gets the record timestamp.
*
* @return Timestamp, in integer microseconds
*/
public long getTimestamp() {
return m_timestamp;
}
/**
* Gets the size of the raw data.
*
* @return size
*/
public int getSize() {
return m_data.remaining();
}
/**
* Gets the raw data. Use the GetX functions to decode based on the data type in the entry's start
* record.
*
* @return byte array
*/
public byte[] getRaw() {
ByteBuffer buf = getRawBuffer();
byte[] arr = new byte[buf.remaining()];
buf.get(arr);
return arr;
}
/**
* Gets the raw data. Use the GetX functions to decode based on the data type in the entry's start
* record.
*
* @return byte buffer
*/
public ByteBuffer getRawBuffer() {
ByteBuffer buf = m_data.duplicate();
buf.order(ByteOrder.LITTLE_ENDIAN);
return buf;
}
/**
* Returns true if the record is a control record.
*
* @return True if control record, false if normal data record.
*/
public boolean isControl() {
return m_entry == 0;
}
/**
* Returns true if the record is a start control record. Use GetStartData() to decode the
* contents.
*
* @return True if start control record, false otherwise.
*/
public boolean isStart() {
return m_entry == 0 && m_data.remaining() >= 17 && m_data.get(0) == kControlStart;
}
/**
* Returns true if the record is a finish control record. Use GetFinishEntry() to decode the
* contents.
*
* @return True if finish control record, false otherwise.
*/
public boolean isFinish() {
return m_entry == 0 && m_data.remaining() == 5 && m_data.get(0) == kControlFinish;
}
/**
* Returns true if the record is a set metadata control record. Use GetSetMetadataData() to decode
* the contents.
*
* @return True if set metadata control record, false otherwise.
*/
public boolean isSetMetadata() {
return m_entry == 0 && m_data.remaining() >= 9 && m_data.get(0) == kControlSetMetadata;
}
/**
* Data contained in a start control record as created by DataLog.start() when writing the log.
* This can be read by calling getStartData().
*/
@SuppressWarnings("MemberName")
public static class StartRecordData {
StartRecordData(int entry, String name, String type, String metadata) {
this.entry = entry;
this.name = name;
this.type = type;
this.metadata = metadata;
}
/** Entry ID; this will be used for this entry in future records. */
public final int entry;
/** Entry name. */
public final String name;
/** Type of the stored data for this entry, as a string, e.g. "double". */
public final String type;
/** Initial metadata. */
public final String metadata;
}
/**
* Decodes a start control record.
*
* @return start record decoded data
* @throws InputMismatchException on error
*/
public StartRecordData getStartData() {
if (!isStart()) {
throw new InputMismatchException("not a start record");
}
ByteBuffer buf = getRawBuffer();
buf.position(1); // skip over control type
int entry = buf.getInt();
String name = readInnerString(buf);
String type = readInnerString(buf);
String metadata = readInnerString(buf);
return new StartRecordData(entry, name, type, metadata);
}
/**
* Data contained in a set metadata control record as created by DataLog.setMetadata(). This can
* be read by calling getSetMetadataData().
*/
@SuppressWarnings("MemberName")
public static class MetadataRecordData {
MetadataRecordData(int entry, String metadata) {
this.entry = entry;
this.metadata = metadata;
}
/** Entry ID. */
public final int entry;
/** New metadata for the entry. */
public final String metadata;
}
/**
* Decodes a finish control record.
*
* @return finish record entry ID
* @throws InputMismatchException on error
*/
public int getFinishEntry() {
if (!isFinish()) {
throw new InputMismatchException("not a finish record");
}
return m_data.getInt(1);
}
/**
* Decodes a set metadata control record.
*
* @return set metadata record decoded data
* @throws InputMismatchException on error
*/
public MetadataRecordData getSetMetadataData() {
if (!isSetMetadata()) {
throw new InputMismatchException("not a set metadata record");
}
ByteBuffer buf = getRawBuffer();
buf.position(1); // skip over control type
int entry = buf.getInt();
String metadata = readInnerString(buf);
return new MetadataRecordData(entry, metadata);
}
/**
* Decodes a data record as a boolean. Note if the data type (as indicated in the corresponding
* start control record for this entry) is not "boolean", invalid results may be returned.
*
* @return boolean value
* @throws InputMismatchException on error
*/
public boolean getBoolean() {
try {
return m_data.get(0) != 0;
} catch (IndexOutOfBoundsException ex) {
throw new InputMismatchException();
}
}
/**
* Decodes a data record as an integer. Note if the data type (as indicated in the corresponding
* start control record for this entry) is not "int64", invalid results may be returned.
*
* @return integer value
* @throws InputMismatchException on error
*/
public long getInteger() {
try {
return m_data.getLong(0);
} catch (BufferUnderflowException | IndexOutOfBoundsException ex) {
throw new InputMismatchException();
}
}
/**
* Decodes a data record as a float. Note if the data type (as indicated in the corresponding
* start control record for this entry) is not "float", invalid results may be returned.
*
* @return float value
* @throws InputMismatchException on error
*/
public float getFloat() {
try {
return m_data.getFloat(0);
} catch (BufferUnderflowException | IndexOutOfBoundsException ex) {
throw new InputMismatchException();
}
}
/**
* Decodes a data record as a double. Note if the data type (as indicated in the corresponding
* start control record for this entry) is not "double", invalid results may be returned.
*
* @return double value
* @throws InputMismatchException on error
*/
public double getDouble() {
try {
return m_data.getDouble(0);
} catch (BufferUnderflowException | IndexOutOfBoundsException ex) {
throw new InputMismatchException();
}
}
/**
* Decodes a data record as a string. Note if the data type (as indicated in the corresponding
* start control record for this entry) is not "string", invalid results may be returned.
*
* @return string value
*/
public String getString() {
return new String(getRaw(), StandardCharsets.UTF_8);
}
/**
* Decodes a data record as a boolean array. Note if the data type (as indicated in the
* corresponding start control record for this entry) is not "boolean[]", invalid results may be
* returned.
*
* @return boolean array
*/
public boolean[] getBooleanArray() {
boolean[] arr = new boolean[m_data.remaining()];
for (int i = 0; i < m_data.remaining(); i++) {
arr[i] = m_data.get(i) != 0;
}
return arr;
}
/**
* Decodes a data record as an integer array. Note if the data type (as indicated in the
* corresponding start control record for this entry) is not "int64[]", invalid results may be
* returned.
*
* @return integer array
* @throws InputMismatchException on error
*/
public long[] getIntegerArray() {
LongBuffer buf = getIntegerBuffer();
long[] arr = new long[buf.remaining()];
buf.get(arr);
return arr;
}
/**
* Decodes a data record as an integer array. Note if the data type (as indicated in the
* corresponding start control record for this entry) is not "int64[]", invalid results may be
* returned.
*
* @return integer buffer
* @throws InputMismatchException on error
*/
public LongBuffer getIntegerBuffer() {
if ((m_data.limit() % 8) != 0) {
throw new InputMismatchException("data size is not a multiple of 8");
}
return m_data.asLongBuffer();
}
/**
* Decodes a data record as a float array. Note if the data type (as indicated in the
* corresponding start control record for this entry) is not "float[]", invalid results may be
* returned.
*
* @return float array
* @throws InputMismatchException on error
*/
public float[] getFloatArray() {
FloatBuffer buf = getFloatBuffer();
float[] arr = new float[buf.remaining()];
buf.get(arr);
return arr;
}
/**
* Decodes a data record as a float array. Note if the data type (as indicated in the
* corresponding start control record for this entry) is not "float[]", invalid results may be
* returned.
*
* @return float buffer
* @throws InputMismatchException on error
*/
public FloatBuffer getFloatBuffer() {
if ((m_data.limit() % 4) != 0) {
throw new InputMismatchException("data size is not a multiple of 4");
}
return m_data.asFloatBuffer();
}
/**
* Decodes a data record as a double array. Note if the data type (as indicated in the
* corresponding start control record for this entry) is not "double[]", invalid results may be
* returned.
*
* @return double array
* @throws InputMismatchException on error
*/
public double[] getDoubleArray() {
DoubleBuffer buf = getDoubleBuffer();
double[] arr = new double[buf.remaining()];
buf.get(arr);
return arr;
}
/**
* Decodes a data record as a double array. Note if the data type (as indicated in the
* corresponding start control record for this entry) is not "double[]", invalid results may be
* returned.
*
* @return double buffer
* @throws InputMismatchException on error
*/
public DoubleBuffer getDoubleBuffer() {
if ((m_data.limit() % 8) != 0) {
throw new InputMismatchException("data size is not a multiple of 8");
}
return m_data.asDoubleBuffer();
}
/**
* Decodes a data record as a string array. Note if the data type (as indicated in the
* corresponding start control record for this entry) is not "string[]", invalid results may be
* returned.
*
* @return string array
* @throws InputMismatchException on error
*/
public String[] getStringArray() {
ByteBuffer buf = getRawBuffer();
try {
int size = buf.getInt();
// sanity check size
if (size > (buf.remaining() / 4)) {
throw new InputMismatchException("invalid size");
}
String[] arr = new String[size];
for (int i = 0; i < size; i++) {
arr[i] = readInnerString(buf);
}
return arr;
} catch (BufferUnderflowException | IndexOutOfBoundsException ex) {
throw new InputMismatchException();
}
}
private String readInnerString(ByteBuffer buf) {
int size = buf.getInt();
if (size > buf.remaining()) {
throw new InputMismatchException("invalid string size");
}
byte[] arr = new byte[size];
buf.get(arr);
return new String(arr, StandardCharsets.UTF_8);
}
private final int m_entry;
private final long m_timestamp;
private final ByteBuffer m_data;
}

View File

@@ -0,0 +1,84 @@
// 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.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 {
/**
* Construct a new Data Log.
*
* @param filename filename to use
* @param extraHeader extra header data
* @throws IOException if file cannot be opened
*/
public DataLogWriter(String filename, String extraHeader) throws IOException {
super(DataLogJNI.fgCreate(filename, extraHeader));
m_os = null;
m_buf = null;
}
/**
* Construct a new Data Log.
*
* @param filename filename to use
* @throws IOException if file cannot be opened
*/
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;
}

View File

@@ -0,0 +1,151 @@
// 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.datalog;
import java.util.Arrays;
/** Log array of double values. */
public class DoubleArrayLogEntry extends DataLogEntry {
/** The data type for double array values. */
public static final String kDataType = "double[]";
/**
* Constructs a double array log entry.
*
* @param log datalog
* @param name name of the entry
* @param metadata metadata
* @param timestamp entry creation timestamp (0=now)
*/
public DoubleArrayLogEntry(DataLog log, String name, String metadata, long timestamp) {
super(log, name, kDataType, metadata, timestamp);
}
/**
* Constructs a double array log entry.
*
* @param log datalog
* @param name name of the entry
* @param metadata metadata
*/
public DoubleArrayLogEntry(DataLog log, String name, String metadata) {
this(log, name, metadata, 0);
}
/**
* Constructs a double array log entry.
*
* @param log datalog
* @param name name of the entry
* @param timestamp entry creation timestamp (0=now)
*/
public DoubleArrayLogEntry(DataLog log, String name, long timestamp) {
this(log, name, "", timestamp);
}
/**
* Constructs a double array log entry.
*
* @param log datalog
* @param name name of the entry
*/
public DoubleArrayLogEntry(DataLog log, String name) {
this(log, name, 0);
}
/**
* Appends a record to the log.
*
* @param value Value to record
* @param timestamp Time stamp (0 to indicate now)
*/
public void append(double[] value, long timestamp) {
m_log.appendDoubleArray(m_entry, value, timestamp);
}
/**
* Appends a record to the log.
*
* @param value Value to record
*/
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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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;
}

View File

@@ -0,0 +1,130 @@
// 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.datalog;
/** Log double values. */
public class DoubleLogEntry extends DataLogEntry {
/** The data type for double values. */
public static final String kDataType = "double";
/**
* Constructs a double log entry.
*
* @param log datalog
* @param name name of the entry
* @param metadata metadata
* @param timestamp entry creation timestamp (0=now)
*/
public DoubleLogEntry(DataLog log, String name, String metadata, long timestamp) {
super(log, name, kDataType, metadata, timestamp);
}
/**
* Constructs a double log entry.
*
* @param log datalog
* @param name name of the entry
* @param metadata metadata
*/
public DoubleLogEntry(DataLog log, String name, String metadata) {
this(log, name, metadata, 0);
}
/**
* Constructs a double log entry.
*
* @param log datalog
* @param name name of the entry
* @param timestamp entry creation timestamp (0=now)
*/
public DoubleLogEntry(DataLog log, String name, long timestamp) {
this(log, name, "", timestamp);
}
/**
* Constructs a double log entry.
*
* @param log datalog
* @param name name of the entry
*/
public DoubleLogEntry(DataLog log, String name) {
this(log, name, 0);
}
/**
* Appends a record to the log.
*
* @param value Value to record
* @param timestamp Time stamp (0 to indicate now)
*/
public void append(double value, long timestamp) {
m_log.appendDouble(m_entry, value, timestamp);
}
/**
* Appends a record to the log.
*
* @param value Value to record
*/
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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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;
}

View File

@@ -0,0 +1,30 @@
// 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.datalog;
/**
* A class version of `tail -f`, otherwise known as `tail -f` at home. Watches a file and puts the
* data into a data log. Only works on Linux-based platforms.
*/
public class FileLogger implements AutoCloseable {
private final long m_impl;
/**
* Construct a FileLogger. When the specified file is modified, appended data will be appended to
* the specified data log.
*
* @param file The path to the file.
* @param log A data log.
* @param key The log key to append data to.
*/
public FileLogger(String file, DataLog log, String key) {
m_impl = DataLogJNI.createFileLogger(file, log.getImpl(), key);
}
@Override
public void close() {
DataLogJNI.freeFileLogger(m_impl);
}
}

View File

@@ -0,0 +1,151 @@
// 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.datalog;
import java.util.Arrays;
/** Log array of float values. */
public class FloatArrayLogEntry extends DataLogEntry {
/** The data type for float array values. */
public static final String kDataType = "float[]";
/**
* Constructs a float array log entry.
*
* @param log datalog
* @param name name of the entry
* @param metadata metadata
* @param timestamp entry creation timestamp (0=now)
*/
public FloatArrayLogEntry(DataLog log, String name, String metadata, long timestamp) {
super(log, name, kDataType, metadata, timestamp);
}
/**
* Constructs a float array log entry.
*
* @param log datalog
* @param name name of the entry
* @param metadata metadata
*/
public FloatArrayLogEntry(DataLog log, String name, String metadata) {
this(log, name, metadata, 0);
}
/**
* Constructs a float array log entry.
*
* @param log datalog
* @param name name of the entry
* @param timestamp entry creation timestamp (0=now)
*/
public FloatArrayLogEntry(DataLog log, String name, long timestamp) {
this(log, name, "", timestamp);
}
/**
* Constructs a float array log entry.
*
* @param log datalog
* @param name name of the entry
*/
public FloatArrayLogEntry(DataLog log, String name) {
this(log, name, 0);
}
/**
* Appends a record to the log.
*
* @param value Value to record
* @param timestamp Time stamp (0 to indicate now)
*/
public void append(float[] value, long timestamp) {
m_log.appendFloatArray(m_entry, value, timestamp);
}
/**
* Appends a record to the log.
*
* @param value Value to record
*/
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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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;
}

View File

@@ -0,0 +1,130 @@
// 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.datalog;
/** Log float values. */
public class FloatLogEntry extends DataLogEntry {
/** The data type for float values. */
public static final String kDataType = "float";
/**
* Constructs a float log entry.
*
* @param log datalog
* @param name name of the entry
* @param metadata metadata
* @param timestamp entry creation timestamp (0=now)
*/
public FloatLogEntry(DataLog log, String name, String metadata, long timestamp) {
super(log, name, kDataType, metadata, timestamp);
}
/**
* Constructs a float log entry.
*
* @param log datalog
* @param name name of the entry
* @param metadata metadata
*/
public FloatLogEntry(DataLog log, String name, String metadata) {
this(log, name, metadata, 0);
}
/**
* Constructs a float log entry.
*
* @param log datalog
* @param name name of the entry
* @param timestamp entry creation timestamp (0=now)
*/
public FloatLogEntry(DataLog log, String name, long timestamp) {
this(log, name, "", timestamp);
}
/**
* Constructs a float log entry.
*
* @param log datalog
* @param name name of the entry
*/
public FloatLogEntry(DataLog log, String name) {
this(log, name, 0);
}
/**
* Appends a record to the log.
*
* @param value Value to record
* @param timestamp Time stamp (0 to indicate now)
*/
public void append(float value, long timestamp) {
m_log.appendFloat(m_entry, value, timestamp);
}
/**
* Appends a record to the log.
*
* @param value Value to record
*/
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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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;
}

View File

@@ -0,0 +1,151 @@
// 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.datalog;
import java.util.Arrays;
/** Log array of integer values. */
public class IntegerArrayLogEntry extends DataLogEntry {
/** The data type for integer array values. */
public static final String kDataType = "int64[]";
/**
* Constructs a integer array log entry.
*
* @param log datalog
* @param name name of the entry
* @param metadata metadata
* @param timestamp entry creation timestamp (0=now)
*/
public IntegerArrayLogEntry(DataLog log, String name, String metadata, long timestamp) {
super(log, name, kDataType, metadata, timestamp);
}
/**
* Constructs a integer array log entry.
*
* @param log datalog
* @param name name of the entry
* @param metadata metadata
*/
public IntegerArrayLogEntry(DataLog log, String name, String metadata) {
this(log, name, metadata, 0);
}
/**
* Constructs a integer array log entry.
*
* @param log datalog
* @param name name of the entry
* @param timestamp entry creation timestamp (0=now)
*/
public IntegerArrayLogEntry(DataLog log, String name, long timestamp) {
this(log, name, "", timestamp);
}
/**
* Constructs a integer array log entry.
*
* @param log datalog
* @param name name of the entry
*/
public IntegerArrayLogEntry(DataLog log, String name) {
this(log, name, 0);
}
/**
* Appends a record to the log.
*
* @param value Value to record
* @param timestamp Time stamp (0 to indicate now)
*/
public void append(long[] value, long timestamp) {
m_log.appendIntegerArray(m_entry, value, timestamp);
}
/**
* Appends a record to the log.
*
* @param value Value to record
*/
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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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;
}

View File

@@ -0,0 +1,130 @@
// 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.datalog;
/** Log integer values. */
public class IntegerLogEntry extends DataLogEntry {
/** The data type for integer values. */
public static final String kDataType = "int64";
/**
* Constructs a integer log entry.
*
* @param log datalog
* @param name name of the entry
* @param metadata metadata
* @param timestamp entry creation timestamp (0=now)
*/
public IntegerLogEntry(DataLog log, String name, String metadata, long timestamp) {
super(log, name, kDataType, metadata, timestamp);
}
/**
* Constructs a integer log entry.
*
* @param log datalog
* @param name name of the entry
* @param metadata metadata
*/
public IntegerLogEntry(DataLog log, String name, String metadata) {
this(log, name, metadata, 0);
}
/**
* Constructs a integer log entry.
*
* @param log datalog
* @param name name of the entry
* @param timestamp entry creation timestamp (0=now)
*/
public IntegerLogEntry(DataLog log, String name, long timestamp) {
this(log, name, "", timestamp);
}
/**
* Constructs a integer log entry.
*
* @param log datalog
* @param name name of the entry
*/
public IntegerLogEntry(DataLog log, String name) {
this(log, name, 0);
}
/**
* Appends a record to the log.
*
* @param value Value to record
* @param timestamp Time stamp (0 to indicate now)
*/
public void append(long value, long timestamp) {
m_log.appendInteger(m_entry, value, timestamp);
}
/**
* Appends a record to the log.
*
* @param value Value to record
*/
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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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;
}

View File

@@ -0,0 +1,240 @@
// 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.datalog;
import edu.wpi.first.util.protobuf.Protobuf;
import edu.wpi.first.util.protobuf.ProtobufBuffer;
import java.io.IOException;
import java.nio.ByteBuffer;
import us.hebi.quickbuf.ProtoMessage;
/**
* Log protobuf-encoded values.
*
* @param <T> value class
*/
public final class ProtobufLogEntry<T> extends DataLogEntry {
private ProtobufLogEntry(
DataLog log, String name, Protobuf<T, ?> 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);
}
/**
* Creates a protobuf-encoded log entry.
*
* @param <T> value class (inferred from proto)
* @param <MessageType> protobuf message type (inferred from proto)
* @param log datalog
* @param name name of the entry
* @param proto protobuf serialization implementation
* @param metadata metadata
* @param timestamp entry creation timestamp (0=now)
* @return ProtobufLogEntry
*/
public static <T, MessageType extends ProtoMessage<?>> ProtobufLogEntry<T> create(
DataLog log, String name, Protobuf<T, MessageType> proto, String metadata, long timestamp) {
return new ProtobufLogEntry<>(log, name, proto, metadata, timestamp);
}
/**
* Creates a protobuf-encoded log entry.
*
* @param <T> value class (inferred from proto)
* @param <MessageType> protobuf message type (inferred from proto)
* @param log datalog
* @param name name of the entry
* @param proto protobuf serialization implementation
* @param metadata metadata
* @return ProtobufLogEntry
*/
public static <T, MessageType extends ProtoMessage<?>> ProtobufLogEntry<T> create(
DataLog log, String name, Protobuf<T, MessageType> proto, String metadata) {
return create(log, name, proto, metadata, 0);
}
/**
* Creates a protobuf-encoded log entry.
*
* @param <T> value class (inferred from proto)
* @param <MessageType> protobuf message type (inferred from proto)
* @param log datalog
* @param name name of the entry
* @param proto protobuf serialization implementation
* @param timestamp entry creation timestamp (0=now)
* @return ProtobufLogEntry
*/
public static <T, MessageType extends ProtoMessage<?>> ProtobufLogEntry<T> create(
DataLog log, String name, Protobuf<T, MessageType> proto, long timestamp) {
return create(log, name, proto, "", timestamp);
}
/**
* Creates a protobuf-encoded log entry.
*
* @param <T> value class (inferred from proto)
* @param <MessageType> protobuf message type (inferred from proto)
* @param log datalog
* @param name name of the entry
* @param proto protobuf serialization implementation
* @return ProtobufLogEntry
*/
public static <T, MessageType extends ProtoMessage<?>> ProtobufLogEntry<T> create(
DataLog log, String name, Protobuf<T, MessageType> proto) {
return create(log, name, proto, 0);
}
/**
* Appends a record to the log.
*
* @param value Value to record
* @param timestamp Time stamp (0 to indicate now)
*/
public void append(T value, long timestamp) {
try {
synchronized (m_buf) {
ByteBuffer bb = m_buf.write(value);
m_log.appendRaw(m_entry, bb, 0, bb.position(), timestamp);
}
} catch (IOException e) {
// ignore
}
}
/**
* Appends a record to the log.
*
* @param value Value to record
*/
public void append(T value) {
append(value, 0);
}
/**
* Updates the last value and appends a record to the log if it has changed.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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<T, ?> m_buf;
private ByteBuffer m_lastValueBuf;
private final boolean m_immutable;
private final boolean m_cloneable;
private T m_lastValue;
}

View File

@@ -0,0 +1,367 @@
// 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.datalog;
import java.nio.ByteBuffer;
import java.util.Arrays;
/** Log raw byte array values. */
public class RawLogEntry extends DataLogEntry {
/** The data type for raw values. */
public static final String kDataType = "raw";
/**
* Constructs a raw log entry.
*
* @param log datalog
* @param name name of the entry
* @param metadata metadata
* @param type Data type
* @param timestamp entry creation timestamp (0=now)
*/
public RawLogEntry(DataLog log, String name, String metadata, String type, long timestamp) {
super(log, name, type, metadata, timestamp);
}
/**
* Constructs a raw log entry.
*
* @param log datalog
* @param name name of the entry
* @param metadata metadata
* @param type Data type
*/
public RawLogEntry(DataLog log, String name, String metadata, String type) {
this(log, name, metadata, type, 0);
}
/**
* Constructs a raw log entry.
*
* @param log datalog
* @param name name of the entry
* @param metadata metadata
* @param timestamp entry creation timestamp (0=now)
*/
public RawLogEntry(DataLog log, String name, String metadata, long timestamp) {
this(log, name, metadata, kDataType, timestamp);
}
/**
* Constructs a raw log entry.
*
* @param log datalog
* @param name name of the entry
* @param metadata metadata
*/
public RawLogEntry(DataLog log, String name, String metadata) {
this(log, name, metadata, 0);
}
/**
* Constructs a raw log entry.
*
* @param log datalog
* @param name name of the entry
* @param timestamp entry creation timestamp (0=now)
*/
public RawLogEntry(DataLog log, String name, long timestamp) {
this(log, name, "", timestamp);
}
/**
* Constructs a raw log entry.
*
* @param log datalog
* @param name name of the entry
*/
public RawLogEntry(DataLog log, String name) {
this(log, name, 0);
}
/**
* Appends a record to the log.
*
* @param value Value to record; will send entire array contents
* @param timestamp Time stamp (0 to indicate now)
*/
public void append(byte[] value, long timestamp) {
m_log.appendRaw(m_entry, value, timestamp);
}
/**
* Appends a record to the log.
*
* @param value Value to record; will send entire array contents
*/
public void append(byte[] value) {
append(value, 0);
}
/**
* Appends a record to the log.
*
* @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 void append(byte[] value, int start, int len, long timestamp) {
m_log.appendRaw(m_entry, value, start, len, timestamp);
}
/**
* Appends a record to the log.
*
* @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 append(byte[] value, int start, int len) {
append(value, start, len, 0);
}
/**
* Appends a record to the log.
*
* @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) {
m_log.appendRaw(m_entry, value, timestamp);
}
/**
* Appends a record to the log.
*
* @param value Data to record; will send from value.position() to value.limit()
*/
public void append(ByteBuffer value) {
append(value, 0);
}
/**
* Appends a record to the log.
*
* @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 void append(ByteBuffer value, int start, int len, long timestamp) {
m_log.appendRaw(m_entry, value, start, len, timestamp);
}
/**
* Appends a record to the log.
*
* @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 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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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;
}

View File

@@ -0,0 +1,151 @@
// 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.datalog;
import java.util.Arrays;
/** Log array of string values. */
public class StringArrayLogEntry extends DataLogEntry {
/** The data type for string array values. */
public static final String kDataType = "string[]";
/**
* Constructs a string array log entry.
*
* @param log datalog
* @param name name of the entry
* @param metadata metadata
* @param timestamp entry creation timestamp (0=now)
*/
public StringArrayLogEntry(DataLog log, String name, String metadata, long timestamp) {
super(log, name, kDataType, metadata, timestamp);
}
/**
* Constructs a string array log entry.
*
* @param log datalog
* @param name name of the entry
* @param metadata metadata
*/
public StringArrayLogEntry(DataLog log, String name, String metadata) {
this(log, name, metadata, 0);
}
/**
* Constructs a string array log entry.
*
* @param log datalog
* @param name name of the entry
* @param timestamp entry creation timestamp (0=now)
*/
public StringArrayLogEntry(DataLog log, String name, long timestamp) {
this(log, name, "", timestamp);
}
/**
* Constructs a string array log entry.
*
* @param log datalog
* @param name name of the entry
*/
public StringArrayLogEntry(DataLog log, String name) {
this(log, name, 0);
}
/**
* Appends a record to the log.
*
* @param value Value to record
* @param timestamp Time stamp (0 to indicate now)
*/
public void append(String[] value, long timestamp) {
m_log.appendStringArray(m_entry, value, timestamp);
}
/**
* Appends a record to the log.
*
* @param value Value to record
*/
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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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;
}

View File

@@ -0,0 +1,153 @@
// 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.datalog;
/** Log string values. */
public class StringLogEntry extends DataLogEntry {
/** The data type for string values. */
public static final String kDataType = "string";
/**
* Constructs a String log entry.
*
* @param log datalog
* @param name name of the entry
* @param metadata metadata
* @param type Data type
* @param timestamp entry creation timestamp (0=now)
*/
public StringLogEntry(DataLog log, String name, String metadata, String type, long timestamp) {
super(log, name, type, metadata, timestamp);
}
/**
* Constructs a String log entry.
*
* @param log datalog
* @param name name of the entry
* @param metadata metadata
* @param type Data type
*/
public StringLogEntry(DataLog log, String name, String metadata, String type) {
this(log, name, metadata, type, 0);
}
/**
* Constructs a String log entry.
*
* @param log datalog
* @param name name of the entry
* @param metadata metadata
* @param timestamp entry creation timestamp (0=now)
*/
public StringLogEntry(DataLog log, String name, String metadata, long timestamp) {
this(log, name, metadata, kDataType, timestamp);
}
/**
* Constructs a String log entry.
*
* @param log datalog
* @param name name of the entry
* @param metadata metadata
*/
public StringLogEntry(DataLog log, String name, String metadata) {
this(log, name, metadata, 0);
}
/**
* Constructs a String log entry.
*
* @param log datalog
* @param name name of the entry
* @param timestamp entry creation timestamp (0=now)
*/
public StringLogEntry(DataLog log, String name, long timestamp) {
this(log, name, "", timestamp);
}
/**
* Constructs a String log entry.
*
* @param log datalog
* @param name name of the entry
*/
public StringLogEntry(DataLog log, String name) {
this(log, name, 0);
}
/**
* Appends a record to the log.
*
* @param value Value to record
* @param timestamp Time stamp (0 to indicate now)
*/
public void append(String value, long timestamp) {
m_log.appendString(m_entry, value, timestamp);
}
/**
* Appends a record to the log.
*
* @param value Value to record
*/
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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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;
}

View File

@@ -0,0 +1,381 @@
// 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.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;
/**
* Log struct-encoded array values.
*
* @param <T> value class
*/
public final class StructArrayLogEntry<T> extends DataLogEntry {
private StructArrayLogEntry(
DataLog log, String name, Struct<T> 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);
}
/**
* Creates a struct-encoded array log entry.
*
* @param <T> value class (inferred from struct)
* @param log datalog
* @param name name of the entry
* @param struct struct serialization implementation
* @param metadata metadata
* @param timestamp entry creation timestamp (0=now)
* @return StructArrayLogEntry
*/
public static <T> StructArrayLogEntry<T> create(
DataLog log, String name, Struct<T> struct, String metadata, long timestamp) {
return new StructArrayLogEntry<>(log, name, struct, metadata, timestamp);
}
/**
* Creates a struct-encoded array log entry.
*
* @param <T> value class (inferred from struct)
* @param log datalog
* @param name name of the entry
* @param struct struct serialization implementation
* @param metadata metadata
* @return StructArrayLogEntry
*/
public static <T> StructArrayLogEntry<T> create(
DataLog log, String name, Struct<T> struct, String metadata) {
return create(log, name, struct, metadata, 0);
}
/**
* Creates a struct-encoded array log entry.
*
* @param <T> value class (inferred from struct)
* @param log datalog
* @param name name of the entry
* @param struct struct serialization implementation
* @param timestamp entry creation timestamp (0=now)
* @return StructArrayLogEntry
*/
public static <T> StructArrayLogEntry<T> create(
DataLog log, String name, Struct<T> struct, long timestamp) {
return create(log, name, struct, "", timestamp);
}
/**
* Creates a struct-encoded array log entry.
*
* @param <T> value class (inferred from struct)
* @param log datalog
* @param name name of the entry
* @param struct struct serialization implementation
* @return StructArrayLogEntry
*/
public static <T> StructArrayLogEntry<T> create(DataLog log, String name, Struct<T> struct) {
return create(log, name, struct, 0);
}
/**
* Ensures sufficient buffer space is available for the given number of elements.
*
* @param nelem number of elements
*/
public void reserve(int nelem) {
synchronized (m_buf) {
m_buf.reserve(nelem);
}
}
/**
* Appends a record to the log.
*
* @param value Value to record
* @param timestamp Time stamp (0 to indicate now)
*/
public void append(T[] value, long timestamp) {
synchronized (m_buf) {
ByteBuffer bb = m_buf.writeArray(value);
m_log.appendRaw(m_entry, bb, 0, bb.position(), timestamp);
}
}
/**
* Appends a record to the log.
*
* @param value Value to record
*/
public void append(T[] value) {
append(value, 0);
}
/**
* Appends a record to the log.
*
* @param value Value to record
* @param timestamp Time stamp (0 to indicate now)
*/
public void append(Collection<T> value, long timestamp) {
synchronized (m_buf) {
ByteBuffer bb = m_buf.writeArray(value);
m_log.appendRaw(m_entry, bb, 0, bb.position(), timestamp);
}
}
/**
* Appends a record to the log.
*
* @param value Value to record
*/
public void append(Collection<T> value) {
append(value, 0);
}
/**
* Updates the last value and appends a record to the log if it has changed.
*
* <p>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.
*
* <p>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.
*
* <p>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<T> 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.
*
* <p>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<T> value) {
update(value, 0);
}
/**
* Gets whether there is a last value.
*
* <p>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.
*
* <p>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<T> 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<T> 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<T> in) throws CloneNotSupportedException {
Struct<T> 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<T> 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<T> value) throws CloneNotSupportedException {
if (m_lastValue == null || m_lastValue.length < value.size()) {
if (m_immutable) {
Struct<T> 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<T> 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<T> m_buf;
private ByteBuffer m_lastValueBuf;
private final boolean m_immutable;
private final boolean m_cloneable;
private T[] m_lastValue;
private int m_lastValueLen;
}

View File

@@ -0,0 +1,221 @@
// 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.datalog;
import edu.wpi.first.util.struct.Struct;
import edu.wpi.first.util.struct.StructBuffer;
import java.nio.ByteBuffer;
/**
* Log struct-encoded values.
*
* @param <T> value class
*/
public final class StructLogEntry<T> extends DataLogEntry {
private StructLogEntry(
DataLog log, String name, Struct<T> 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);
}
/**
* Creates a struct-encoded log entry.
*
* @param <T> value class (inferred from struct)
* @param log datalog
* @param name name of the entry
* @param struct struct serialization implementation
* @param metadata metadata
* @param timestamp entry creation timestamp (0=now)
* @return StructLogEntry
*/
public static <T> StructLogEntry<T> create(
DataLog log, String name, Struct<T> struct, String metadata, long timestamp) {
return new StructLogEntry<>(log, name, struct, metadata, timestamp);
}
/**
* Creates a struct-encoded log entry.
*
* @param <T> value class (inferred from struct)
* @param log datalog
* @param name name of the entry
* @param struct struct serialization implementation
* @param metadata metadata
* @return StructLogEntry
*/
public static <T> StructLogEntry<T> create(
DataLog log, String name, Struct<T> struct, String metadata) {
return create(log, name, struct, metadata, 0);
}
/**
* Creates a struct-encoded log entry.
*
* @param <T> value class (inferred from struct)
* @param log datalog
* @param name name of the entry
* @param struct struct serialization implementation
* @param timestamp entry creation timestamp (0=now)
* @return StructLogEntry
*/
public static <T> StructLogEntry<T> create(
DataLog log, String name, Struct<T> struct, long timestamp) {
return create(log, name, struct, "", timestamp);
}
/**
* Creates a struct-encoded log entry.
*
* @param <T> value class (inferred from struct)
* @param log datalog
* @param name name of the entry
* @param struct struct serialization implementation
* @return StructLogEntry
*/
public static <T> StructLogEntry<T> create(DataLog log, String name, Struct<T> struct) {
return create(log, name, struct, 0);
}
/**
* Appends a record to the log.
*
* @param value Value to record
* @param timestamp Time stamp (0 to indicate now)
*/
public void append(T value, long timestamp) {
synchronized (m_buf) {
ByteBuffer bb = m_buf.write(value);
m_log.appendRaw(m_entry, bb, 0, bb.position(), timestamp);
}
}
/**
* Appends a record to the log.
*
* @param value Value to record
*/
public void append(T value) {
append(value, 0);
}
/**
* Updates the last value and appends a record to the log if it has changed.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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<T> m_buf;
private ByteBuffer m_lastValueBuf;
private final boolean m_immutable;
private final boolean m_cloneable;
private T m_lastValue;
}

View File

@@ -0,0 +1,862 @@
// 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.
#include <algorithm>
#include <bit>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <utility>
#include <vector>
#include <wpi/Endian.h>
#include <wpi/Logger.h>
#include <wpi/SmallString.h>
#include <wpi/print.h>
#include <wpi/timestamp.h>
#include "wpi/datalog/DataLog.h"
using namespace wpi::log;
static constexpr size_t kRecordMaxHeaderSize = 17;
static void DefaultLog(unsigned int level, const char* file, unsigned int line,
const char* msg) {
if (level > wpi::WPI_LOG_INFO) {
wpi::print(stderr, "DataLog: {}\n", msg);
} else if (level == wpi::WPI_LOG_INFO) {
wpi::print("DataLog: {}\n", msg);
}
}
wpi::Logger DataLog::s_defaultMessageLog{DefaultLog};
template <typename T>
static unsigned int WriteVarInt(uint8_t* buf, T val) {
unsigned int len = 0;
do {
*buf++ = static_cast<unsigned int>(val) & 0xff;
++len;
val >>= 8;
} while (val != 0);
return len;
}
// min size: 4, max size: 17
static unsigned int WriteRecordHeader(uint8_t* buf, uint32_t entry,
uint64_t timestamp,
uint32_t payloadSize) {
uint8_t* origbuf = buf++;
unsigned int entryLen = WriteVarInt(buf, entry);
buf += entryLen;
unsigned int payloadLen = WriteVarInt(buf, payloadSize);
buf += payloadLen;
unsigned int timestampLen =
WriteVarInt(buf, timestamp == 0 ? wpi::Now() : timestamp);
buf += timestampLen;
*origbuf =
((timestampLen - 1) << 4) | ((payloadLen - 1) << 2) | (entryLen - 1);
return buf - origbuf;
}
void DataLog::StartFile() {
std::scoped_lock lock{m_mutex};
if (m_active) {
return;
}
// Grab previously pending writes
std::vector<Buffer> bufs;
bufs.swap(m_outgoing);
m_outgoing.reserve(bufs.size() + 1);
// File header (version 1.0)
uint8_t* buf = Reserve(m_extraHeader.size() + 12);
static const uint8_t header[] = {'W', 'P', 'I', 'L', 'O', 'G', 0, 1};
std::memcpy(buf, header, 8);
support::endian::write32le(buf + 8, m_extraHeader.size());
std::memcpy(buf + 12, m_extraHeader.data(), m_extraHeader.size());
// Existing start and schema data records
for (auto&& entryInfo : m_entries) {
AppendStartRecord(entryInfo.second.id, entryInfo.first,
entryInfo.second.type,
m_entryIds[entryInfo.second.id].metadata, 0);
}
// Existing schema data records
for (auto&& schemaInfo : m_schemas) {
if (schemaInfo.second.id != 0) {
StartRecord(schemaInfo.second.id, 0, schemaInfo.second.data.size(), 0);
AppendImpl(schemaInfo.second.data);
}
}
// Append previously pending writes
for (auto&& buf : bufs) {
m_outgoing.emplace_back(std::move(buf));
}
m_active = true;
}
void DataLog::FlushBufs(std::vector<Buffer>* writeBufs) {
std::scoped_lock lock{m_mutex};
writeBufs->swap(m_outgoing);
DoReleaseBufs(&m_outgoing);
}
void DataLog::ReleaseBufs(std::vector<Buffer>* bufs) {
std::scoped_lock lock{m_mutex};
DoReleaseBufs(bufs);
}
void DataLog::Pause() {
std::scoped_lock lock{m_mutex};
m_paused = true;
}
void DataLog::Resume() {
std::scoped_lock lock{m_mutex};
m_paused = false;
}
void DataLog::Stop() {
std::scoped_lock lock{m_mutex};
m_active = false;
}
void DataLog::BufferHalfFull() {}
bool DataLog::HasSchema(std::string_view name) const {
std::scoped_lock lock{m_mutex};
return m_schemas.contains(name);
}
void DataLog::AddSchema(std::string_view name, std::string_view type,
std::span<const uint8_t> schema, int64_t timestamp) {
std::scoped_lock lock{m_mutex};
auto& schemaInfo = m_schemas[name];
if (schemaInfo.id != 0) {
return; // don't add duplicates
}
schemaInfo.data.assign(schema.begin(), schema.end());
wpi::SmallString<128> fullName{"/.schema/"};
fullName += name;
int entry = StartImpl(fullName, type, {}, timestamp);
// inline AppendRaw() without releasing lock
if (entry <= 0) {
[[unlikely]] return; // should never happen, but check anyway
}
schemaInfo.id = entry;
if (!m_active) {
[[unlikely]] return;
}
StartRecord(entry, timestamp, schema.size(), 0);
AppendImpl(schema);
}
// Control records use the following format:
// 1-byte type
// 4-byte entry
// rest of data (depending on type)
int DataLog::Start(std::string_view name, std::string_view type,
std::string_view metadata, int64_t timestamp) {
std::scoped_lock lock{m_mutex};
return StartImpl(name, type, metadata, timestamp);
}
int DataLog::StartImpl(std::string_view name, std::string_view type,
std::string_view metadata, int64_t timestamp) {
auto& entryInfo = m_entries[name];
if (entryInfo.id == 0) {
entryInfo.id = ++m_lastId;
}
auto& entryInfo2 = m_entryIds[entryInfo.id];
++entryInfo2.count;
if (entryInfo2.count > 1) {
if (entryInfo.type != type) {
WPI_ERROR(m_msglog,
"type mismatch for '{}': was '{}', requested '{}'; ignoring",
name, entryInfo.type, type);
return 0;
}
return entryInfo.id;
}
entryInfo.type = type;
entryInfo2.metadata = metadata;
if (!m_active) {
[[unlikely]] return entryInfo.id;
}
AppendStartRecord(entryInfo.id, name, type, metadata, timestamp);
return entryInfo.id;
}
void DataLog::AppendStartRecord(int id, std::string_view name,
std::string_view type,
std::string_view metadata, int64_t timestamp) {
size_t strsize = name.size() + type.size() + metadata.size();
uint8_t* buf = StartRecord(0, timestamp, 5 + 12 + strsize, 5);
*buf++ = impl::kControlStart;
wpi::support::endian::write32le(buf, id);
AppendStringImpl(name);
AppendStringImpl(type);
AppendStringImpl(metadata);
}
void DataLog::DoReleaseBufs(std::vector<Buffer>* bufs) {
for (auto&& buf : *bufs) {
buf.Clear();
if (m_free.size() < kMaxFreeCount) {
[[likely]] m_free.emplace_back(std::move(buf));
}
}
bufs->resize(0);
}
void DataLog::Finish(int entry, int64_t timestamp) {
if (entry <= 0) {
return;
}
std::scoped_lock lock{m_mutex};
auto& entryInfo2 = m_entryIds[entry];
if (entryInfo2.count == 0) {
return;
}
--entryInfo2.count;
if (entryInfo2.count != 0) {
return;
}
m_entryIds.erase(entry);
if (!m_active) {
[[unlikely]] return;
}
uint8_t* buf = StartRecord(0, timestamp, 5, 5);
*buf++ = impl::kControlFinish;
wpi::support::endian::write32le(buf, entry);
}
void DataLog::SetMetadata(int entry, std::string_view metadata,
int64_t timestamp) {
if (entry <= 0) {
return;
}
std::scoped_lock lock{m_mutex};
m_entryIds[entry].metadata = metadata;
if (!m_active) {
[[unlikely]] return;
}
uint8_t* buf = StartRecord(0, timestamp, 5 + 4 + metadata.size(), 5);
*buf++ = impl::kControlSetMetadata;
wpi::support::endian::write32le(buf, entry);
AppendStringImpl(metadata);
}
uint8_t* DataLog::Reserve(size_t size) {
assert(size <= kBlockSize);
if (m_outgoing.empty() || size > m_outgoing.back().GetRemaining()) {
if (m_outgoing.size() == kMaxBufferCount / 2) {
[[unlikely]] BufferHalfFull();
}
if (m_free.empty()) {
if (m_outgoing.size() >= kMaxBufferCount) {
[[unlikely]]
if (BufferFull()) {
m_paused = true;
}
}
m_outgoing.emplace_back();
} else {
m_outgoing.emplace_back(std::move(m_free.back()));
m_free.pop_back();
}
}
return m_outgoing.back().Reserve(size);
}
uint8_t* DataLog::StartRecord(uint32_t entry, uint64_t timestamp,
uint32_t payloadSize, size_t reserveSize) {
uint8_t* buf = Reserve(kRecordMaxHeaderSize + reserveSize);
auto headerLen = WriteRecordHeader(buf, entry, timestamp, payloadSize);
m_outgoing.back().Unreserve(kRecordMaxHeaderSize - headerLen);
buf += headerLen;
return buf;
}
void DataLog::AppendImpl(std::span<const uint8_t> data) {
while (data.size() > kBlockSize) {
uint8_t* buf = Reserve(kBlockSize);
std::memcpy(buf, data.data(), kBlockSize);
data = data.subspan(kBlockSize);
}
if (!data.empty()) {
uint8_t* buf = Reserve(data.size());
std::memcpy(buf, data.data(), data.size());
}
}
void DataLog::AppendStringImpl(std::string_view str) {
uint8_t* buf = Reserve(4);
wpi::support::endian::write32le(buf, str.size());
AppendImpl({reinterpret_cast<const uint8_t*>(str.data()), str.size()});
}
void DataLog::AppendRaw(int entry, std::span<const uint8_t> data,
int64_t timestamp) {
if (entry <= 0) {
return;
}
std::scoped_lock lock{m_mutex};
if (m_paused) {
[[unlikely]] return;
}
StartRecord(entry, timestamp, data.size(), 0);
AppendImpl(data);
}
void DataLog::AppendRaw2(int entry,
std::span<const std::span<const uint8_t>> data,
int64_t timestamp) {
if (entry <= 0) {
return;
}
std::scoped_lock lock{m_mutex};
if (m_paused) {
[[unlikely]] return;
}
size_t size = 0;
for (auto&& chunk : data) {
size += chunk.size();
}
StartRecord(entry, timestamp, size, 0);
for (auto chunk : data) {
AppendImpl(chunk);
}
}
void DataLog::AppendBoolean(int entry, bool value, int64_t timestamp) {
if (entry <= 0) {
return;
}
std::scoped_lock lock{m_mutex};
if (m_paused) {
[[unlikely]] return;
}
uint8_t* buf = StartRecord(entry, timestamp, 1, 1);
buf[0] = value ? 1 : 0;
}
void DataLog::AppendInteger(int entry, int64_t value, int64_t timestamp) {
if (entry <= 0) {
return;
}
std::scoped_lock lock{m_mutex};
if (m_paused) {
[[unlikely]] return;
}
uint8_t* buf = StartRecord(entry, timestamp, 8, 8);
wpi::support::endian::write64le(buf, value);
}
void DataLog::AppendFloat(int entry, float value, int64_t timestamp) {
if (entry <= 0) {
return;
}
std::scoped_lock lock{m_mutex};
if (m_paused) {
[[unlikely]] return;
}
uint8_t* buf = StartRecord(entry, timestamp, 4, 4);
if constexpr (std::endian::native == std::endian::little) {
std::memcpy(buf, &value, 4);
} else {
wpi::support::endian::write32le(buf, std::bit_cast<uint32_t>(value));
}
}
void DataLog::AppendDouble(int entry, double value, int64_t timestamp) {
if (entry <= 0) {
return;
}
std::scoped_lock lock{m_mutex};
if (m_paused) {
[[unlikely]] return;
}
uint8_t* buf = StartRecord(entry, timestamp, 8, 8);
if constexpr (std::endian::native == std::endian::little) {
std::memcpy(buf, &value, 8);
} else {
wpi::support::endian::write64le(buf, std::bit_cast<uint64_t>(value));
}
}
void DataLog::AppendString(int entry, std::string_view value,
int64_t timestamp) {
AppendRaw(entry,
{reinterpret_cast<const uint8_t*>(value.data()), value.size()},
timestamp);
}
void DataLog::AppendBooleanArray(int entry, std::span<const bool> arr,
int64_t timestamp) {
if (entry <= 0) {
return;
}
std::scoped_lock lock{m_mutex};
if (m_paused) {
[[unlikely]] return;
}
StartRecord(entry, timestamp, arr.size(), 0);
uint8_t* buf;
while (arr.size() > kBlockSize) {
buf = Reserve(kBlockSize);
for (auto val : arr.subspan(0, kBlockSize)) {
*buf++ = val ? 1 : 0;
}
arr = arr.subspan(kBlockSize);
}
buf = Reserve(arr.size());
for (auto val : arr) {
*buf++ = val ? 1 : 0;
}
}
void DataLog::AppendBooleanArray(int entry, std::span<const int> arr,
int64_t timestamp) {
if (entry <= 0) {
return;
}
std::scoped_lock lock{m_mutex};
if (m_paused) {
[[unlikely]] return;
}
StartRecord(entry, timestamp, arr.size(), 0);
uint8_t* buf;
while (arr.size() > kBlockSize) {
buf = Reserve(kBlockSize);
for (auto val : arr.subspan(0, kBlockSize)) {
*buf++ = val & 1;
}
arr = arr.subspan(kBlockSize);
}
buf = Reserve(arr.size());
for (auto val : arr) {
*buf++ = val & 1;
}
}
void DataLog::AppendBooleanArray(int entry, std::span<const uint8_t> arr,
int64_t timestamp) {
AppendRaw(entry, arr, timestamp);
}
void DataLog::AppendIntegerArray(int entry, std::span<const int64_t> arr,
int64_t timestamp) {
if constexpr (std::endian::native == std::endian::little) {
AppendRaw(entry,
{reinterpret_cast<const uint8_t*>(arr.data()), arr.size() * 8},
timestamp);
} else {
if (entry <= 0) {
return;
}
std::scoped_lock lock{m_mutex};
if (m_paused) {
[[unlikely]] return;
}
StartRecord(entry, timestamp, arr.size() * 8, 0);
uint8_t* buf;
while ((arr.size() * 8) > kBlockSize) {
buf = Reserve(kBlockSize);
for (auto val : arr.subspan(0, kBlockSize / 8)) {
wpi::support::endian::write64le(buf, val);
buf += 8;
}
arr = arr.subspan(kBlockSize / 8);
}
buf = Reserve(arr.size() * 8);
for (auto val : arr) {
wpi::support::endian::write64le(buf, val);
buf += 8;
}
}
}
void DataLog::AppendFloatArray(int entry, std::span<const float> arr,
int64_t timestamp) {
if constexpr (std::endian::native == std::endian::little) {
AppendRaw(entry,
{reinterpret_cast<const uint8_t*>(arr.data()), arr.size() * 4},
timestamp);
} else {
if (entry <= 0) {
return;
}
std::scoped_lock lock{m_mutex};
if (m_paused) {
[[unlikely]] return;
}
StartRecord(entry, timestamp, arr.size() * 4, 0);
uint8_t* buf;
while ((arr.size() * 4) > kBlockSize) {
buf = Reserve(kBlockSize);
for (auto val : arr.subspan(0, kBlockSize / 4)) {
wpi::support::endian::write32le(buf, std::bit_cast<uint32_t>(val));
buf += 4;
}
arr = arr.subspan(kBlockSize / 4);
}
buf = Reserve(arr.size() * 4);
for (auto val : arr) {
wpi::support::endian::write32le(buf, std::bit_cast<uint32_t>(val));
buf += 4;
}
}
}
void DataLog::AppendDoubleArray(int entry, std::span<const double> arr,
int64_t timestamp) {
if constexpr (std::endian::native == std::endian::little) {
AppendRaw(entry,
{reinterpret_cast<const uint8_t*>(arr.data()), arr.size() * 8},
timestamp);
} else {
if (entry <= 0) {
return;
}
std::scoped_lock lock{m_mutex};
if (m_paused) {
[[unlikely]] return;
}
StartRecord(entry, timestamp, arr.size() * 8, 0);
uint8_t* buf;
while ((arr.size() * 8) > kBlockSize) {
buf = Reserve(kBlockSize);
for (auto val : arr.subspan(0, kBlockSize / 8)) {
wpi::support::endian::write64le(buf, std::bit_cast<uint64_t>(val));
buf += 8;
}
arr = arr.subspan(kBlockSize / 8);
}
buf = Reserve(arr.size() * 8);
for (auto val : arr) {
wpi::support::endian::write64le(buf, std::bit_cast<uint64_t>(val));
buf += 8;
}
}
}
void DataLog::AppendStringArray(int entry, std::span<const std::string> arr,
int64_t timestamp) {
if (entry <= 0) {
return;
}
// storage: 4-byte array length, each string prefixed by 4-byte length
// calculate total size
size_t size = 4;
for (auto&& str : arr) {
size += 4 + str.size();
}
std::scoped_lock lock{m_mutex};
if (m_paused) {
[[unlikely]] return;
}
uint8_t* buf = StartRecord(entry, timestamp, size, 4);
wpi::support::endian::write32le(buf, arr.size());
for (auto&& str : arr) {
AppendStringImpl(str);
}
}
void DataLog::AppendStringArray(int entry,
std::span<const std::string_view> arr,
int64_t timestamp) {
if (entry <= 0) {
return;
}
// storage: 4-byte array length, each string prefixed by 4-byte length
// calculate total size
size_t size = 4;
for (auto&& str : arr) {
size += 4 + str.size();
}
std::scoped_lock lock{m_mutex};
if (m_paused) {
[[unlikely]] return;
}
uint8_t* buf = StartRecord(entry, timestamp, size, 4);
wpi::support::endian::write32le(buf, arr.size());
for (auto&& sv : arr) {
AppendStringImpl(sv);
}
}
void DataLog::AppendStringArray(int entry,
std::span<const struct WPI_String> arr,
int64_t timestamp) {
if (entry <= 0) {
return;
}
// storage: 4-byte array length, each string prefixed by 4-byte length
// calculate total size
size_t size = 4;
for (auto&& str : arr) {
size += 4 + str.len;
}
std::scoped_lock lock{m_mutex};
if (m_paused) {
[[unlikely]] return;
}
uint8_t* buf = StartRecord(entry, timestamp, size, 4);
wpi::support::endian::write32le(buf, arr.size());
for (auto&& sv : arr) {
AppendStringImpl(sv.str);
}
}
template <typename V1, typename V2>
inline bool UpdateImpl(std::optional<std::vector<V1>>& lastValue,
std::span<const V2> 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<V1>{data.begin(), data.end()};
}
return true;
}
return false;
}
template <typename V1>
inline bool UpdateImpl(std::optional<std::vector<V1>>& lastValue,
std::span<const bool> data) {
if (!lastValue || !std::equal(data.begin(), data.end(), lastValue->begin(),
lastValue->end(), [](auto a, auto b) {
return a == static_cast<bool>(b);
})) {
if (lastValue) {
lastValue->assign(data.begin(), data.end());
} else {
lastValue = std::vector<V1>{data.begin(), data.end()};
}
return true;
}
return false;
}
void RawLogEntry::Update(std::span<const uint8_t> data, int64_t timestamp) {
std::scoped_lock lock{m_mutex};
if (UpdateImpl(m_lastValue, data)) {
Append(data, timestamp);
}
}
void BooleanArrayLogEntry::Update(std::span<const bool> arr,
int64_t timestamp) {
std::scoped_lock lock{m_mutex};
if (UpdateImpl(m_lastValue, arr)) {
Append(arr, timestamp);
}
}
void BooleanArrayLogEntry::Update(std::span<const int> arr, int64_t timestamp) {
std::scoped_lock lock{m_mutex};
if (UpdateImpl(m_lastValue, arr)) {
Append(arr, timestamp);
}
}
void BooleanArrayLogEntry::Update(std::span<const uint8_t> arr,
int64_t timestamp) {
std::scoped_lock lock{m_mutex};
if (UpdateImpl(m_lastValue, arr)) {
Append(arr, timestamp);
}
}
void IntegerArrayLogEntry::Update(std::span<const int64_t> arr,
int64_t timestamp) {
std::scoped_lock lock{m_mutex};
if (UpdateImpl(m_lastValue, arr)) {
Append(arr, timestamp);
}
}
void FloatArrayLogEntry::Update(std::span<const float> arr, int64_t timestamp) {
std::scoped_lock lock{m_mutex};
if (UpdateImpl(m_lastValue, arr)) {
Append(arr, timestamp);
}
}
void DoubleArrayLogEntry::Update(std::span<const double> arr,
int64_t timestamp) {
std::scoped_lock lock{m_mutex};
if (UpdateImpl(m_lastValue, arr)) {
Append(arr, timestamp);
}
}
void StringArrayLogEntry::Update(std::span<const std::string> arr,
int64_t timestamp) {
std::scoped_lock lock{m_mutex};
if (UpdateImpl(m_lastValue, arr)) {
Append(arr, timestamp);
}
}
void StringArrayLogEntry::Update(std::span<const std::string_view> 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) {
delete reinterpret_cast<DataLog*>(datalog);
}
void WPI_DataLog_Flush(struct WPI_DataLog* datalog) {
reinterpret_cast<DataLog*>(datalog)->Flush();
}
void WPI_DataLog_Pause(struct WPI_DataLog* datalog) {
reinterpret_cast<DataLog*>(datalog)->Pause();
}
void WPI_DataLog_Resume(struct WPI_DataLog* datalog) {
reinterpret_cast<DataLog*>(datalog)->Resume();
}
void WPI_DataLog_Stop(struct WPI_DataLog* datalog) {
reinterpret_cast<DataLog*>(datalog)->Stop();
}
int WPI_DataLog_Start(struct WPI_DataLog* datalog,
const struct WPI_String* name,
const struct WPI_String* type,
const struct WPI_String* metadata, int64_t timestamp) {
return reinterpret_cast<DataLog*>(datalog)->Start(
wpi::to_string_view(name), wpi::to_string_view(type),
wpi::to_string_view(metadata), timestamp);
}
void WPI_DataLog_Finish(struct WPI_DataLog* datalog, int entry,
int64_t timestamp) {
reinterpret_cast<DataLog*>(datalog)->Finish(entry, timestamp);
}
void WPI_DataLog_SetMetadata(struct WPI_DataLog* datalog, int entry,
const struct WPI_String* metadata,
int64_t timestamp) {
reinterpret_cast<DataLog*>(datalog)->SetMetadata(
entry, wpi::to_string_view(metadata), timestamp);
}
void WPI_DataLog_AppendRaw(struct WPI_DataLog* datalog, int entry,
const uint8_t* data, size_t len, int64_t timestamp) {
reinterpret_cast<DataLog*>(datalog)->AppendRaw(entry, {data, len}, timestamp);
}
void WPI_DataLog_AppendBoolean(struct WPI_DataLog* datalog, int entry,
int value, int64_t timestamp) {
reinterpret_cast<DataLog*>(datalog)->AppendBoolean(entry, value, timestamp);
}
void WPI_DataLog_AppendInteger(struct WPI_DataLog* datalog, int entry,
int64_t value, int64_t timestamp) {
reinterpret_cast<DataLog*>(datalog)->AppendInteger(entry, value, timestamp);
}
void WPI_DataLog_AppendFloat(struct WPI_DataLog* datalog, int entry,
float value, int64_t timestamp) {
reinterpret_cast<DataLog*>(datalog)->AppendFloat(entry, value, timestamp);
}
void WPI_DataLog_AppendDouble(struct WPI_DataLog* datalog, int entry,
double value, int64_t timestamp) {
reinterpret_cast<DataLog*>(datalog)->AppendDouble(entry, value, timestamp);
}
void WPI_DataLog_AppendString(struct WPI_DataLog* datalog, int entry,
const struct WPI_String* value,
int64_t timestamp) {
reinterpret_cast<DataLog*>(datalog)->AppendString(
entry, {value->str, value->len}, timestamp);
}
void WPI_DataLog_AppendBooleanArray(struct WPI_DataLog* datalog, int entry,
const int* arr, size_t len,
int64_t timestamp) {
reinterpret_cast<DataLog*>(datalog)->AppendBooleanArray(entry, {arr, len},
timestamp);
}
void WPI_DataLog_AppendBooleanArrayByte(struct WPI_DataLog* datalog, int entry,
const uint8_t* arr, size_t len,
int64_t timestamp) {
reinterpret_cast<DataLog*>(datalog)->AppendBooleanArray(entry, {arr, len},
timestamp);
}
void WPI_DataLog_AppendIntegerArray(struct WPI_DataLog* datalog, int entry,
const int64_t* arr, size_t len,
int64_t timestamp) {
reinterpret_cast<DataLog*>(datalog)->AppendIntegerArray(entry, {arr, len},
timestamp);
}
void WPI_DataLog_AppendFloatArray(struct WPI_DataLog* datalog, int entry,
const float* arr, size_t len,
int64_t timestamp) {
reinterpret_cast<DataLog*>(datalog)->AppendFloatArray(entry, {arr, len},
timestamp);
}
void WPI_DataLog_AppendDoubleArray(struct WPI_DataLog* datalog, int entry,
const double* arr, size_t len,
int64_t timestamp) {
reinterpret_cast<DataLog*>(datalog)->AppendDoubleArray(entry, {arr, len},
timestamp);
}
void WPI_DataLog_AppendStringArray(struct WPI_DataLog* datalog, int entry,
const struct WPI_String* arr, size_t len,
int64_t timestamp) {
reinterpret_cast<DataLog*>(datalog)->AppendStringArray(entry, {arr, len},
timestamp);
}
void WPI_DataLog_AddSchemaString(struct WPI_DataLog* datalog,
const struct WPI_String* name,
const struct WPI_String* type,
const struct WPI_String* schema,
int64_t timestamp) {
reinterpret_cast<DataLog*>(datalog)->AddSchema(
wpi::to_string_view(name), wpi::to_string_view(type),
wpi::to_string_view(schema), timestamp);
}
void WPI_DataLog_AddSchema(struct WPI_DataLog* datalog,
const struct WPI_String* name,
const struct WPI_String* type, const uint8_t* schema,
size_t schema_len, int64_t timestamp) {
reinterpret_cast<DataLog*>(datalog)->AddSchema(
wpi::to_string_view(name), wpi::to_string_view(type),
std::span<const uint8_t>{schema, schema_len}, timestamp);
}
} // extern "C"

View File

@@ -0,0 +1,490 @@
// 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.
#include "wpi/datalog/DataLogBackgroundWriter.h"
#ifndef _WIN32
#include <unistd.h>
#endif
#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h> // NOLINT(build/include_order)
#endif
#include <random>
#include <string>
#include <utility>
#include <vector>
#include <fmt/format.h>
#include "wpi/Logger.h"
#include "wpi/fs.h"
using namespace wpi::log;
static constexpr uintmax_t kMinFreeSpace = 5 * 1024 * 1024;
static std::string FormatBytesSize(uintmax_t value) {
static constexpr uintmax_t kKiB = 1024;
static constexpr uintmax_t kMiB = kKiB * 1024;
static constexpr uintmax_t kGiB = kMiB * 1024;
if (value >= kGiB) {
return fmt::format("{:.1f} GiB", static_cast<double>(value) / kGiB);
} else if (value >= kMiB) {
return fmt::format("{:.1f} MiB", static_cast<double>(value) / kMiB);
} else if (value >= kKiB) {
return fmt::format("{:.1f} KiB", static_cast<double>(value) / kKiB);
} else {
return fmt::format("{} B", value);
}
}
DataLogBackgroundWriter::DataLogBackgroundWriter(std::string_view dir,
std::string_view filename,
double period,
std::string_view extraHeader)
: DataLogBackgroundWriter{s_defaultMessageLog, dir, filename, period,
extraHeader} {}
DataLogBackgroundWriter::DataLogBackgroundWriter(wpi::Logger& msglog,
std::string_view dir,
std::string_view filename,
double period,
std::string_view extraHeader)
: DataLog{msglog, extraHeader},
m_period{period},
m_newFilename{filename},
m_thread{[this, dir = std::string{dir}] { WriterThreadMain(dir); }} {}
DataLogBackgroundWriter::DataLogBackgroundWriter(
std::function<void(std::span<const uint8_t> data)> write, double period,
std::string_view extraHeader)
: DataLogBackgroundWriter{s_defaultMessageLog, std::move(write), period,
extraHeader} {}
DataLogBackgroundWriter::DataLogBackgroundWriter(
wpi::Logger& msglog,
std::function<void(std::span<const uint8_t> data)> write, double period,
std::string_view extraHeader)
: DataLog{msglog, extraHeader},
m_period{period},
m_thread{[this, write = std::move(write)] {
WriterThreadMain(std::move(write));
}} {}
DataLogBackgroundWriter::~DataLogBackgroundWriter() {
{
std::scoped_lock lock{m_mutex};
m_shutdown = true;
m_doFlush = true;
}
m_cond.notify_all();
m_thread.join();
}
void DataLogBackgroundWriter::SetFilename(std::string_view filename) {
{
std::scoped_lock lock{m_mutex};
m_newFilename = filename;
}
m_cond.notify_all();
}
void DataLogBackgroundWriter::Flush() {
{
std::scoped_lock lock{m_mutex};
m_doFlush = true;
}
m_cond.notify_all();
}
void DataLogBackgroundWriter::Pause() {
DataLog::Pause();
std::scoped_lock lock{m_mutex};
m_state = kPaused;
}
void DataLogBackgroundWriter::Resume() {
DataLog::Resume();
std::scoped_lock lock{m_mutex};
if (m_state == kPaused) {
m_state = kActive;
} else if (m_state == kStopped) {
m_state = kStart;
}
}
void DataLogBackgroundWriter::Stop() {
DataLog::Stop();
{
std::scoped_lock lock{m_mutex};
m_state = kStopped;
m_newFilename.clear();
}
m_cond.notify_all();
}
static void WriteToFile(fs::file_t f, std::span<const uint8_t> data,
std::string_view filename, wpi::Logger& msglog) {
do {
#ifdef _WIN32
DWORD ret;
if (!WriteFile(f, data.data(), data.size(), &ret, nullptr)) {
WPI_ERROR(msglog, "Error writing to log file '{}': {}", filename,
GetLastError());
break;
}
#else
ssize_t ret = ::write(f, data.data(), data.size());
if (ret < 0) {
// If it's a recoverable error, swallow it and retry the write
if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) {
continue;
}
// Otherwise it's a non-recoverable error; quit trying
WPI_ERROR(msglog, "Error writing to log file '{}': {}", filename,
std::strerror(errno));
break;
}
#endif
// The write may have written some or all of the data
data = data.subspan(ret);
} while (data.size() > 0);
}
static std::string MakeRandomFilename() {
// build random filename
static std::random_device dev;
static std::mt19937 rng(dev());
std::uniform_int_distribution<int> dist(0, 15);
const char* v = "0123456789abcdef";
std::string filename = "wpilog_";
for (int i = 0; i < 16; i++) {
filename += v[dist(rng)];
}
filename += ".wpilog";
return filename;
}
struct DataLogBackgroundWriter::WriterThreadState {
explicit WriterThreadState(std::string_view dir)
: dirPath{dir.empty() ? "." : dir} {}
WriterThreadState(const WriterThreadState&) = delete;
WriterThreadState& operator=(const WriterThreadState&) = delete;
~WriterThreadState() { Close(); }
void Close() {
if (f != WPI_kInvalidFile) {
fs::CloseFile(f);
f = WPI_kInvalidFile;
}
}
void SetFilename(std::string_view fn) {
baseFilename = fn;
filename = fn;
path = dirPath / filename;
segmentCount = 1;
}
void IncrementFilename() {
fs::path basePath{baseFilename};
filename = fmt::format("{}.{}{}", basePath.stem().string(), ++segmentCount,
basePath.extension().string());
path = dirPath / filename;
}
fs::path dirPath;
std::string baseFilename;
std::string filename;
fs::path path;
fs::file_t f = WPI_kInvalidFile;
uintmax_t freeSpace = UINTMAX_MAX;
int segmentCount = 1;
};
void DataLogBackgroundWriter::BufferHalfFull() {
Flush();
}
bool DataLogBackgroundWriter::BufferFull() {
WPI_ERROR(m_msglog,
"outgoing buffers exceeded threshold, pausing logging--"
"consider flushing to disk more frequently (smaller period)");
return true;
}
void DataLogBackgroundWriter::StartLogFile(WriterThreadState& state) {
std::error_code ec;
if (state.filename.empty()) {
state.SetFilename(MakeRandomFilename());
}
// get free space
auto freeSpaceInfo = fs::space(state.dirPath, ec);
if (!ec) {
state.freeSpace = freeSpaceInfo.available;
} else {
state.freeSpace = UINTMAX_MAX;
}
if (state.freeSpace < kMinFreeSpace) {
WPI_ERROR(m_msglog,
"Insufficient free space ({} available), no log being saved",
FormatBytesSize(state.freeSpace));
m_state = kStopped;
} else {
// try preferred filename, or randomize it a few times, before giving up
for (int i = 0; i < 5; ++i) {
// open file for append
#ifdef _WIN32
// WIN32 doesn't allow combination of CreateNew and Append
state.f =
fs::OpenFileForWrite(state.path, ec, fs::CD_CreateNew, fs::OF_None);
#else
state.f =
fs::OpenFileForWrite(state.path, ec, fs::CD_CreateNew, fs::OF_Append);
#endif
if (ec) {
WPI_ERROR(m_msglog, "Could not open log file '{}': {}",
state.path.string(), ec.message());
// try again with random filename
state.SetFilename(MakeRandomFilename());
} else {
break;
}
}
if (state.f == WPI_kInvalidFile) {
WPI_ERROR(m_msglog, "Could not open log file, no log being saved");
} else {
WPI_INFO(m_msglog, "Logging to '{}' ({} free space)", state.path.string(),
FormatBytesSize(state.freeSpace));
}
}
// start file
if (state.f != WPI_kInvalidFile) {
StartFile();
}
}
void DataLogBackgroundWriter::WriterThreadMain(std::string_view dir) {
std::chrono::duration<double> periodTime{m_period};
WriterThreadState state{dir};
{
std::scoped_lock lock{m_mutex};
state.SetFilename(m_newFilename);
m_newFilename.clear();
}
StartLogFile(state);
std::error_code ec;
std::vector<DataLog::Buffer> toWrite;
int freeSpaceCount = 0;
int checkExistCount = 0;
bool blocked = false;
uintmax_t written = 0;
std::unique_lock lock{m_mutex};
do {
bool doFlush = false;
auto timeoutTime = std::chrono::steady_clock::now() + periodTime;
if (m_cond.wait_until(lock, timeoutTime) == std::cv_status::timeout) {
doFlush = true;
}
if (m_state == kStopped) {
state.Close();
continue;
}
bool doStart = false;
// if file was deleted, recreate it with the same name
if (++checkExistCount >= 10) {
checkExistCount = 0;
lock.unlock();
bool exists = fs::exists(state.path, ec);
lock.lock();
if (!ec && !exists) {
state.Close();
state.IncrementFilename();
WPI_INFO(m_msglog, "Log file deleted, recreating as fresh log '{}'",
state.filename);
doStart = true;
}
}
// start new file if file exceeds 1.8 GB
if (written > 1800000000ull) {
state.Close();
state.IncrementFilename();
WPI_INFO(m_msglog, "Log file reached 1.8 GB, starting new file '{}'",
state.filename);
doStart = true;
}
if (m_state == kStart || doStart) {
lock.unlock();
DataLog::Stop();
StartLogFile(state);
lock.lock();
if (m_state == kStopped) {
continue;
}
m_state = kActive;
written = 0;
}
if (!m_newFilename.empty() && state.f != WPI_kInvalidFile) {
auto newFilename = std::move(m_newFilename);
m_newFilename.clear();
// rename
if (state.filename != newFilename) {
lock.unlock();
fs::rename(state.path, state.dirPath / newFilename, ec);
lock.lock();
}
if (ec) {
WPI_ERROR(m_msglog, "Could not rename log file from '{}' to '{}': {}",
state.filename, newFilename, ec.message());
} else {
WPI_INFO(m_msglog, "Renamed log file from '{}' to '{}'", state.filename,
newFilename);
}
state.SetFilename(newFilename);
}
if (doFlush || m_doFlush) {
// flush to file
m_doFlush = false;
DataLog::FlushBufs(&toWrite);
if (toWrite.empty()) {
continue;
}
if (state.f != WPI_kInvalidFile && !blocked) {
lock.unlock();
// update free space every 10 flushes (in case other things are writing)
if (++freeSpaceCount >= 10) {
freeSpaceCount = 0;
auto freeSpaceInfo = fs::space(state.dirPath, ec);
if (!ec) {
state.freeSpace = freeSpaceInfo.available;
} else {
state.freeSpace = UINTMAX_MAX;
}
}
// write buffers to file
for (auto&& buf : toWrite) {
// stop writing when we go below the minimum free space
state.freeSpace -= buf.GetData().size();
written += buf.GetData().size();
if (state.freeSpace < kMinFreeSpace) {
[[unlikely]] WPI_ERROR(
m_msglog,
"Stopped logging due to low free space ({} available)",
FormatBytesSize(state.freeSpace));
blocked = true;
break;
}
WriteToFile(state.f, buf.GetData(), state.filename, m_msglog);
}
// sync to storage
#if defined(__linux__)
::fdatasync(state.f);
#elif defined(__APPLE__)
::fsync(state.f);
#endif
lock.lock();
if (blocked) {
[[unlikely]] m_state = kPaused;
}
}
// release buffers back to free list
ReleaseBufs(&toWrite);
}
} while (!m_shutdown);
}
void DataLogBackgroundWriter::WriterThreadMain(
std::function<void(std::span<const uint8_t> data)> write) {
std::chrono::duration<double> periodTime{m_period};
StartFile();
std::vector<DataLog::Buffer> toWrite;
std::unique_lock lock{m_mutex};
do {
bool doFlush = false;
auto timeoutTime = std::chrono::steady_clock::now() + periodTime;
if (m_cond.wait_until(lock, timeoutTime) == std::cv_status::timeout) {
doFlush = true;
}
if (doFlush || m_doFlush) {
// flush to file
m_doFlush = false;
DataLog::FlushBufs(&toWrite);
if (toWrite.empty()) {
continue;
}
lock.unlock();
// write buffers
for (auto&& buf : toWrite) {
if (!buf.GetData().empty()) {
write(buf.GetData());
}
}
lock.lock();
// release buffers back to free list
ReleaseBufs(&toWrite);
}
} while (!m_shutdown);
write({}); // indicate EOF
}
extern "C" {
struct WPI_DataLog* WPI_DataLog_CreateBackgroundWriter(
const struct WPI_String* dir, const struct WPI_String* filename,
double period, const struct WPI_String* extraHeader) {
return reinterpret_cast<WPI_DataLog*>(new DataLogBackgroundWriter{
wpi::to_string_view(dir), wpi::to_string_view(filename), period,
wpi::to_string_view(extraHeader)});
}
struct WPI_DataLog* WPI_DataLog_CreateBackgroundWriter_Func(
void (*write)(void* ptr, const uint8_t* data, size_t len), void* ptr,
double period, const struct WPI_String* extraHeader) {
return reinterpret_cast<WPI_DataLog*>(new DataLogBackgroundWriter{
[=](auto data) { write(ptr, data.data(), data.size()); }, period,
wpi::to_string_view(extraHeader)});
}
void WPI_DataLog_SetBackgroundWriterFilename(
struct WPI_DataLog* datalog, const struct WPI_String* filename) {
reinterpret_cast<DataLogBackgroundWriter*>(datalog)->SetFilename(
wpi::to_string_view(filename));
}
} // extern "C"

View File

@@ -0,0 +1,309 @@
// 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.
#include <bit>
#include <utility>
#include <wpi/Endian.h>
#include "wpi/datalog/DataLog.h"
#include "wpi/datalog/DataLogReader.h"
using namespace wpi::log;
static bool ReadString(std::span<const uint8_t>* buf, std::string_view* str) {
if (buf->size() < 4) {
*str = {};
return false;
}
uint32_t len = wpi::support::endian::read32le(buf->data());
if (len > (buf->size() - 4)) {
*str = {};
return false;
}
*str = {reinterpret_cast<const char*>(buf->data() + 4), len};
*buf = buf->subspan(len + 4);
return true;
}
bool DataLogRecord::IsStart() const {
return m_entry == 0 && m_data.size() >= 17 &&
m_data[0] == impl::kControlStart;
}
bool DataLogRecord::IsFinish() const {
return m_entry == 0 && m_data.size() == 5 &&
m_data[0] == impl::kControlFinish;
}
bool DataLogRecord::IsSetMetadata() const {
return m_entry == 0 && m_data.size() >= 9 &&
m_data[0] == impl::kControlSetMetadata;
}
bool DataLogRecord::GetStartData(StartRecordData* out) const {
if (!IsStart()) {
return false;
}
out->entry = wpi::support::endian::read32le(&m_data[1]);
auto buf = m_data.subspan(5);
if (!ReadString(&buf, &out->name)) {
return false;
}
if (!ReadString(&buf, &out->type)) {
return false;
}
if (!ReadString(&buf, &out->metadata)) {
return false;
}
return true;
}
bool DataLogRecord::GetFinishEntry(int* out) const {
if (!IsFinish()) {
return false;
}
*out = wpi::support::endian::read32le(&m_data[1]);
return true;
}
bool DataLogRecord::GetSetMetadataData(MetadataRecordData* out) const {
if (!IsSetMetadata()) {
return false;
}
out->entry = wpi::support::endian::read32le(&m_data[1]);
auto buf = m_data.subspan(5);
return ReadString(&buf, &out->metadata);
}
bool DataLogRecord::GetBoolean(bool* value) const {
if (m_data.size() != 1) {
return false;
}
*value = m_data[0] != 0;
return true;
}
bool DataLogRecord::GetInteger(int64_t* value) const {
if (m_data.size() != 8) {
return false;
}
*value = wpi::support::endian::read64le(m_data.data());
return true;
}
bool DataLogRecord::GetFloat(float* value) const {
if (m_data.size() != 4) {
return false;
}
*value = std::bit_cast<float>(wpi::support::endian::read32le(m_data.data()));
return true;
}
bool DataLogRecord::GetDouble(double* value) const {
if (m_data.size() != 8) {
return false;
}
*value = std::bit_cast<double>(wpi::support::endian::read64le(m_data.data()));
return true;
}
bool DataLogRecord::GetString(std::string_view* value) const {
*value = {reinterpret_cast<const char*>(m_data.data()), m_data.size()};
return true;
}
bool DataLogRecord::GetBooleanArray(std::vector<int>* arr) const {
arr->clear();
arr->reserve(m_data.size());
for (auto v : m_data) {
arr->push_back(v);
}
return true;
}
bool DataLogRecord::GetIntegerArray(std::vector<int64_t>* arr) const {
arr->clear();
if ((m_data.size() % 8) != 0) {
return false;
}
arr->reserve(m_data.size() / 8);
for (size_t pos = 0; pos < m_data.size(); pos += 8) {
arr->push_back(wpi::support::endian::read64le(&m_data[pos]));
}
return true;
}
bool DataLogRecord::GetFloatArray(std::vector<float>* arr) const {
arr->clear();
if ((m_data.size() % 4) != 0) {
return false;
}
arr->reserve(m_data.size() / 4);
for (size_t pos = 0; pos < m_data.size(); pos += 4) {
arr->push_back(
std::bit_cast<float>(wpi::support::endian::read32le(&m_data[pos])));
}
return true;
}
bool DataLogRecord::GetDoubleArray(std::vector<double>* arr) const {
arr->clear();
if ((m_data.size() % 8) != 0) {
return false;
}
arr->reserve(m_data.size() / 8);
for (size_t pos = 0; pos < m_data.size(); pos += 8) {
arr->push_back(
std::bit_cast<double>(wpi::support::endian::read64le(&m_data[pos])));
}
return true;
}
bool DataLogRecord::GetStringArray(std::vector<std::string_view>* arr) const {
arr->clear();
if (m_data.size() < 4) {
return false;
}
uint32_t size = wpi::support::endian::read32le(m_data.data());
// sanity check size
if (size > ((m_data.size() - 4) / 4)) {
return false;
}
auto buf = m_data.subspan(4);
arr->reserve(size);
for (uint32_t i = 0; i < size; ++i) {
std::string_view str;
if (!ReadString(&buf, &str)) {
arr->clear();
return false;
}
arr->push_back(str);
}
// any left over? treat as corrupt
if (!buf.empty()) {
arr->clear();
return false;
}
return true;
}
DataLogReader::DataLogReader(std::unique_ptr<MemoryBuffer> buffer)
: m_buf{std::move(buffer)} {}
bool DataLogReader::IsValid() const {
if (!m_buf) {
return false;
}
auto buf = m_buf->GetBuffer();
return buf.size() >= 12 &&
std::string_view{reinterpret_cast<const char*>(buf.data()), 6} ==
"WPILOG" &&
wpi::support::endian::read16le(&buf[6]) >= 0x0100;
}
uint16_t DataLogReader::GetVersion() const {
if (!m_buf) {
return 0;
}
auto buf = m_buf->GetBuffer();
if (buf.size() < 12) {
return 0;
}
return wpi::support::endian::read16le(&buf[6]);
}
std::string_view DataLogReader::GetExtraHeader() const {
if (!m_buf) {
return {};
}
auto buf = m_buf->GetBuffer();
if (buf.size() < 8) {
return {};
}
std::string_view rv;
buf = buf.subspan(8);
ReadString(&buf, &rv);
return rv;
}
DataLogReader::iterator DataLogReader::begin() const {
if (!m_buf) {
return end();
}
auto buf = m_buf->GetBuffer();
if (buf.size() < 12) {
return end();
}
uint32_t size = wpi::support::endian::read32le(&buf[8]);
if (buf.size() < (12 + size)) {
return end();
}
return DataLogIterator{this, 12 + size};
}
static uint64_t ReadVarInt(std::span<const uint8_t> buf) {
uint64_t val = 0;
int shift = 0;
for (auto v : buf) {
val |= static_cast<uint64_t>(v) << shift;
shift += 8;
}
return val;
}
bool DataLogReader::GetRecord(size_t* pos, DataLogRecord* out) const {
if (!m_buf) {
return false;
}
auto buf = m_buf->GetBuffer();
if (*pos >= buf.size()) {
return false;
}
buf = buf.subspan(*pos);
if (buf.size() < 4) { // minimum header length
return false;
}
unsigned int entryLen = (buf[0] & 0x3) + 1;
unsigned int sizeLen = ((buf[0] >> 2) & 0x3) + 1;
unsigned int timestampLen = ((buf[0] >> 4) & 0x7) + 1;
unsigned int headerLen = 1 + entryLen + sizeLen + timestampLen;
if (buf.size() < headerLen) {
return false;
}
int entry = ReadVarInt(buf.subspan(1, entryLen));
uint32_t size = ReadVarInt(buf.subspan(1 + entryLen, sizeLen));
if (size > (buf.size() - headerLen)) {
return false;
}
int64_t timestamp =
ReadVarInt(buf.subspan(1 + entryLen + sizeLen, timestampLen));
*out = DataLogRecord{entry, timestamp, buf.subspan(headerLen, size)};
*pos += headerLen + size;
return true;
}
bool DataLogReader::GetNextRecord(size_t* pos) const {
if (!m_buf) {
return false;
}
auto buf = m_buf->GetBuffer();
if (buf.size() < (*pos + 4)) { // minimum header length
return false;
}
unsigned int entryLen = (buf[*pos] & 0x3) + 1;
unsigned int sizeLen = ((buf[*pos] >> 2) & 0x3) + 1;
unsigned int timestampLen = ((buf[*pos] >> 4) & 0x7) + 1;
unsigned int headerLen = 1 + entryLen + sizeLen + timestampLen;
if (buf.size() < (*pos + headerLen)) {
return false;
}
uint32_t size = ReadVarInt(buf.subspan(*pos + 1 + entryLen, sizeLen));
// check this way to avoid overflow
if (size >= (buf.size() - *pos - headerLen)) {
return false;
}
*pos += headerLen + size;
return true;
}

View File

@@ -0,0 +1,125 @@
// 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.
#include <string>
#include <utility>
#include <wpi/StringExtras.h>
#include <wpi/print.h>
#include "wpi/datalog/DataLogReaderThread.h"
using namespace wpi::log;
DataLogReaderThread::~DataLogReaderThread() {
if (m_thread.joinable()) {
m_active = false;
m_thread.join();
}
}
void DataLogReaderThread::ReadMain() {
wpi::SmallDenseMap<
int, std::pair<DataLogReaderEntry*, std::span<const uint8_t>>, 8>
schemaEntries;
for (auto recordIt = m_reader.begin(), recordEnd = m_reader.end();
recordIt != recordEnd; ++recordIt) {
auto& record = *recordIt;
if (!m_active) {
break;
}
++m_numRecords;
if (record.IsStart()) {
DataLogReaderEntry data;
if (record.GetStartData(&data)) {
std::scoped_lock lock{m_mutex};
auto& entryPtr = m_entriesById[data.entry];
if (entryPtr) {
wpi::print("...DUPLICATE entry ID, overriding\n");
}
auto [it, isNew] = m_entriesByName.emplace(data.name, data);
if (isNew) {
it->second.ranges.emplace_back(recordIt, recordEnd);
}
entryPtr = &it->second;
if (data.type == "structschema" ||
data.type == "proto:FileDescriptorProto") {
schemaEntries.try_emplace(data.entry, entryPtr,
std::span<const uint8_t>{});
}
sigEntryAdded(data);
} else {
wpi::print("Start(INVALID)\n");
}
} else if (record.IsFinish()) {
int entry;
if (record.GetFinishEntry(&entry)) {
std::scoped_lock lock{m_mutex};
auto it = m_entriesById.find(entry);
if (it == m_entriesById.end()) {
wpi::print("...ID not found\n");
} else {
it->second->ranges.back().m_end = recordIt;
m_entriesById.erase(it);
}
} else {
wpi::print("Finish(INVALID)\n");
}
} else if (record.IsSetMetadata()) {
wpi::log::MetadataRecordData data;
if (record.GetSetMetadataData(&data)) {
std::scoped_lock lock{m_mutex};
auto it = m_entriesById.find(data.entry);
if (it == m_entriesById.end()) {
wpi::print("...ID not found\n");
} else {
it->second->metadata = data.metadata;
}
} else {
wpi::print("SetMetadata(INVALID)\n");
}
} else if (record.IsControl()) {
wpi::print("Unrecognized control record\n");
} else {
auto it = schemaEntries.find(record.GetEntry());
if (it != schemaEntries.end()) {
it->second.second = record.GetRaw();
}
}
}
// build schema databases
for (auto&& schemaPair : schemaEntries) {
auto name = schemaPair.second.first->name;
auto data = schemaPair.second.second;
if (data.empty()) {
continue;
}
if (auto strippedName = wpi::remove_prefix(name, "NT:")) {
name = *strippedName;
}
if (auto typeStr = wpi::remove_prefix(name, "/.schema/struct:")) {
std::string_view schema{reinterpret_cast<const char*>(data.data()),
data.size()};
std::string err;
auto desc = m_structDb.Add(*typeStr, schema, &err);
if (!desc) {
wpi::print("could not decode struct '{}' schema '{}': {}\n", name,
schema, err);
}
} else if (auto filename = wpi::remove_prefix(name, "/.schema/proto:")) {
#ifndef NO_PROTOBUF
// protobuf descriptor handling
if (!m_protoDb.Add(*filename, data)) {
wpi::print("could not decode protobuf '{}' filename '{}'\n", name,
*filename);
}
#endif
}
}
sigDone();
m_done = true;
}

View File

@@ -0,0 +1,88 @@
// 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.
#include <memory>
#include <utility>
#include <vector>
#include <wpi/raw_ostream.h>
#include "wpi/datalog/DataLogWriter.h"
using namespace wpi::log;
static std::unique_ptr<wpi::raw_ostream> CheckOpen(std::string_view filename,
std::error_code& ec) {
auto rv = std::make_unique<wpi::raw_fd_ostream>(filename, ec);
if (ec) {
return nullptr;
}
return rv;
}
DataLogWriter::DataLogWriter(std::string_view filename, std::error_code& ec,
std::string_view extraHeader)
: DataLogWriter{s_defaultMessageLog, filename, ec, extraHeader} {}
DataLogWriter::DataLogWriter(wpi::Logger& msglog, std::string_view filename,
std::error_code& ec, std::string_view extraHeader)
: DataLogWriter{msglog, CheckOpen(filename, ec), extraHeader} {
if (ec) {
Stop();
}
}
DataLogWriter::DataLogWriter(std::unique_ptr<wpi::raw_ostream> os,
std::string_view extraHeader)
: DataLogWriter{s_defaultMessageLog, std::move(os), extraHeader} {}
DataLogWriter::DataLogWriter(wpi::Logger& msglog,
std::unique_ptr<wpi::raw_ostream> os,
std::string_view extraHeader)
: DataLog{msglog, extraHeader}, m_os{std::move(os)} {
StartFile();
}
DataLogWriter::~DataLogWriter() {
if (m_os) {
Flush();
}
}
void DataLogWriter::Flush() {
if (!m_os) {
return;
}
std::vector<Buffer> writeBufs;
FlushBufs(&writeBufs);
for (auto&& buf : writeBufs) {
(*m_os) << buf.GetData();
}
ReleaseBufs(&writeBufs);
}
void DataLogWriter::Stop() {
DataLog::Stop();
Flush();
m_os.reset();
}
bool DataLogWriter::BufferFull() {
Flush();
return false;
}
extern "C" {
struct WPI_DataLog* WPI_DataLog_CreateWriter(
const struct WPI_String* filename, int* errorCode,
const struct WPI_String* extraHeader) {
std::error_code ec;
auto rv = reinterpret_cast<WPI_DataLog*>(new DataLogWriter{
wpi::to_string_view(filename), ec, wpi::to_string_view(extraHeader)});
*errorCode = ec.value();
return rv;
}
} // extern "C"

View File

@@ -0,0 +1,103 @@
// 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.
#ifdef __linux__
#include <fcntl.h>
#include <sys/inotify.h>
#include <unistd.h>
#endif
#include <chrono>
#include <string>
#include <string_view>
#include <thread>
#include <tuple>
#include <utility>
#include <fmt/format.h>
#include <wpi/StringExtras.h>
#include "wpi/datalog/FileLogger.h"
namespace wpi::log {
FileLogger::FileLogger(std::string_view file,
std::function<void(std::string_view)> callback)
#ifdef __linux__
: m_fileHandle{open(file.data(), O_RDONLY)},
m_inotifyHandle{inotify_init()},
m_inotifyWatchHandle{
inotify_add_watch(m_inotifyHandle, file.data(), IN_MODIFY)},
m_thread{[=, this] {
char buf[8000];
char eventBuf[sizeof(struct inotify_event) + NAME_MAX + 1];
lseek(m_fileHandle, 0, SEEK_END);
while (read(m_inotifyHandle, eventBuf, sizeof(eventBuf)) > 0) {
int bufLen = 0;
if ((bufLen = read(m_fileHandle, buf, sizeof(buf))) > 0) {
callback(std::string_view{buf, static_cast<size_t>(bufLen)});
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}}
#endif
{
}
FileLogger::FileLogger(std::string_view file, log::DataLog& log,
std::string_view key)
: FileLogger(file, Buffer([entry = log.Start(key, "string"),
&log](std::string_view line) {
log.AppendString(entry, line, 0);
})) {}
FileLogger::FileLogger(FileLogger&& other)
#ifdef __linux__
: m_fileHandle{std::exchange(other.m_fileHandle, -1)},
m_inotifyHandle{std::exchange(other.m_inotifyHandle, -1)},
m_inotifyWatchHandle{std::exchange(other.m_inotifyWatchHandle, -1)},
m_thread{std::move(other.m_thread)}
#endif
{
}
FileLogger& FileLogger::operator=(FileLogger&& rhs) {
#ifdef __linux__
std::swap(m_fileHandle, rhs.m_fileHandle);
std::swap(m_inotifyHandle, rhs.m_inotifyHandle);
std::swap(m_inotifyWatchHandle, rhs.m_inotifyWatchHandle);
m_thread = std::move(rhs.m_thread);
#endif
return *this;
}
FileLogger::~FileLogger() {
#ifdef __linux__
if (m_inotifyWatchHandle != -1) {
inotify_rm_watch(m_inotifyHandle, m_inotifyWatchHandle);
}
if (m_inotifyHandle != -1) {
close(m_inotifyHandle);
}
if (m_fileHandle != -1) {
close(m_fileHandle);
}
if (m_thread.joinable()) {
m_thread.join();
}
#endif
}
std::function<void(std::string_view)> FileLogger::Buffer(
std::function<void(std::string_view)> callback) {
return [callback,
buf = wpi::SmallVector<char, 64>{}](std::string_view data) mutable {
buf.append(data.begin(), data.end());
if (!wpi::contains({data.data(), data.size()}, "\n")) {
return;
}
auto [wholeData, extra] = wpi::rsplit({buf.data(), buf.size()}, "\n");
std::string leftover{extra};
callback(wholeData);
buf.clear();
buf.append(leftover.begin(), leftover.end());
};
}
} // namespace wpi::log

View File

@@ -0,0 +1,725 @@
// 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.
#include "DataLogJNI.h"
#include <jni.h>
#include <algorithm>
#include <string>
#include <vector>
#include <fmt/format.h>
#include <wpi/jni_util.h>
#include "edu_wpi_first_datalog_DataLogJNI.h"
#include "wpi/datalog/DataLog.h"
#include "wpi/datalog/DataLogBackgroundWriter.h"
#include "wpi/datalog/DataLogWriter.h"
#include "wpi/datalog/FileLogger.h"
using namespace wpi::java;
using namespace wpi::log;
static bool mockTimeEnabled = false;
static uint64_t mockNow = 0;
static JException illegalArgEx;
static JException indexOobEx;
static JException ioEx;
static JException nullPointerEx;
static const JExceptionInit exceptions[] = {
{"java/lang/IllegalArgumentException", &illegalArgEx},
{"java/lang/IndexOutOfBoundsException", &indexOobEx},
{"java/io/IOException", &ioEx},
{"java/lang/NullPointerException", &nullPointerEx}};
void wpi::ThrowIllegalArgumentException(JNIEnv* env, std::string_view msg) {
illegalArgEx.Throw(env, msg);
}
void wpi::ThrowIndexOobException(JNIEnv* env, std::string_view msg) {
indexOobEx.Throw(env, msg);
}
void wpi::ThrowIOException(JNIEnv* env, std::string_view msg) {
ioEx.Throw(env, msg);
}
void wpi::ThrowNullPointerException(JNIEnv* env, std::string_view msg) {
nullPointerEx.Throw(env, msg);
}
namespace {
class buf_ostream : public wpi::raw_uvector_ostream {
private:
std::vector<uint8_t> data;
public:
buf_ostream() : raw_uvector_ostream{data} {}
void clear() { data.clear(); }
};
} // namespace
extern "C" {
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env;
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
for (auto& c : exceptions) {
*c.cls = JException(env, c.name);
if (!*c.cls) {
return JNI_ERR;
}
}
return JNI_VERSION_1_6;
}
JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* vm, void* reserved) {
JNIEnv* env;
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
return;
}
for (auto& c : exceptions) {
c.cls->free(env);
}
}
/*
* Class: edu_wpi_first_datalog_DataLogJNI
* Method: bgCreate
* Signature: (Ljava/lang/String;Ljava/lang/String;DLjava/lang/String;)J
*/
JNIEXPORT jlong JNICALL
Java_edu_wpi_first_datalog_DataLogJNI_bgCreate
(JNIEnv* env, jclass, jstring dir, jstring filename, jdouble period,
jstring extraHeader)
{
if (!dir) {
wpi::ThrowNullPointerException(env, "dir is null");
return 0;
}
if (!filename) {
wpi::ThrowNullPointerException(env, "filename is null");
return 0;
}
if (!extraHeader) {
wpi::ThrowNullPointerException(env, "extraHeader is null");
return 0;
}
return reinterpret_cast<jlong>(new DataLogBackgroundWriter{
JStringRef{env, dir}, JStringRef{env, filename}, period,
JStringRef{env, extraHeader}});
}
/*
* Class: edu_wpi_first_datalog_DataLogJNI
* Method: bgSetFilename
* Signature: (JLjava/lang/String;)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_datalog_DataLogJNI_bgSetFilename
(JNIEnv* env, jclass, jlong impl, jstring filename)
{
if (impl == 0) {
wpi::ThrowNullPointerException(env, "impl is null");
return;
}
if (!filename) {
wpi::ThrowNullPointerException(env, "filename is null");
return;
}
reinterpret_cast<DataLogBackgroundWriter*>(impl)->SetFilename(
JStringRef{env, filename});
}
/*
* Class: edu_wpi_first_datalog_DataLogJNI
* Method: fgCreate
* Signature: (Ljava/lang/String;Ljava/lang/String;)J
*/
JNIEXPORT jlong JNICALL
Java_edu_wpi_first_datalog_DataLogJNI_fgCreate
(JNIEnv* env, jclass, jstring filename, jstring extraHeader)
{
if (!filename) {
wpi::ThrowNullPointerException(env, "filename is null");
return 0;
}
if (!extraHeader) {
wpi::ThrowNullPointerException(env, "extraHeader is null");
return 0;
}
std::error_code ec;
auto writer = new DataLogWriter{JStringRef{env, filename}, ec,
JStringRef{env, extraHeader}};
if (ec) {
wpi::ThrowIOException(env, ec.message());
delete writer;
return 0;
}
return reinterpret_cast<jlong>(writer);
}
/*
* Class: edu_wpi_first_util_WPIUtilJNI
* Method: now
* Signature: ()J
*/
JNIEXPORT jlong JNICALL
Java_edu_wpi_first_util_WPIUtilJNI_now
(JNIEnv*, jclass)
{
if (mockTimeEnabled) {
return mockNow;
} else {
return wpi::Now();
}
}
/*
* Class: edu_wpi_first_datalog_DataLogJNI
* Method: fgCreateMemory
* Signature: (Ljava/lang/String;)J
*/
JNIEXPORT jlong JNICALL
Java_edu_wpi_first_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<buf_ostream>(),
JStringRef{env, extraHeader}};
return reinterpret_cast<jlong>(writer);
}
/*
* Class: edu_wpi_first_datalog_DataLogJNI
* Method: flush
* Signature: (J)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_datalog_DataLogJNI_flush
(JNIEnv* env, jclass, jlong impl)
{
if (impl == 0) {
wpi::ThrowNullPointerException(env, "impl is null");
return;
}
reinterpret_cast<DataLog*>(impl)->Flush();
}
/*
* Class: edu_wpi_first_datalog_DataLogJNI
* Method: copyWriteBuffer
* Signature: (J[BI)I
*/
JNIEXPORT jint JNICALL
Java_edu_wpi_first_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<DataLogWriter*>(impl);
writer->Flush();
auto& stream = static_cast<buf_ostream&>(writer->GetStream());
JSpan<jbyte> jbuf{env, buf};
auto arr = stream.array();
if (start < 0 || static_cast<size_t>(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;
}
/*
* Class: edu_wpi_first_datalog_DataLogJNI
* Method: pause
* Signature: (J)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_datalog_DataLogJNI_pause
(JNIEnv* env, jclass, jlong impl)
{
if (impl == 0) {
wpi::ThrowNullPointerException(env, "impl is null");
return;
}
reinterpret_cast<DataLog*>(impl)->Pause();
}
/*
* Class: edu_wpi_first_datalog_DataLogJNI
* Method: resume
* Signature: (J)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_datalog_DataLogJNI_resume
(JNIEnv* env, jclass, jlong impl)
{
if (impl == 0) {
wpi::ThrowNullPointerException(env, "impl is null");
return;
}
reinterpret_cast<DataLog*>(impl)->Resume();
}
/*
* Class: edu_wpi_first_datalog_DataLogJNI
* Method: stop
* Signature: (J)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_datalog_DataLogJNI_stop
(JNIEnv* env, jclass, jlong impl)
{
if (impl == 0) {
wpi::ThrowNullPointerException(env, "impl is null");
return;
}
reinterpret_cast<DataLog*>(impl)->Stop();
}
/*
* Class: edu_wpi_first_datalog_DataLogJNI
* Method: addSchema
* Signature: (JLjava/lang/String;Ljava/lang/String;[BJ)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_datalog_DataLogJNI_addSchema
(JNIEnv* env, jclass, jlong impl, jstring name, jstring type,
jbyteArray schema, jlong timestamp)
{
if (impl == 0) {
wpi::ThrowNullPointerException(env, "impl is null");
return;
}
reinterpret_cast<DataLog*>(impl)->AddSchema(
JStringRef{env, name}, JStringRef{env, type},
JSpan<const jbyte>{env, schema}.uarray(), timestamp);
}
/*
* Class: edu_wpi_first_datalog_DataLogJNI
* Method: addSchemaString
* Signature: (JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;J)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_datalog_DataLogJNI_addSchemaString
(JNIEnv* env, jclass, jlong impl, jstring name, jstring type, jstring schema,
jlong timestamp)
{
if (impl == 0) {
wpi::ThrowNullPointerException(env, "impl is null");
return;
}
JStringRef schemaStr{env, schema};
std::string_view schemaView = schemaStr.str();
reinterpret_cast<DataLog*>(impl)->AddSchema(
JStringRef{env, name}, JStringRef{env, type},
{reinterpret_cast<const uint8_t*>(schemaView.data()), schemaView.size()},
timestamp);
}
/*
* Class: edu_wpi_first_datalog_DataLogJNI
* Method: start
* Signature: (JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;J)I
*/
JNIEXPORT jint JNICALL
Java_edu_wpi_first_datalog_DataLogJNI_start
(JNIEnv* env, jclass, jlong impl, jstring name, jstring type,
jstring metadata, jlong timestamp)
{
if (impl == 0) {
wpi::ThrowNullPointerException(env, "impl is null");
return 0;
}
return reinterpret_cast<DataLog*>(impl)->Start(
JStringRef{env, name}, JStringRef{env, type}, JStringRef{env, metadata},
timestamp);
}
/*
* Class: edu_wpi_first_datalog_DataLogJNI
* Method: finish
* Signature: (JIJ)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_datalog_DataLogJNI_finish
(JNIEnv* env, jclass, jlong impl, jint entry, jlong timestamp)
{
if (impl == 0) {
wpi::ThrowNullPointerException(env, "impl is null");
return;
}
reinterpret_cast<DataLog*>(impl)->Finish(entry, timestamp);
}
/*
* Class: edu_wpi_first_datalog_DataLogJNI
* Method: setMetadata
* Signature: (JILjava/lang/String;J)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_datalog_DataLogJNI_setMetadata
(JNIEnv* env, jclass, jlong impl, jint entry, jstring metadata,
jlong timestamp)
{
if (impl == 0) {
wpi::ThrowNullPointerException(env, "impl is null");
return;
}
reinterpret_cast<DataLog*>(impl)->SetMetadata(
entry, JStringRef{env, metadata}, timestamp);
}
/*
* Class: edu_wpi_first_datalog_DataLogJNI
* Method: close
* Signature: (J)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_datalog_DataLogJNI_close
(JNIEnv*, jclass, jlong impl)
{
delete reinterpret_cast<DataLog*>(impl);
}
/*
* Class: edu_wpi_first_datalog_DataLogJNI
* Method: appendRaw
* Signature: (JI[BIIJ)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_datalog_DataLogJNI_appendRaw
(JNIEnv* env, jclass, jlong impl, jint entry, jbyteArray value, jint start,
jint length, jlong timestamp)
{
if (impl == 0) {
wpi::ThrowNullPointerException(env, "impl is null");
return;
}
if (!value) {
wpi::ThrowNullPointerException(env, "value is null");
return;
}
if (start < 0) {
wpi::ThrowIndexOobException(env, "start must be >= 0");
return;
}
if (length < 0) {
wpi::ThrowIndexOobException(env, "length must be >= 0");
return;
}
CriticalJSpan<const jbyte> cvalue{env, value};
if (static_cast<unsigned int>(start + length) > cvalue.size()) {
wpi::ThrowIndexOobException(
env, "start + len must be smaller than array length");
return;
}
reinterpret_cast<DataLog*>(impl)->AppendRaw(
entry, cvalue.uarray().subspan(start, length), timestamp);
}
/*
* Class: edu_wpi_first_datalog_DataLogJNI
* Method: appendRawBuffer
* Signature: (JILjava/lang/Object;IIJ)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_datalog_DataLogJNI_appendRawBuffer
(JNIEnv* env, jclass, jlong impl, jint entry, jobject value, jint start,
jint length, jlong timestamp)
{
if (impl == 0) {
wpi::ThrowNullPointerException(env, "impl is null");
return;
}
if (!value) {
wpi::ThrowNullPointerException(env, "value is null");
return;
}
if (start < 0) {
wpi::ThrowIndexOobException(env, "start must be >= 0");
return;
}
if (length < 0) {
wpi::ThrowIndexOobException(env, "length must be >= 0");
return;
}
JSpan<const jbyte> cvalue{env, value, static_cast<size_t>(start + length)};
if (!cvalue) {
wpi::ThrowIllegalArgumentException(env,
"value must be a native ByteBuffer");
return;
}
reinterpret_cast<DataLog*>(impl)->AppendRaw(
entry, cvalue.uarray().subspan(start, length), timestamp);
}
/*
* Class: edu_wpi_first_datalog_DataLogJNI
* Method: appendBoolean
* Signature: (JIZJ)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_datalog_DataLogJNI_appendBoolean
(JNIEnv* env, jclass, jlong impl, jint entry, jboolean value, jlong timestamp)
{
if (impl == 0) {
wpi::ThrowNullPointerException(env, "impl is null");
return;
}
reinterpret_cast<DataLog*>(impl)->AppendBoolean(entry, value, timestamp);
}
/*
* Class: edu_wpi_first_datalog_DataLogJNI
* Method: appendInteger
* Signature: (JIJJ)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_datalog_DataLogJNI_appendInteger
(JNIEnv* env, jclass, jlong impl, jint entry, jlong value, jlong timestamp)
{
if (impl == 0) {
wpi::ThrowNullPointerException(env, "impl is null");
return;
}
reinterpret_cast<DataLog*>(impl)->AppendInteger(entry, value, timestamp);
}
/*
* Class: edu_wpi_first_datalog_DataLogJNI
* Method: appendFloat
* Signature: (JIFJ)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_datalog_DataLogJNI_appendFloat
(JNIEnv* env, jclass, jlong impl, jint entry, jfloat value, jlong timestamp)
{
if (impl == 0) {
wpi::ThrowNullPointerException(env, "impl is null");
return;
}
reinterpret_cast<DataLog*>(impl)->AppendFloat(entry, value, timestamp);
}
/*
* Class: edu_wpi_first_datalog_DataLogJNI
* Method: appendDouble
* Signature: (JIDJ)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_datalog_DataLogJNI_appendDouble
(JNIEnv* env, jclass, jlong impl, jint entry, jdouble value, jlong timestamp)
{
if (impl == 0) {
wpi::ThrowNullPointerException(env, "impl is null");
return;
}
reinterpret_cast<DataLog*>(impl)->AppendDouble(entry, value, timestamp);
}
/*
* Class: edu_wpi_first_datalog_DataLogJNI
* Method: appendString
* Signature: (JILjava/lang/String;J)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_datalog_DataLogJNI_appendString
(JNIEnv* env, jclass, jlong impl, jint entry, jstring value, jlong timestamp)
{
if (impl == 0) {
wpi::ThrowNullPointerException(env, "impl is null");
return;
}
reinterpret_cast<DataLog*>(impl)->AppendString(entry, JStringRef{env, value},
timestamp);
}
/*
* Class: edu_wpi_first_datalog_DataLogJNI
* Method: appendBooleanArray
* Signature: (JI[ZJ)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_datalog_DataLogJNI_appendBooleanArray
(JNIEnv* env, jclass, jlong impl, jint entry, jbooleanArray value,
jlong timestamp)
{
if (impl == 0) {
wpi::ThrowNullPointerException(env, "impl is null");
return;
}
if (!value) {
wpi::ThrowNullPointerException(env, "value is null");
return;
}
reinterpret_cast<DataLog*>(impl)->AppendBooleanArray(
entry, JSpan<const jboolean>{env, value}, timestamp);
}
/*
* Class: edu_wpi_first_datalog_DataLogJNI
* Method: appendIntegerArray
* Signature: (JI[JJ)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_datalog_DataLogJNI_appendIntegerArray
(JNIEnv* env, jclass, jlong impl, jint entry, jlongArray value,
jlong timestamp)
{
if (impl == 0) {
wpi::ThrowNullPointerException(env, "impl is null");
return;
}
if (!value) {
wpi::ThrowNullPointerException(env, "value is null");
return;
}
JSpan<const jlong> jarr{env, value};
if constexpr (sizeof(jlong) == sizeof(int64_t)) {
reinterpret_cast<DataLog*>(impl)->AppendIntegerArray(
entry, {reinterpret_cast<const int64_t*>(jarr.data()), jarr.size()},
timestamp);
} else {
wpi::SmallVector<int64_t, 16> arr;
arr.reserve(jarr.size());
for (auto v : jarr) {
arr.push_back(v);
}
reinterpret_cast<DataLog*>(impl)->AppendIntegerArray(entry, arr, timestamp);
}
}
/*
* Class: edu_wpi_first_datalog_DataLogJNI
* Method: appendFloatArray
* Signature: (JI[FJ)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_datalog_DataLogJNI_appendFloatArray
(JNIEnv* env, jclass, jlong impl, jint entry, jfloatArray value,
jlong timestamp)
{
if (impl == 0) {
wpi::ThrowNullPointerException(env, "impl is null");
return;
}
if (!value) {
wpi::ThrowNullPointerException(env, "value is null");
return;
}
reinterpret_cast<DataLog*>(impl)->AppendFloatArray(
entry, JSpan<const jfloat>{env, value}, timestamp);
}
/*
* Class: edu_wpi_first_datalog_DataLogJNI
* Method: appendDoubleArray
* Signature: (JI[DJ)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_datalog_DataLogJNI_appendDoubleArray
(JNIEnv* env, jclass, jlong impl, jint entry, jdoubleArray value,
jlong timestamp)
{
if (impl == 0) {
wpi::ThrowNullPointerException(env, "impl is null");
return;
}
if (!value) {
wpi::ThrowNullPointerException(env, "value is null");
return;
}
reinterpret_cast<DataLog*>(impl)->AppendDoubleArray(
entry, JSpan<const jdouble>{env, value}, timestamp);
}
/*
* Class: edu_wpi_first_datalog_DataLogJNI
* Method: appendStringArray
* Signature: (JI[Ljava/lang/Object;J)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_datalog_DataLogJNI_appendStringArray
(JNIEnv* env, jclass, jlong impl, jint entry, jobjectArray value,
jlong timestamp)
{
if (impl == 0) {
wpi::ThrowNullPointerException(env, "impl is null");
return;
}
if (!value) {
wpi::ThrowNullPointerException(env, "value is null");
return;
}
size_t len = env->GetArrayLength(value);
std::vector<std::string> arr;
arr.reserve(len);
for (size_t i = 0; i < len; ++i) {
JLocal<jstring> elem{
env, static_cast<jstring>(env->GetObjectArrayElement(value, i))};
if (!elem) {
wpi::ThrowNullPointerException(
env, fmt::format("string at element {} is null", i));
return;
}
arr.emplace_back(JStringRef{env, elem}.str());
}
reinterpret_cast<DataLog*>(impl)->AppendStringArray(entry, arr, timestamp);
}
/*
* Class: edu_wpi_first_datalog_DataLogJNI
* Method: createFileLogger
* Signature: (Ljava/lang/String;JLjava/lang/String;)J
*/
JNIEXPORT jlong JNICALL
Java_edu_wpi_first_datalog_DataLogJNI_createFileLogger
(JNIEnv* env, jclass, jstring file, jlong log, jstring key)
{
if (!file) {
wpi::ThrowNullPointerException(env, "file is null");
return 0;
}
auto* f = reinterpret_cast<wpi::log::DataLog*>(log);
if (!f) {
wpi::ThrowNullPointerException(env, "log is null");
return 0;
}
if (!key) {
wpi::ThrowNullPointerException(env, "key is null");
return 0;
}
return reinterpret_cast<jlong>(new wpi::log::FileLogger{
JStringRef{env, file}, *f, JStringRef{env, key}});
}
/*
* Class: edu_wpi_first_datalog_DataLogJNI
* Method: freeFileLogger
* Signature: (J)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_datalog_DataLogJNI_freeFileLogger
(JNIEnv* env, jclass, jlong fileTail)
{
delete reinterpret_cast<wpi::log::FileLogger*>(fileTail);
}
} // extern "C"

View File

@@ -0,0 +1,18 @@
// 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.
#pragma once
#include <jni.h>
#include <string_view>
namespace wpi {
void ThrowIllegalArgumentException(JNIEnv* env, std::string_view msg);
void ThrowIndexOobException(JNIEnv* env, std::string_view msg);
void ThrowIOException(JNIEnv* env, std::string_view msg);
void ThrowNullPointerException(JNIEnv* env, std::string_view msg);
} // namespace wpi

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,171 @@
// 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.
#pragma once
#include <stdint.h>
#include <functional>
#include <span>
#include <string>
#include <string_view>
#include <thread>
#include <wpi/condition_variable.h>
#include <wpi/mutex.h>
#include "wpi/datalog/DataLog.h"
namespace wpi {
class Logger;
} // namespace wpi
namespace wpi::log {
/**
* A data log background writer that periodically flushes the data log on a
* background thread. The data log file is created immediately upon
* construction with a temporary filename. The file may be renamed at any time
* using the SetFilename() function.
*
* The lifetime of this object must be longer than any data log entry objects
* that refer to it.
*
* The data log is periodically flushed to disk. It can also be explicitly
* flushed to disk by using the Flush() function. This operation is, however,
* non-blocking.
*/
class DataLogBackgroundWriter final : public DataLog {
public:
/**
* Construct a new Data Log. The log will be initially created with a
* temporary filename.
*
* @param dir directory to store the log
* @param filename filename to use; if none provided, a random filename is
* generated of the form "wpilog_{}.wpilog"
* @param period time between automatic flushes to disk, in seconds;
* this is a time/storage tradeoff
* @param extraHeader extra header data
*/
explicit DataLogBackgroundWriter(std::string_view dir = "",
std::string_view filename = "",
double period = 0.25,
std::string_view extraHeader = "");
/**
* Construct a new Data Log. The log will be initially created with a
* temporary filename.
*
* @param msglog message logger (will be called from separate thread)
* @param dir directory to store the log
* @param filename filename to use; if none provided, a random filename is
* generated of the form "wpilog_{}.wpilog"
* @param period time between automatic flushes to disk, in seconds;
* this is a time/storage tradeoff
* @param extraHeader extra header data
*/
explicit DataLogBackgroundWriter(wpi::Logger& msglog,
std::string_view dir = "",
std::string_view filename = "",
double period = 0.25,
std::string_view extraHeader = "");
/**
* Construct a new Data Log that passes its output to the provided function
* rather than a file. The write function will be called on a separate
* background thread and may block. The write function is called with an
* empty data array when the thread is terminating.
*
* @param write write function
* @param period time between automatic calls to write, in seconds;
* this is a time/storage tradeoff
* @param extraHeader extra header data
*/
explicit DataLogBackgroundWriter(
std::function<void(std::span<const uint8_t> data)> write,
double period = 0.25, std::string_view extraHeader = "");
/**
* Construct a new Data Log that passes its output to the provided function
* rather than a file. The write function will be called on a separate
* background thread and may block. The write function is called with an
* empty data array when the thread is terminating.
*
* @param msglog message logger (will be called from separate thread)
* @param write write function
* @param period time between automatic calls to write, in seconds;
* this is a time/storage tradeoff
* @param extraHeader extra header data
*/
explicit DataLogBackgroundWriter(
wpi::Logger& msglog,
std::function<void(std::span<const uint8_t> data)> write,
double period = 0.25, std::string_view extraHeader = "");
~DataLogBackgroundWriter() final;
DataLogBackgroundWriter(const DataLogBackgroundWriter&) = delete;
DataLogBackgroundWriter& operator=(const DataLogBackgroundWriter&) = delete;
DataLogBackgroundWriter(DataLogBackgroundWriter&&) = delete;
DataLogBackgroundWriter& operator=(const DataLogBackgroundWriter&&) = delete;
/**
* Change log filename.
*
* @param filename filename
*/
void SetFilename(std::string_view filename);
/**
* Explicitly flushes the log data to disk.
*/
void Flush() final;
/**
* 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.
*/
void Pause() final;
/**
* Resumes appending of data records to the log. If called after Stop(),
* opens a new file (with random name if SetFilename was not called after
* Stop()) and appends Start records and schema data values for all previously
* started entries and schemas.
*/
void Resume() final;
/**
* Stops appending all records to the log, and closes the log file.
*/
void Stop() final;
private:
struct WriterThreadState;
void BufferHalfFull() final;
bool BufferFull() final;
void StartLogFile(WriterThreadState& state);
void WriterThreadMain(std::string_view dir);
void WriterThreadMain(
std::function<void(std::span<const uint8_t> data)> write);
mutable wpi::mutex m_mutex;
wpi::condition_variable m_cond;
bool m_doFlush{false};
bool m_shutdown{false};
enum State {
kStart,
kActive,
kPaused,
kStopped,
} m_state = kActive;
double m_period;
std::string m_newFilename;
std::thread m_thread;
};
} // namespace wpi::log

View File

@@ -0,0 +1,369 @@
// 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.
#pragma once
#include <stdint.h>
#include <iterator>
#include <memory>
#include <span>
#include <utility>
#include <vector>
#include <wpi/MemoryBuffer.h>
namespace wpi::log {
/**
* Data contained in a start control record as created by DataLog::Start() when
* writing the log. This can be read by calling DataLogRecord::GetStartData().
*/
struct StartRecordData {
/** Entry ID; this will be used for this entry in future records. */
int entry;
/** Entry name. */
std::string_view name;
/** Type of the stored data for this entry, as a string, e.g. "double". */
std::string_view type;
/** Initial metadata. */
std::string_view metadata;
};
/**
* Data contained in a set metadata control record as created by
* DataLog::SetMetadata(). This can be read by calling
* DataLogRecord::GetSetMetadataData().
*/
struct MetadataRecordData {
/** Entry ID. */
int entry;
/** New metadata for the entry. */
std::string_view metadata;
};
/**
* A record in the data log. May represent either a control record (entry == 0)
* or a data record. Used only for reading (e.g. with DataLogReader).
*/
class DataLogRecord {
public:
DataLogRecord() = default;
DataLogRecord(int entry, int64_t timestamp, std::span<const uint8_t> data)
: m_timestamp{timestamp}, m_data{data}, m_entry{entry} {}
/**
* Gets the entry ID.
*
* @return entry ID
*/
int GetEntry() const { return m_entry; }
/**
* Gets the record timestamp.
*
* @return Timestamp, in integer microseconds
*/
int64_t GetTimestamp() const { return m_timestamp; }
/**
* Gets the size of the raw data.
*
* @return size
*/
size_t GetSize() const { return m_data.size(); }
/**
* Gets the raw data. Use the GetX functions to decode based on the data type
* in the entry's start record.
*/
std::span<const uint8_t> GetRaw() const { return m_data; }
/**
* Returns true if the record is a control record.
*
* @return True if control record, false if normal data record.
*/
bool IsControl() const { return m_entry == 0; }
/**
* Returns true if the record is a start control record. Use GetStartData()
* to decode the contents.
*
* @return True if start control record, false otherwise.
*/
bool IsStart() const;
/**
* Returns true if the record is a finish control record. Use GetFinishEntry()
* to decode the contents.
*
* @return True if finish control record, false otherwise.
*/
bool IsFinish() const;
/**
* Returns true if the record is a set metadata control record. Use
* GetSetMetadataData() to decode the contents.
*
* @return True if set metadata control record, false otherwise.
*/
bool IsSetMetadata() const;
/**
* Decodes a start control record.
*
* @param[out] out start record decoded data (if successful)
* @return True on success, false on error
*/
bool GetStartData(StartRecordData* out) const;
/**
* Decodes a finish control record.
*
* @param[out] out finish record entry ID (if successful)
* @return True on success, false on error
*/
bool GetFinishEntry(int* out) const;
/**
* Decodes a set metadata control record.
*
* @param[out] out set metadata record decoded data (if successful)
* @return True on success, false on error
*/
bool GetSetMetadataData(MetadataRecordData* out) const;
/**
* Decodes a data record as a boolean. Note if the data type (as indicated in
* the corresponding start control record for this entry) is not "boolean",
* invalid results may be returned.
*
* @param[out] value boolean value (if successful)
* @return True on success, false on error
*/
bool GetBoolean(bool* value) const;
/**
* Decodes a data record as an integer. Note if the data type (as indicated in
* the corresponding start control record for this entry) is not "int64",
* invalid results may be returned.
*
* @param[out] value integer value (if successful)
* @return True on success, false on error
*/
bool GetInteger(int64_t* value) const;
/**
* Decodes a data record as a float. Note if the data type (as indicated in
* the corresponding start control record for this entry) is not "float",
* invalid results may be returned.
*
* @param[out] value float value (if successful)
* @return True on success, false on error
*/
bool GetFloat(float* value) const;
/**
* Decodes a data record as a double. Note if the data type (as indicated in
* the corresponding start control record for this entry) is not "double",
* invalid results may be returned.
*
* @param[out] value double value (if successful)
* @return True on success, false on error
*/
bool GetDouble(double* value) const;
/**
* Decodes a data record as a string. Note if the data type (as indicated in
* the corresponding start control record for this entry) is not "string",
* invalid results may be returned.
*
* @param[out] value string value
* @return True (never fails)
*/
bool GetString(std::string_view* value) const;
/**
* Decodes a data record as a boolean array. Note if the data type (as
* indicated in the corresponding start control record for this entry) is not
* "boolean[]", invalid results may be returned.
*
* @param[out] arr boolean array
* @return True (never fails)
*/
bool GetBooleanArray(std::vector<int>* arr) const;
/**
* Decodes a data record as an integer array. Note if the data type (as
* indicated in the corresponding start control record for this entry) is not
* "int64[]", invalid results may be returned.
*
* @param[out] arr integer array (if successful)
* @return True on success, false on error
*/
bool GetIntegerArray(std::vector<int64_t>* arr) const;
/**
* Decodes a data record as a float array. Note if the data type (as
* indicated in the corresponding start control record for this entry) is not
* "float[]", invalid results may be returned.
*
* @param[out] arr float array (if successful)
* @return True on success, false on error
*/
bool GetFloatArray(std::vector<float>* arr) const;
/**
* Decodes a data record as a double array. Note if the data type (as
* indicated in the corresponding start control record for this entry) is not
* "double[]", invalid results may be returned.
*
* @param[out] arr double array (if successful)
* @return True on success, false on error
*/
bool GetDoubleArray(std::vector<double>* arr) const;
/**
* Decodes a data record as a string array. Note if the data type (as
* indicated in the corresponding start control record for this entry) is not
* "string[]", invalid results may be returned.
*
* @param[out] arr string array (if successful)
* @return True on success, false on error
*/
bool GetStringArray(std::vector<std::string_view>* arr) const;
private:
int64_t m_timestamp{0};
std::span<const uint8_t> m_data;
int m_entry{-1};
};
class DataLogReader;
/** DataLogReader iterator. */
class DataLogIterator {
public:
using iterator_category = std::forward_iterator_tag;
using value_type = DataLogRecord;
using pointer = const value_type*;
using reference = const value_type&;
DataLogIterator(const DataLogReader* reader, size_t pos)
: m_reader{reader}, m_pos{pos} {}
bool operator==(const DataLogIterator& oth) const {
return m_reader == oth.m_reader && m_pos == oth.m_pos;
}
bool operator!=(const DataLogIterator& oth) const {
return !this->operator==(oth);
}
bool operator<(const DataLogIterator& oth) const { return m_pos < oth.m_pos; }
bool operator>(const DataLogIterator& oth) const {
return !this->operator<(oth) && !this->operator==(oth);
}
bool operator<=(const DataLogIterator& oth) const {
return !this->operator>(oth);
}
bool operator>=(const DataLogIterator& oth) const {
return !this->operator<(oth);
}
DataLogIterator& operator++();
DataLogIterator operator++(int) {
DataLogIterator tmp = *this;
++*this;
return tmp;
}
reference operator*() const;
pointer operator->() const { return &this->operator*(); }
protected:
const DataLogReader* m_reader;
size_t m_pos;
mutable bool m_valid = false;
mutable DataLogRecord m_value;
};
/** Data log reader (reads logs written by the DataLog class). */
class DataLogReader {
friend class DataLogIterator;
public:
using iterator = DataLogIterator;
/** Constructs from a memory buffer. */
explicit DataLogReader(std::unique_ptr<MemoryBuffer> buffer);
/** Returns true if the data log is valid (e.g. has a valid header). */
explicit operator bool() const { return IsValid(); }
/** Returns true if the data log is valid (e.g. has a valid header). */
bool IsValid() const;
/**
* Gets the data log version. Returns 0 if data log is invalid.
*
* @return Version number; most significant byte is major, least significant
* is minor (so version 1.0 will be 0x0100)
*/
uint16_t GetVersion() const;
/**
* Gets the extra header data.
*
* @return Extra header data
*/
std::string_view GetExtraHeader() const;
/**
* Gets the buffer identifier, typically the filename.
*
* @return Identifier string
*/
std::string_view GetBufferIdentifier() const {
return m_buf ? m_buf->GetBufferIdentifier() : "Invalid";
}
/** Returns iterator to first record. */
iterator begin() const;
/** Returns end iterator. */
iterator end() const { return DataLogIterator{this, SIZE_MAX}; }
private:
std::unique_ptr<MemoryBuffer> m_buf;
bool GetRecord(size_t* pos, DataLogRecord* out) const;
bool GetNextRecord(size_t* pos) const;
};
inline DataLogIterator& DataLogIterator::operator++() {
if (!m_reader->GetNextRecord(&m_pos)) {
m_pos = SIZE_MAX;
}
m_valid = false;
return *this;
}
inline DataLogIterator::reference DataLogIterator::operator*() const {
if (!m_valid) {
size_t pos = m_pos;
if (m_reader->GetRecord(&pos, &m_value)) {
m_valid = true;
}
}
return m_value;
}
} // namespace wpi::log

View File

@@ -0,0 +1,109 @@
// 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.
#pragma once
#include <atomic>
#include <functional>
#include <map>
#include <string>
#include <string_view>
#include <thread>
#include <utility>
#include <vector>
#include <wpi/DenseMap.h>
#include <wpi/Signal.h>
#include <wpi/mutex.h>
#include <wpi/struct/DynamicStruct.h>
#include "wpi/datalog/DataLogReader.h"
#ifndef NO_PROTOBUF
#include <wpi/protobuf/ProtobufMessageDatabase.h>
#endif
namespace wpi::log {
class DataLogReaderRange {
public:
DataLogReaderRange(wpi::log::DataLogReader::iterator begin,
wpi::log::DataLogReader::iterator end)
: m_begin{begin}, m_end{end} {}
wpi::log::DataLogReader::iterator begin() const { return m_begin; }
wpi::log::DataLogReader::iterator end() const { return m_end; }
wpi::log::DataLogReader::iterator m_begin;
wpi::log::DataLogReader::iterator m_end;
};
class DataLogReaderEntry : public wpi::log::StartRecordData {
public:
std::vector<DataLogReaderRange> ranges; // ranges where this entry is valid
};
class DataLogReaderThread {
public:
explicit DataLogReaderThread(wpi::log::DataLogReader reader)
: m_reader{std::move(reader)}, m_thread{[this] { ReadMain(); }} {}
~DataLogReaderThread();
bool IsDone() const { return m_done; }
std::string_view GetBufferIdentifier() const {
return m_reader.GetBufferIdentifier();
}
unsigned int GetNumRecords() const { return m_numRecords; }
unsigned int GetNumEntries() const {
std::scoped_lock lock{m_mutex};
return m_entriesByName.size();
}
// Passes Entry& to func
template <typename T>
void ForEachEntryName(T&& func) {
std::scoped_lock lock{m_mutex};
for (auto&& kv : m_entriesByName) {
func(kv.second);
}
}
const DataLogReaderEntry* GetEntry(std::string_view name) const {
std::scoped_lock lock{m_mutex};
auto it = m_entriesByName.find(name);
if (it == m_entriesByName.end()) {
return nullptr;
}
return &it->second;
}
wpi::StructDescriptorDatabase& GetStructDatabase() { return m_structDb; }
#ifndef NO_PROTOBUF
wpi::ProtobufMessageDatabase& GetProtobufDatabase() { return m_protoDb; }
#endif
const wpi::log::DataLogReader& GetReader() const { return m_reader; }
// note: these are called on separate thread
wpi::sig::Signal_mt<const DataLogReaderEntry&> sigEntryAdded;
wpi::sig::Signal_mt<> sigDone;
private:
void ReadMain();
wpi::log::DataLogReader m_reader;
mutable wpi::mutex m_mutex;
std::atomic_bool m_active{true};
std::atomic_bool m_done{false};
std::atomic<unsigned int> m_numRecords{0};
std::map<std::string, DataLogReaderEntry, std::less<>> m_entriesByName;
wpi::DenseMap<int, DataLogReaderEntry*> m_entriesById;
wpi::StructDescriptorDatabase m_structDb;
#ifndef NO_PROTOBUF
wpi::ProtobufMessageDatabase m_protoDb;
#endif
std::thread m_thread;
};
} // namespace wpi::log

View File

@@ -0,0 +1,97 @@
// 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.
#pragma once
#include <memory>
#include <string_view>
#include <system_error>
#include "wpi/datalog/DataLog.h"
namespace wpi {
class raw_ostream;
class Logger;
} // namespace wpi
namespace wpi::log {
/**
* A data log writer that flushes the data log to a file when Flush() is called.
*
* The lifetime of this object must be longer than any data log entry objects
* that refer to it.
*/
class DataLogWriter final : public DataLog {
public:
/**
* Constructs with a filename.
*
* @param filename filename to use
* @param ec error code if failed to open file (output)
* @param extraHeader extra header data
*/
explicit DataLogWriter(std::string_view filename, std::error_code& ec,
std::string_view extraHeader = "");
/**
* Construct with a filename.
*
* @param msglog message logger
* @param filename filename to use
* @param ec error code if failed to open file (output)
* @param extraHeader extra header data
*/
DataLogWriter(wpi::Logger& msglog, std::string_view filename,
std::error_code& ec, std::string_view extraHeader = "");
/**
* Constructs with an output stream.
*
* @param os output stream
* @param extraHeader extra header data
*/
explicit DataLogWriter(std::unique_ptr<wpi::raw_ostream> os,
std::string_view extraHeader = "");
/**
* Constructs with an output stream.
*
* @param msglog message logger
* @param os output stream
* @param extraHeader extra header data
*/
DataLogWriter(wpi::Logger& msglog, std::unique_ptr<wpi::raw_ostream> os,
std::string_view extraHeader = "");
~DataLogWriter() final;
DataLogWriter(const DataLogWriter&) = delete;
DataLogWriter& operator=(const DataLogWriter&) = delete;
DataLogWriter(DataLogWriter&&) = delete;
DataLogWriter& operator=(const DataLogWriter&&) = delete;
/**
* Flushes the log data to disk.
*/
void Flush() final;
/**
* Stops appending all records to the log, and closes the log file.
*/
void Stop() final;
/**
* Gets the output stream.
*
* @return output stream
*/
wpi::raw_ostream& GetStream() { return *m_os; }
private:
bool BufferFull() final;
std::unique_ptr<wpi::raw_ostream> m_os;
};
} // namespace wpi::log

View File

@@ -0,0 +1,313 @@
// 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.
#pragma once
#include <stddef.h> // NOLINT
#include <stdint.h>
#include <wpi/string.h>
#ifdef __cplusplus
extern "C" {
#endif
/** C-compatible data log (opaque struct). */
struct WPI_DataLog;
/**
* Construct a new Data Log.
*
* @param filename filename to use
* @param errorCode error if file failed to open (output)
* @param extraHeader extra header data
*/
struct WPI_DataLog* WPI_DataLog_CreateWriter(
const struct WPI_String* filename, int* errorCode,
const struct WPI_String* extraHeader);
/**
* Construct a new Data Log background writer. The log will be initially
* created with a temporary filename.
*
* @param dir directory to store the log
* @param filename filename to use; if none provided, a random filename is
* generated of the form "wpilog_{}.wpilog"
* @param period time between automatic flushes to disk, in seconds;
* this is a time/storage tradeoff
* @param extraHeader extra header data
*/
struct WPI_DataLog* WPI_DataLog_CreateBackgroundWriter(
const struct WPI_String* dir, const struct WPI_String* filename,
double period, const struct WPI_String* extraHeader);
/**
* Construct a new Data Log background writer that passes its output to the
* provided function rather than a file. The write function will be called on a
* separate background thread and may block. The write function is called with
* an empty data array (data=NULL, len=0) when the thread is terminating.
*
* @param write write function
* @param ptr pointer to pass to write function ptr parameter
* @param period time between automatic calls to write, in seconds;
* this is a time/storage tradeoff
* @param extraHeader extra header data
*/
struct WPI_DataLog* WPI_DataLog_CreateBackgroundWriter_Func(
void (*write)(void* ptr, const uint8_t* data, size_t len), void* ptr,
double period, const struct WPI_String* extraHeader);
/**
* Change log filename. Can only be used on background writer data logs.
*
* @param datalog data log
* @param filename filename
*/
void WPI_DataLog_SetBackgroundWriterFilename(struct WPI_DataLog* datalog,
const struct WPI_String* filename);
/**
* Releases a data log object. Closes the file and returns resources to the
* system.
*
* @param datalog data log
*/
void WPI_DataLog_Release(struct WPI_DataLog* datalog);
/**
* Explicitly flushes the log data to disk.
*
* @param datalog data log
*/
void WPI_DataLog_Flush(struct WPI_DataLog* datalog);
/**
* 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.
*
* @param datalog data log
*/
void WPI_DataLog_Pause(struct WPI_DataLog* datalog);
/**
* Resumes appending of data records to the log. If called after Stop(),
* opens a new file (with random name if SetFilename was not called after
* Stop()) and appends Start records and schema data values for all previously
* started entries and schemas.
*
* @param datalog data log
*/
void WPI_DataLog_Resume(struct WPI_DataLog* datalog);
/**
* Stops appending all records to the log, and closes the log file.
*
* @param datalog data log
*/
void WPI_DataLog_Stop(struct WPI_DataLog* datalog);
/**
* Start an entry. Duplicate names are allowed (with the same type), and
* result in the same index being returned (Start/Finish are reference
* counted). A duplicate name with a different type will result in an error
* message being printed to the console and 0 being returned (which will be
* ignored by the Append functions).
*
* @param datalog data log
* @param name Name
* @param type Data type
* @param metadata Initial metadata (e.g. data properties)
* @param timestamp Time stamp (may be 0 to indicate now)
*
* @return Entry index
*/
int WPI_DataLog_Start(struct WPI_DataLog* datalog,
const struct WPI_String* name,
const struct WPI_String* type,
const struct WPI_String* metadata, int64_t timestamp);
/**
* Finish an entry.
*
* @param datalog data log
* @param entry Entry index
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void WPI_DataLog_Finish(struct WPI_DataLog* datalog, int entry,
int64_t timestamp);
/**
* Updates the metadata for an entry.
*
* @param datalog data log
* @param entry Entry index
* @param metadata New metadata for the entry
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void WPI_DataLog_SetMetadata(struct WPI_DataLog* datalog, int entry,
const struct WPI_String* metadata,
int64_t timestamp);
/**
* Appends a raw record to the log.
*
* @param datalog data log
* @param entry Entry index, as returned by WPI_DataLog_Start()
* @param data Byte array to record
* @param len Length of byte array
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void WPI_DataLog_AppendRaw(struct WPI_DataLog* datalog, int entry,
const uint8_t* data, size_t len, int64_t timestamp);
/**
* Appends a boolean record to the log.
*
* @param datalog data log
* @param entry Entry index, as returned by WPI_DataLog_Start()
* @param value Boolean value to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void WPI_DataLog_AppendBoolean(struct WPI_DataLog* datalog, int entry,
int value, int64_t timestamp);
/**
* Appends an integer record to the log.
*
* @param datalog data log
* @param entry Entry index, as returned by WPI_DataLog_Start()
* @param value Integer value to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void WPI_DataLog_AppendInteger(struct WPI_DataLog* datalog, int entry,
int64_t value, int64_t timestamp);
/**
* Appends a float record to the log.
*
* @param datalog data log
* @param entry Entry index, as returned by WPI_DataLog_Start()
* @param value Float value to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void WPI_DataLog_AppendFloat(struct WPI_DataLog* datalog, int entry,
float value, int64_t timestamp);
/**
* Appends a double record to the log.
*
* @param datalog data log
* @param entry Entry index, as returned by WPI_DataLog_Start()
* @param value Double value to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void WPI_DataLog_AppendDouble(struct WPI_DataLog* datalog, int entry,
double value, int64_t timestamp);
/**
* Appends a string record to the log.
*
* @param datalog data log
* @param entry Entry index, as returned by WPI_DataLog_Start()
* @param value String value to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void WPI_DataLog_AppendString(struct WPI_DataLog* datalog, int entry,
const struct WPI_String* value,
int64_t timestamp);
/**
* Appends a boolean array record to the log.
*
* @param datalog data log
* @param entry Entry index, as returned by WPI_DataLog_Start()
* @param arr Boolean array to record
* @param len Number of elements in array
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void WPI_DataLog_AppendBooleanArray(struct WPI_DataLog* datalog, int entry,
const int* arr, size_t len,
int64_t timestamp);
/**
* Appends a boolean array record to the log.
*
* @param datalog data log
* @param entry Entry index, as returned by WPI_DataLog_Start()
* @param arr Boolean array to record
* @param len Number of elements in array
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void WPI_DataLog_AppendBooleanArrayByte(struct WPI_DataLog* datalog, int entry,
const uint8_t* arr, size_t len,
int64_t timestamp);
/**
* Appends an integer array record to the log.
*
* @param datalog data log
* @param entry Entry index, as returned by WPI_DataLog_Start()
* @param arr Integer array to record
* @param len Number of elements in array
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void WPI_DataLog_AppendIntegerArray(struct WPI_DataLog* datalog, int entry,
const int64_t* arr, size_t len,
int64_t timestamp);
/**
* Appends a float array record to the log.
*
* @param datalog data log
* @param entry Entry index, as returned by WPI_DataLog_Start()
* @param arr Float array to record
* @param len Number of elements in array
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void WPI_DataLog_AppendFloatArray(struct WPI_DataLog* datalog, int entry,
const float* arr, size_t len,
int64_t timestamp);
/**
* Appends a double array record to the log.
*
* @param datalog data log
* @param entry Entry index, as returned by WPI_DataLog_Start()
* @param arr Double array to record
* @param len Number of elements in array
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void WPI_DataLog_AppendDoubleArray(struct WPI_DataLog* datalog, int entry,
const double* arr, size_t len,
int64_t timestamp);
/**
* Appends a string array record to the log.
*
* @param datalog data log
* @param entry Entry index, as returned by WPI_DataLog_Start()
* @param arr String array to record
* @param len Number of elements in array
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void WPI_DataLog_AppendStringArray(struct WPI_DataLog* datalog, int entry,
const struct WPI_String* arr, size_t len,
int64_t timestamp);
void WPI_DataLog_AddSchemaString(struct WPI_DataLog* datalog,
const struct WPI_String* name,
const struct WPI_String* type,
const struct WPI_String* schema,
int64_t timestamp);
void WPI_DataLog_AddSchema(struct WPI_DataLog* datalog,
const struct WPI_String* name,
const struct WPI_String* type, const uint8_t* schema,
size_t schema_len, int64_t timestamp);
#ifdef __cplusplus
} // extern "C"
#endif

View File

@@ -0,0 +1,61 @@
// 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.
#pragma once
#include <functional>
#include <string_view>
#include <thread>
#include "wpi/datalog/DataLog.h"
namespace wpi::log {
/**
* A class version of `tail -f`, otherwise known as `tail -f` at home. Watches
* a file and puts the data somewhere else. Only works on Linux-based platforms.
*/
class FileLogger {
public:
FileLogger() = default;
/**
* Construct a FileLogger. When the specified file is modified, the callback
* will be called with the appended changes.
*
* @param file The path to the file.
* @param callback A callback that accepts the appended file data.
*/
FileLogger(std::string_view file,
std::function<void(std::string_view)> callback);
/**
* Construct a FileLogger. When the specified file is modified, appended data
* will be appended to the specified data log.
*
* @param file The path to the file.
* @param log A data log.
* @param key The log key to append data to.
*/
FileLogger(std::string_view file, log::DataLog& log, std::string_view key);
FileLogger(FileLogger&& other);
FileLogger& operator=(FileLogger&& rhs);
~FileLogger();
/**
* Creates a function that chunks incoming data into blocks of whole lines and
* stores incomplete lines to add to the next block of data.
*
* @param callback A callback that accepts the blocks of whole lines.
* @return The function.
*/
static std::function<void(std::string_view)> Buffer(
std::function<void(std::string_view)> callback);
private:
#ifdef __linux__
int m_fileHandle = -1;
int m_inotifyHandle = -1;
int m_inotifyWatchHandle = -1;
std::thread m_thread;
#endif
};
} // namespace wpi::log

View File

@@ -0,0 +1,147 @@
// 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 printlog;
import edu.wpi.first.datalog.DataLogReader;
import edu.wpi.first.datalog.DataLogRecord;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.InputMismatchException;
import java.util.List;
import java.util.Map;
public final class PrintLog {
private static final DateTimeFormatter m_timeFormatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
/** Main function. */
public static void main(String[] args) {
if (args.length != 1) {
System.err.println("Usage: printlog <file>");
System.exit(1);
return;
}
DataLogReader reader;
try {
reader = new DataLogReader(args[0]);
} catch (IOException ex) {
System.err.println("could not open file: " + ex.getMessage());
System.exit(1);
return;
}
if (!reader.isValid()) {
System.err.println("not a log file");
System.exit(1);
return;
}
Map<Integer, DataLogRecord.StartRecordData> entries = new HashMap<>();
for (DataLogRecord record : reader) {
if (record.isStart()) {
try {
DataLogRecord.StartRecordData data = record.getStartData();
System.out.println(
"Start("
+ data.entry
+ ", name='"
+ data.name
+ "', type='"
+ data.type
+ "', metadata='"
+ data.metadata
+ "') ["
+ (record.getTimestamp() / 1000000.0)
+ "]");
if (entries.containsKey(data.entry)) {
System.out.println("...DUPLICATE entry ID, overriding");
}
entries.put(data.entry, data);
} catch (InputMismatchException ex) {
System.out.println("Start(INVALID)");
}
} else if (record.isFinish()) {
try {
int entry = record.getFinishEntry();
System.out.println("Finish(" + entry + ") [" + (record.getTimestamp() / 1000000.0) + "]");
if (!entries.containsKey(entry)) {
System.out.println("...ID not found");
} else {
entries.remove(entry);
}
} catch (InputMismatchException ex) {
System.out.println("Finish(INVALID)");
}
} else if (record.isSetMetadata()) {
try {
DataLogRecord.MetadataRecordData data = record.getSetMetadataData();
System.out.println(
"SetMetadata("
+ data.entry
+ ", '"
+ data.metadata
+ "') ["
+ (record.getTimestamp() / 1000000.0)
+ "]");
if (!entries.containsKey(data.entry)) {
System.out.println("...ID not found");
}
} catch (InputMismatchException ex) {
System.out.println("SetMetadata(INVALID)");
}
} else if (record.isControl()) {
System.out.println("Unrecognized control record");
} else {
System.out.print("Data(" + record.getEntry() + ", size=" + record.getSize() + ") ");
DataLogRecord.StartRecordData entry = entries.get(record.getEntry());
if (entry == null) {
System.out.println("<ID not found>");
continue;
}
System.out.println(
"<name='"
+ entry.name
+ "', type='"
+ entry.type
+ "'> ["
+ (record.getTimestamp() / 1000000.0)
+ "]");
try {
// handle systemTime specially
if ("systemTime".equals(entry.name) && "int64".equals(entry.type)) {
long val = record.getInteger();
System.out.println(
" "
+ m_timeFormatter.format(
LocalDateTime.ofEpochSecond(val / 1000000, 0, ZoneOffset.UTC))
+ "."
+ String.format("%06d", val % 1000000));
continue;
}
switch (entry.type) {
case "double" -> System.out.println(" " + record.getDouble());
case "int64" -> System.out.println(" " + record.getInteger());
case "string", "json" -> System.out.println(" '" + record.getString() + "'");
case "boolean" -> System.out.println(" " + record.getBoolean());
case "double[]" -> System.out.println(" " + List.of(record.getDoubleArray()));
case "int64[]" -> System.out.println(" " + List.of(record.getIntegerArray()));
case "string[]" -> System.out.println(" " + List.of(record.getStringArray()));
default -> {
// NOP
}
}
} catch (InputMismatchException ex) {
System.out.println(" invalid");
}
}
}
}
private PrintLog() {}
}

View File

@@ -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.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<ImmutableThing> {
@Override
public Class<ImmutableThing> getTypeClass() {
return ImmutableThing.class;
}
@Override
public String getTypeName() {
return "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<CloneableThing> {
@Override
public Class<CloneableThing> getTypeClass() {
return CloneableThing.class;
}
@Override
public String getTypeName() {
return "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<Thing> {
@Override
public Class<Thing> getTypeClass() {
return Thing.class;
}
@Override
public String getTypeName() {
return "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<Thing> 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<Thing> 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<CloneableThing> 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<ImmutableThing> 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<Thing> 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<CloneableThing> 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<ImmutableThing> 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());
}
}

View File

@@ -0,0 +1,674 @@
// 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.
#include <array>
#include <memory>
#include <string>
#include <vector>
#include <gtest/gtest.h>
#include <wpi/Logger.h>
#include <wpi/raw_ostream.h>
#include "wpi/datalog/DataLogWriter.h"
namespace {
struct ThingA {
int x = 0;
};
inline bool operator==(const ThingA& a, const ThingA& b) {
return a.x == b.x;
}
struct ThingB {
int x = 0;
};
struct ThingC {
int x = 0;
};
struct Info1 {
int info = 0;
};
struct Info2 {
int info = 0;
};
} // namespace
template <>
struct wpi::Struct<ThingA> {
static constexpr std::string_view GetTypeName() { return "ThingA"; }
static constexpr size_t GetSize() { return 1; }
static constexpr std::string_view GetSchema() { return "uint8 value"; }
static ThingA Unpack(std::span<const uint8_t> data) {
return ThingA{.x = data[0]};
}
static void Pack(std::span<uint8_t> data, const ThingA& value) {
data[0] = value.x;
}
};
template <>
struct wpi::Struct<ThingB, Info1> {
static constexpr std::string_view GetTypeName(const Info1&) {
return "ThingB";
}
static constexpr size_t GetSize(const Info1&) { return 1; }
static constexpr std::string_view GetSchema(const Info1&) {
return "uint8 value";
}
static ThingB Unpack(std::span<const uint8_t> data, const Info1&) {
return ThingB{.x = data[0]};
}
static void Pack(std::span<uint8_t> data, const ThingB& value, const Info1&) {
data[0] = value.x;
}
};
template <>
struct wpi::Struct<ThingC> {
static constexpr std::string_view GetTypeName() { return "ThingC"; }
static constexpr size_t GetSize() { return 1; }
static constexpr std::string_view GetSchema() { return "uint8 value"; }
static ThingC Unpack(std::span<const uint8_t> data) {
return ThingC{.x = data[0]};
}
static void Pack(std::span<uint8_t> data, const ThingC& value) {
data[0] = value.x;
}
};
template <>
struct wpi::Struct<ThingC, Info1> {
static constexpr std::string_view GetTypeName(const Info1&) {
return "ThingC";
}
static constexpr size_t GetSize(const Info1&) { return 1; }
static constexpr std::string_view GetSchema(const Info1&) {
return "uint8 value";
}
static ThingC Unpack(std::span<const uint8_t> data, const Info1&) {
return ThingC{.x = data[0]};
}
static void Pack(std::span<uint8_t> data, const ThingC& value, const Info1&) {
data[0] = value.x;
}
};
template <>
struct wpi::Struct<ThingC, Info2> {
static constexpr std::string_view GetTypeName(const Info2&) {
return "ThingC";
}
static constexpr size_t GetSize(const Info2&) { return 1; }
static constexpr std::string_view GetSchema(const Info2&) {
return "uint8 value";
}
static ThingC Unpack(std::span<const uint8_t> data, const Info2&) {
return ThingC{.x = data[0]};
}
static void Pack(std::span<uint8_t> data, const ThingC& value, const Info2&) {
data[0] = value.x;
}
};
static_assert(wpi::StructSerializable<ThingA>);
static_assert(!wpi::StructSerializable<ThingA, Info1>);
static_assert(!wpi::StructSerializable<ThingB>);
static_assert(wpi::StructSerializable<ThingB, Info1>);
static_assert(!wpi::StructSerializable<ThingB, Info2>);
static_assert(wpi::StructSerializable<ThingC>);
static_assert(wpi::StructSerializable<ThingC, Info1>);
static_assert(wpi::StructSerializable<ThingC, Info2>);
class DataLogTest : public ::testing::Test {
public:
wpi::Logger msglog;
std::vector<uint8_t> data;
wpi::log::DataLogWriter log{msglog,
std::make_unique<wpi::raw_uvector_ostream>(data)};
};
TEST_F(DataLogTest, SimpleInt) {
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<uint8_t>{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<uint8_t>{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<uint8_t>{6, 6}));
entry.Update(std::span<const uint8_t>{}, 10);
log.Flush();
ASSERT_EQ(data.size(), 57u);
ASSERT_TRUE(entry.GetLastValue().has_value());
ASSERT_EQ(entry.GetLastValue().value(), std::vector<uint8_t>{});
}
TEST_F(DataLogTest, BooleanArrayAppendEmpty) {
wpi::log::BooleanArrayLogEntry entry{log, "a", 5};
entry.Append(std::span<const bool>{}, 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<int>{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<int>{true});
entry.Update(std::span<const bool>{}, 10);
log.Flush();
ASSERT_EQ(data.size(), 57u);
ASSERT_TRUE(entry.GetLastValue().has_value());
ASSERT_EQ(entry.GetLastValue().value(), std::vector<int>{});
}
TEST_F(DataLogTest, IntegerArrayAppendEmpty) {
wpi::log::IntegerArrayLogEntry entry{log, "a", 5};
entry.Append(std::span<const int64_t>{}, 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<int64_t>{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<int64_t>{2});
entry.Update(std::span<const int64_t>{}, 10);
log.Flush();
ASSERT_EQ(data.size(), 69u);
ASSERT_TRUE(entry.GetLastValue().has_value());
ASSERT_EQ(entry.GetLastValue().value(), std::vector<int64_t>{});
}
TEST_F(DataLogTest, DoubleArrayAppendEmpty) {
wpi::log::DoubleArrayLogEntry entry{log, "a", 5};
entry.Append(std::span<const double>{}, 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<double>{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<double>{2});
entry.Update(std::span<const double>{}, 10);
log.Flush();
ASSERT_EQ(data.size(), 70u);
ASSERT_TRUE(entry.GetLastValue().has_value());
ASSERT_EQ(entry.GetLastValue().value(), std::vector<double>{});
}
TEST_F(DataLogTest, FloatArrayAppendEmpty) {
wpi::log::FloatArrayLogEntry entry{log, "a", 5};
entry.Append(std::span<const float>{}, 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<float>{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<float>{2.0f});
entry.Update(std::span<const float>{}, 10);
log.Flush();
ASSERT_EQ(data.size(), 61u);
ASSERT_TRUE(entry.GetLastValue().has_value());
ASSERT_EQ(entry.GetLastValue().value(), std::vector<float>{});
}
TEST_F(DataLogTest, StringArrayAppendEmpty) {
wpi::log::StringArrayLogEntry entry{log, "a", 5};
entry.Append(std::span<const std::string>{}, 7);
entry.Append(std::span<const std::string_view>{}, 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<std::string>{"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<std::string>{"y"});
entry.Update(std::span<const std::string_view>{}, 10);
log.Flush();
ASSERT_EQ(data.size(), 76u);
ASSERT_TRUE(entry.GetLastValue().has_value());
ASSERT_EQ(entry.GetLastValue().value(), std::vector<std::string>{});
}
TEST_F(DataLogTest, StructA) {
[[maybe_unused]]
wpi::log::StructLogEntry<ThingA> entry0;
wpi::log::StructLogEntry<ThingA> entry{log, "a", 5};
entry.Append(ThingA{});
entry.Append(ThingA{}, 7);
}
TEST_F(DataLogTest, StructUpdate) {
wpi::log::StructLogEntry<ThingA> 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<ThingA> entry0;
wpi::log::StructArrayLogEntry<ThingA> entry{log, "a", 5};
entry.Append({{ThingA{}, ThingA{}}});
entry.Append({{ThingA{}, ThingA{}}}, 7);
}
TEST_F(DataLogTest, StructArrayUpdate) {
wpi::log::StructArrayLogEntry<ThingA> 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{}, 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{}, ThingA{.x = 2}}));
entry.Update(std::span<const ThingA>{}, 10);
log.Flush();
ASSERT_EQ(data.size(), 135u);
ASSERT_TRUE(entry.GetLastValue().has_value());
ASSERT_EQ(entry.GetLastValue().value(), std::vector<ThingA>{});
}
TEST_F(DataLogTest, StructFixedArrayA) {
[[maybe_unused]]
wpi::log::StructArrayLogEntry<std::array<ThingA, 2>> entry0;
wpi::log::StructLogEntry<std::array<ThingA, 2>> entry{log, "a", 5};
std::array<ThingA, 2> arr;
entry.Append(arr);
entry.Append(arr, 7);
}
TEST_F(DataLogTest, StructB) {
Info1 info;
[[maybe_unused]]
wpi::log::StructLogEntry<ThingB, Info1> entry0;
wpi::log::StructLogEntry<ThingB, Info1> entry{log, "b", info, 5};
entry.Append(ThingB{});
entry.Append(ThingB{}, 7);
}
TEST_F(DataLogTest, StructArrayB) {
Info1 info;
[[maybe_unused]]
wpi::log::StructArrayLogEntry<ThingB, Info1> entry0;
wpi::log::StructArrayLogEntry<ThingB, Info1> entry{log, "a", info, 5};
entry.Append({{ThingB{}, ThingB{}}});
entry.Append({{ThingB{}, ThingB{}}}, 7);
}
TEST_F(DataLogTest, StructFixedArrayB) {
Info1 info;
wpi::log::StructLogEntry<std::array<ThingB, 2>, Info1> entry{log, "a", info,
5};
std::array<ThingB, 2> arr;
entry.Append(arr);
entry.Append(arr, 7);
}
TEST_F(DataLogTest, StructC) {
{
wpi::log::StructLogEntry<ThingC> entry{log, "c", 5};
entry.Append(ThingC{});
entry.Append(ThingC{}, 7);
}
{
Info1 info;
wpi::log::StructLogEntry<ThingC, Info1> entry{log, "c1", info, 5};
entry.Append(ThingC{});
entry.Append(ThingC{}, 7);
}
{
Info2 info;
wpi::log::StructLogEntry<ThingC, Info2> entry{log, "c2", info, 5};
entry.Append(ThingC{});
entry.Append(ThingC{}, 7);
}
}
TEST_F(DataLogTest, StructArrayC) {
{
wpi::log::StructArrayLogEntry<ThingC> entry{log, "c", 5};
entry.Append({{ThingC{}, ThingC{}}});
entry.Append({{ThingC{}, ThingC{}}}, 7);
}
{
Info1 info;
wpi::log::StructArrayLogEntry<ThingC, Info1> entry{log, "c1", info, 5};
entry.Append({{ThingC{}, ThingC{}}});
entry.Append({{ThingC{}, ThingC{}}}, 7);
}
{
Info2 info;
wpi::log::StructArrayLogEntry<ThingC, Info2> entry{log, "c2", info, 5};
entry.Append({{ThingC{}, ThingC{}}});
entry.Append({{ThingC{}, ThingC{}}}, 7);
}
}
TEST_F(DataLogTest, StructFixedArrayC) {
std::array<ThingC, 2> arr;
{
wpi::log::StructLogEntry<std::array<ThingC, 2>> entry{log, "c", 5};
entry.Append(arr);
entry.Append(arr, 7);
}
{
Info1 info;
wpi::log::StructLogEntry<std::array<ThingC, 2>, Info1> entry{log, "c1",
info, 5};
entry.Append(arr);
entry.Append(arr, 7);
}
{
Info2 info;
wpi::log::StructLogEntry<std::array<ThingC, 2>, Info2> entry{log, "c2",
info, 5};
entry.Append(arr);
entry.Append(arr, 7);
}
}

View File

@@ -0,0 +1,59 @@
// 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.
#include <string>
#include <string_view>
#include <vector>
#include <gtest/gtest.h>
#include "wpi/datalog/FileLogger.h"
TEST(FileLoggerTest, BufferSingleLine) {
std::vector<std::string> buf;
auto func = wpi::log::FileLogger::Buffer(
[&buf](std::string_view line) { buf.emplace_back(line); });
func("qwertyuiop\n");
EXPECT_EQ("qwertyuiop", buf[0]);
}
TEST(FileLoggerTest, BufferMultiLine) {
std::vector<std::string> buf;
auto func = wpi::log::FileLogger::Buffer(
[&buf](std::string_view line) { buf.emplace_back(line); });
func("line 1\nline 2\nline 3\n");
EXPECT_EQ("line 1\nline 2\nline 3", buf[0]);
}
TEST(FileLoggerTest, BufferPartials) {
std::vector<std::string> buf;
auto func = wpi::log::FileLogger::Buffer(
[&buf](std::string_view line) { buf.emplace_back(line); });
func("part 1");
func("part 2\npart 3");
EXPECT_EQ("part 1part 2", buf[0]);
func("\n");
EXPECT_EQ("part 3", buf[1]);
}
TEST(FileLoggerTest, BufferMultiplePartials) {
std::vector<std::string> buf;
auto func = wpi::log::FileLogger::Buffer(
[&buf](std::string_view line) { buf.emplace_back(line); });
func("part 1");
func("part 2");
func("part 3");
func("part 4\n");
EXPECT_EQ("part 1part 2part 3part 4", buf[0]);
}
TEST(FileLoggerTest, BufferMultipleMultiLinePartials) {
std::vector<std::string> buf;
auto func = wpi::log::FileLogger::Buffer(
[&buf](std::string_view line) { buf.emplace_back(line); });
func("part 1");
func("part 2\npart 3");
func("part 4\n");
EXPECT_EQ("part 1part 2", buf[0]);
EXPECT_EQ("part 3part 4", buf[1]);
}

View File

@@ -0,0 +1,11 @@
// 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.
#include <gtest/gtest.h>
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
int ret = RUN_ALL_TESTS();
return ret;
}