SCRIPT Move java files

This commit is contained in:
PJ Reiniger
2025-11-07 19:55:40 -05:00
committed by Peter Johnson
parent 7ca1be9bae
commit c350c5f112
1486 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,41 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util;
import static java.util.Objects.requireNonNull;
/** Utility class for common WPILib error messages. */
public final class ErrorMessages {
/** Utility class, so constructor is private. */
private ErrorMessages() {
throw new UnsupportedOperationException("This is a utility class!");
}
/**
* Requires that a parameter of a method not be null; prints an error message with helpful
* debugging instructions if the parameter is null.
*
* @param <T> Type of object.
* @param obj The parameter that must not be null.
* @param paramName The name of the parameter.
* @param methodName The name of the method.
* @return The object parameter confirmed not to be null.
*/
public static <T> T requireNonNullParam(T obj, String paramName, String methodName) {
return requireNonNull(
obj,
"Parameter "
+ paramName
+ " in method "
+ methodName
+ " was null when it"
+ " should not have been! Check the stacktrace to find the responsible line of code - "
+ "usually, it is the first line of user-written code indicated in the stacktrace. "
+ "Make sure all objects passed to the method in question were properly initialized -"
+ " note that this may not be obvious if it is being called under "
+ "dynamically-changing conditions! Please do not seek additional technical assistance"
+ " without doing this first!");
}
}

View File

@@ -0,0 +1,54 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util;
/** Image pixel format. */
public enum PixelFormat {
/** Unknown format. */
kUnknown(0),
/** Motion-JPEG (compressed image data). */
kMJPEG(1),
/** YUY 4:2:2, 16 bpp. */
kYUYV(2),
/** RGB 5-6-5, 16 bpp. */
kRGB565(3),
/** BGR 8-8-8, 24 bpp. */
kBGR(4),
/** Grayscale, 8 bpp. */
kGray(5),
/** Grayscale, 16 bpp. */
kY16(6),
/** YUV 4:2:2, 16 bpp. */
kUYVY(7),
/** BGRA 8-8-8-8. 32 bpp. */
kBGRA(8);
private final int value;
PixelFormat(int value) {
this.value = value;
}
/**
* Gets the integer value of the pixel format.
*
* @return Integer value
*/
public int getValue() {
return value;
}
private static final PixelFormat[] s_values = values();
/**
* Gets a PixelFormat enum value from its integer value.
*
* @param pixelFormat integer value
* @return Enum value
*/
public static PixelFormat getFromInt(int pixelFormat) {
return s_values[pixelFormat];
}
}

View File

@@ -0,0 +1,226 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util;
import java.nio.ByteBuffer;
/**
* Class for storing raw frame data between image read call.
*
* <p>Data is reused for each frame read, rather then reallocating every frame.
*/
public class RawFrame implements AutoCloseable {
private long m_nativeObj;
private ByteBuffer m_data;
private int m_width;
private int m_height;
private int m_stride;
private PixelFormat m_pixelFormat = PixelFormat.kUnknown;
private long m_time;
private TimestampSource m_timeSource = TimestampSource.kUnknown;
/** Construct a new empty RawFrame. */
public RawFrame() {
m_nativeObj = WPIUtilJNI.allocateRawFrame();
}
/**
* Close the RawFrame, releasing native resources. Any images currently using the data will be
* invalidated.
*/
@Override
public void close() {
WPIUtilJNI.freeRawFrame(m_nativeObj);
m_nativeObj = 0;
}
/**
* Called from JNI to set data in class.
*
* @param data A native ByteBuffer pointing to the frame data.
* @param width The width of the frame, in pixels
* @param height The height of the frame, in pixels
* @param stride The number of bytes in each row of image data
* @param pixelFormat The PixelFormat of the frame
*/
void setDataJNI(
ByteBuffer data, int width, int height, int stride, int pixelFormat, long time, int timeSrc) {
m_data = data;
m_width = width;
m_height = height;
m_stride = stride;
m_pixelFormat = PixelFormat.getFromInt(pixelFormat);
m_time = time;
m_timeSource = TimestampSource.getFromInt(timeSrc);
}
/**
* Called from JNI to set info in class.
*
* @param width The width of the frame, in pixels
* @param height The height of the frame, in pixels
* @param stride The number of bytes in each row of image data
* @param pixelFormat The PixelFormat of the frame
*/
void setInfoJNI(int width, int height, int stride, int pixelFormat, long time, int timeSrc) {
m_width = width;
m_height = height;
m_stride = stride;
m_pixelFormat = PixelFormat.getFromInt(pixelFormat);
m_time = time;
m_timeSource = TimestampSource.getFromInt(timeSrc);
}
/**
* Set frame data.
*
* @param data A native ByteBuffer pointing to the frame data.
* @param width The width of the frame, in pixels
* @param height The height of the frame, in pixels
* @param stride The number of bytes in each row of image data
* @param pixelFormat The PixelFormat of the frame
*/
public void setData(ByteBuffer data, int width, int height, int stride, PixelFormat pixelFormat) {
if (!data.isDirect()) {
throw new UnsupportedOperationException("ByteBuffer must be direct");
}
m_data = data;
m_width = width;
m_height = height;
m_stride = stride;
m_pixelFormat = pixelFormat;
WPIUtilJNI.setRawFrameData(
m_nativeObj, data, data.limit(), width, height, stride, pixelFormat.getValue());
}
/**
* Call to set frame information.
*
* @param width The width of the frame, in pixels
* @param height The height of the frame, in pixels
* @param stride The number of bytes in each row of image data
* @param pixelFormat The PixelFormat of the frame
*/
public void setInfo(int width, int height, int stride, PixelFormat pixelFormat) {
m_width = width;
m_height = height;
m_stride = stride;
m_pixelFormat = pixelFormat;
WPIUtilJNI.setRawFrameInfo(
m_nativeObj,
m_data != null ? m_data.limit() : 0,
width,
height,
stride,
pixelFormat.getValue());
}
/**
* Update this frame's timestamp info.
*
* @param frameTime the time this frame was grabbed at. This uses the same time base as
* wpi::Now(), in us.
* @param frameTimeSource the time source for the timestamp this frame was grabbed at.
*/
public void setTimeInfo(long frameTime, TimestampSource frameTimeSource) {
m_time = frameTime;
m_timeSource = frameTimeSource;
WPIUtilJNI.setRawFrameTime(m_nativeObj, frameTime, frameTimeSource.getValue());
}
/**
* Get the pointer to native representation of this frame.
*
* @return The pointer to native representation of this frame.
*/
public long getNativeObj() {
return m_nativeObj;
}
/**
* Get a ByteBuffer pointing to the frame data. This ByteBuffer is backed by the frame directly.
* Its lifetime is controlled by the frame. If a new frame gets read, it will overwrite the
* current one.
*
* @return A ByteBuffer pointing to the frame data.
*/
public ByteBuffer getData() {
return m_data;
}
/**
* Get a long (is a uint8_t* in native code) pointing to the frame data. This pointer is backed by
* the frame directly. Its lifetime is controlled by the frame. If a new frame gets read, it will
* overwrite the current one.
*
* @return A long pointing to the frame data.
*/
public long getDataPtr() {
return WPIUtilJNI.getRawFrameDataPtr(m_nativeObj);
}
/**
* Get the total size of the data stored in the frame, in bytes.
*
* @return The total size of the data stored in the frame.
*/
public int getSize() {
return m_data != null ? m_data.limit() : 0;
}
/**
* Get the width of the image.
*
* @return The width of the image, in pixels.
*/
public int getWidth() {
return m_width;
}
/**
* Get the height of the image.
*
* @return The height of the image, in pixels.
*/
public int getHeight() {
return m_height;
}
/**
* Get the number of bytes in each row of image data.
*
* @return The image data stride, in bytes.
*/
public int getStride() {
return m_stride;
}
/**
* Get the PixelFormat of the frame.
*
* @return The PixelFormat of the frame.
*/
public PixelFormat getPixelFormat() {
return m_pixelFormat;
}
/**
* Get the time this frame was grabbed at. This uses the same time base as wpi::Now(), in us.
*
* @return Time in 1 us increments.
*/
public long getTimestamp() {
return m_time;
}
/**
* Get the time source for the timestamp this frame was grabbed at.
*
* @return Time source
*/
public TimestampSource getTimestampSource() {
return m_timeSource;
}
}

View File

@@ -0,0 +1,51 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util;
/**
* Options for where the timestamp an {@link RawFrame} was captured at can be measured relative to.
*/
public enum TimestampSource {
/** unknown. */
kUnknown(0),
/**
* wpi::Now when the new frame was dequeued by CSCore. Does not account for camera exposure time
* or V4L latency.
*/
kFrameDequeue(1),
/** End of Frame. Same as V4L2_BUF_FLAG_TSTAMP_SRC_EOF, translated into wpi::Now's timebase. */
kV4LEOF(2),
/**
* Start of Exposure. Same as V4L2_BUF_FLAG_TSTAMP_SRC_SOE, translated into wpi::Now's timebase.
*/
kV4LSOE(3);
private final int value;
TimestampSource(int value) {
this.value = value;
}
/**
* Gets the integer value of the pixel format.
*
* @return Integer value
*/
public int getValue() {
return value;
}
private static final TimestampSource[] s_values = values();
/**
* Gets a TimestampSource enum value from its integer value.
*
* @param timestampSource integer value
* @return Enum value
*/
public static TimestampSource getFromInt(int timestampSource) {
return s_values[timestampSource];
}
}

View File

@@ -0,0 +1,8 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util;
/** Marker interface to indicate a class is serializable using WPI serialization methods. */
public interface WPISerializable {}

View File

@@ -0,0 +1,227 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicBoolean;
/** WPIUtil JNI. */
public class WPIUtilJNI {
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("wpiutiljni");
} 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("wpiutiljni");
libraryLoaded = true;
}
/** Checks if the MSVC runtime is valid. Throws a runtime exception if not. */
public static native void checkMsvcRuntime();
/**
* Write the given string to stderr.
*
* @param str String to write.
*/
public static native void writeStderr(String str);
/** Enable mock time. */
public static native void enableMockTime();
/** Disable mock time. */
public static native void disableMockTime();
/**
* Set mock time.
*
* @param time The desired time in microseconds.
*/
public static native void setMockTime(long time);
/**
* Returns the time.
*
* @return The time.
*/
public static native long now();
/**
* Returns the system time.
*
* @return The system time.
*/
public static native long getSystemTime();
/**
* Creates an event. Events have binary state (signaled or not signaled) and may be either
* automatically reset or manually reset. Automatic-reset events go to non-signaled state when a
* WaitForObject is woken up by the event; manual-reset events require ResetEvent() to be called
* to set the event to non-signaled state; if ResetEvent() is not called, any waiter on that event
* will immediately wake when called.
*
* @param manualReset true for manual reset, false for automatic reset
* @param initialState true to make the event initially in signaled state
* @return Event handle
*/
public static native int createEvent(boolean manualReset, boolean initialState);
/**
* Destroys an event. Destruction wakes up any waiters.
*
* @param eventHandle event handle
*/
public static native void destroyEvent(int eventHandle);
/**
* Sets an event to signaled state.
*
* @param eventHandle event handle
*/
public static native void setEvent(int eventHandle);
/**
* Sets an event to non-signaled state.
*
* @param eventHandle event handle
*/
public static native void resetEvent(int eventHandle);
/**
* Creates a semaphore. Semaphores keep an internal counter. Releasing the semaphore increases the
* count. A semaphore with a non-zero count is considered signaled. When a waiter wakes up it
* atomically decrements the count by 1. This is generally useful in a single-supplier,
* multiple-consumer scenario.
*
* @param initialCount initial value for the semaphore's internal counter
* @param maximumCount maximum value for the semaphore's internal counter
* @return Semaphore handle
*/
public static native int createSemaphore(int initialCount, int maximumCount);
/**
* Destroys a semaphore. Destruction wakes up any waiters.
*
* @param semHandle semaphore handle
*/
public static native void destroySemaphore(int semHandle);
/**
* Releases N counts of a semaphore.
*
* @param semHandle semaphore handle
* @param releaseCount amount to add to semaphore's internal counter; must be positive
* @return True on successful release, false on failure (e.g. release count would exceed maximum
* value, or handle invalid)
*/
public static native boolean releaseSemaphore(int semHandle, int releaseCount);
static native long allocateRawFrame();
static native void freeRawFrame(long frame);
static native long getRawFrameDataPtr(long frame);
static native void setRawFrameData(
long frame, ByteBuffer data, int size, int width, int height, int stride, int pixelFormat);
static native void setRawFrameInfo(
long frame, int size, int width, int height, int stride, int pixelFormat);
static native void setRawFrameTime(long frame, long timestamp, int timeSource);
/**
* Waits for a handle to be signaled.
*
* @param handle handle to wait on
* @throws InterruptedException on failure (e.g. object was destroyed)
*/
public static native void waitForObject(int handle) throws InterruptedException;
/**
* Waits for a handle to be signaled, with timeout.
*
* @param handle handle to wait on
* @param timeout timeout in seconds
* @return True if timeout reached without handle being signaled
* @throws InterruptedException on failure (e.g. object was destroyed)
*/
public static native boolean waitForObjectTimeout(int handle, double timeout)
throws InterruptedException;
/**
* Waits for one or more handles to be signaled.
*
* <p>Invalid handles are treated as signaled; the returned array will have the handle error bit
* set for any invalid handles.
*
* @param handles array of handles to wait on
* @return array of signaled handles
* @throws InterruptedException on failure (e.g. no objects were signaled)
*/
public static native int[] waitForObjects(int[] handles) throws InterruptedException;
/**
* Waits for one or more handles to be signaled, with timeout.
*
* <p>Invalid handles are treated as signaled; the returned array will have the handle error bit
* set for any invalid handles.
*
* @param handles array of handles to wait on
* @param timeout timeout in seconds
* @return array of signaled handles; empty if timeout reached without any handle being signaled
* @throws InterruptedException on failure (e.g. no objects were signaled and no timeout)
*/
public static native int[] waitForObjectsTimeout(int[] handles, double timeout)
throws InterruptedException;
/** Utility class. */
protected WPIUtilJNI() {}
}

View File

@@ -0,0 +1,58 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util.cleanup;
import edu.wpi.first.util.ErrorMessages;
import java.util.ArrayDeque;
import java.util.Deque;
/**
* An object containing a Stack of AutoCloseable objects that are closed when this object is closed.
*/
public class CleanupPool implements AutoCloseable {
// Use a Deque instead of a Stack, as Stack's iterators go the wrong way, and docs
// state ArrayDeque is faster anyway.
private final Deque<AutoCloseable> m_closers = new ArrayDeque<>();
/** Default constructor. */
public CleanupPool() {}
/**
* Registers an object in the object stack for cleanup.
*
* @param <T> The object type
* @param object The object to register
* @return The registered object
*/
public <T extends AutoCloseable> T register(T object) {
ErrorMessages.requireNonNullParam(object, "object", "register");
m_closers.addFirst(object);
return object;
}
/**
* Removes an object from the cleanup stack.
*
* @param object the object to remove
*/
public void remove(AutoCloseable object) {
m_closers.remove(object);
}
/** Closes all objects in the stack. */
@Override
@SuppressWarnings("PMD.AvoidCatchingGenericException")
public void close() {
for (AutoCloseable autoCloseable : m_closers) {
try {
autoCloseable.close();
} catch (Exception e) {
// Swallow any exceptions on close
e.printStackTrace();
}
}
m_closers.clear();
}
}

View File

@@ -0,0 +1,51 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util.cleanup;
import java.lang.reflect.Field;
/**
* Implement this interface to have access to a `reflectionCleanup` method that can be called from
* your `close` method, that will use reflection to find all `AutoCloseable` instance members and
* close them.
*/
@SuppressWarnings("PMD.ImplicitFunctionalInterface")
public interface ReflectionCleanup extends AutoCloseable {
/**
* Default implementation that uses reflection to find all AutoCloseable fields not marked
* SkipCleanup and call close() on them. Call this from your `close()` method with the class level
* you want to close.
*
* @param cls the class level to clean up
*/
@SuppressWarnings("PMD.AvoidCatchingGenericException")
default void reflectionCleanup(Class<? extends ReflectionCleanup> cls) {
if (!cls.isAssignableFrom(getClass())) {
System.out.println("Passed in class is not assignable from \"this\"");
System.out.println("Expected something in the hierarchy of" + cls.getName());
System.out.println("This is " + getClass().getName());
return;
}
for (Field field : cls.getDeclaredFields()) {
if (field.isAnnotationPresent(SkipCleanup.class)) {
continue;
}
if (!AutoCloseable.class.isAssignableFrom(field.getType())) {
continue;
}
if (field.trySetAccessible()) {
try {
AutoCloseable c = (AutoCloseable) field.get(this);
if (c != null) {
c.close();
}
} catch (Exception e) {
// Ignore any exceptions
e.printStackTrace();
}
}
}
}
}

View File

@@ -0,0 +1,15 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util.cleanup;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/** Attribute for telling JVM to skip object cleanup. */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SkipCleanup {}

View File

@@ -0,0 +1,28 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util;
import java.lang.ref.Cleaner;
import java.lang.ref.Cleaner.Cleanable;
/** Cleaner object for WPILib objects. */
public final class WPICleaner {
private WPICleaner() {
throw new UnsupportedOperationException("This is a utility class!");
}
private static final Cleaner instance = Cleaner.create();
/**
* Register an object with the cleaner.
*
* @param object The object to register.
* @param runnable The runnable to call on cleanup.
* @return The registered Cleanable.
*/
public static Cleanable register(Object object, Runnable runnable) {
return instance.register(object, runnable);
}
}

View File

@@ -0,0 +1,70 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util.concurrent;
import edu.wpi.first.util.WPIUtilJNI;
/**
* An atomic signaling event for synchronization.
*
* <p>Events have binary state (signaled or not signaled) and may be either automatically reset or
* manually reset. Automatic-reset events go to non-signaled state when a waitForObject is woken up
* by the event; manual-reset events require reset() to be called to set the event to non-signaled
* state; if reset() is not called, any waiter on that event will immediately wake when called.
*/
public final class Event implements AutoCloseable {
/**
* Constructor.
*
* @param manualReset true for manual reset, false for automatic reset
* @param initialState true to make the event initially in signaled state
*/
public Event(boolean manualReset, boolean initialState) {
m_handle = WPIUtilJNI.createEvent(manualReset, initialState);
}
/**
* Constructor. Initial state is false.
*
* @param manualReset true for manual reset, false for automatic reset
*/
public Event(boolean manualReset) {
this(manualReset, false);
}
/** Constructor. Automatic reset, initial state is false. */
public Event() {
this(false, false);
}
@Override
public void close() {
if (m_handle != 0) {
WPIUtilJNI.destroyEvent(m_handle);
m_handle = 0;
}
}
/**
* Gets the event handle (e.g. for waitForObject).
*
* @return handle
*/
public int getHandle() {
return m_handle;
}
/** Sets the event to signaled state. */
public void set() {
WPIUtilJNI.setEvent(m_handle);
}
/** Sets the event to non-signaled state. */
public void reset() {
WPIUtilJNI.resetEvent(m_handle);
}
private int m_handle;
}

View File

@@ -0,0 +1,58 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
/** A thread-safe container for handling events. */
public class EventVector {
private final ReentrantLock m_lock = new ReentrantLock();
private final List<Integer> m_events = new ArrayList<>();
/** Default constructor. */
public EventVector() {}
/**
* Adds an event to the event vector.
*
* @param handle The event to add
*/
public void add(int handle) {
m_lock.lock();
try {
m_events.add(handle);
} finally {
m_lock.unlock();
}
}
/**
* Removes an event from the event vector.
*
* @param handle The event to remove
*/
public void remove(int handle) {
m_lock.lock();
try {
m_events.removeIf(x -> x == handle);
} finally {
m_lock.unlock();
}
}
/** Wakes up all events in the event vector. */
public void wakeup() {
m_lock.lock();
try {
for (int eventHandle : m_events) {
WPIUtilJNI.setEvent(eventHandle);
}
} finally {
m_lock.unlock();
}
}
}

View File

@@ -0,0 +1,80 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util.concurrent;
import edu.wpi.first.util.WPIUtilJNI;
/**
* A semaphore for synchronization.
*
* <p>Semaphores keep an internal counter. Releasing the semaphore increases the count. A semaphore
* with a non-zero count is considered signaled. When a waiter wakes up it atomically decrements the
* count by 1. This is generally useful in a single-supplier, multiple-consumer scenario.
*/
public final class Semaphore implements AutoCloseable {
/**
* Constructor.
*
* @param initialCount initial value for the semaphore's internal counter
* @param maximumCount maximum value for the semaphore's internal counter
*/
public Semaphore(int initialCount, int maximumCount) {
m_handle = WPIUtilJNI.createSemaphore(initialCount, maximumCount);
}
/**
* Constructor. Maximum count is Integer.MAX_VALUE.
*
* @param initialCount initial value for the semaphore's internal counter
*/
public Semaphore(int initialCount) {
this(initialCount, Integer.MAX_VALUE);
}
/** Constructor. Initial count is 0, maximum count is Integer.MAX_VALUE. */
public Semaphore() {
this(0, Integer.MAX_VALUE);
}
@Override
public void close() {
if (m_handle != 0) {
WPIUtilJNI.destroySemaphore(m_handle);
m_handle = 0;
}
}
/**
* Gets the semaphore handle (e.g. for waitForObject).
*
* @return handle
*/
public int getHandle() {
return m_handle;
}
/**
* Releases N counts of the semaphore.
*
* @param releaseCount amount to add to semaphore's internal counter; must be positive
* @return True on successful release, false on failure (e.g. release count would exceed maximum
* value, or handle invalid)
*/
public boolean release(int releaseCount) {
return WPIUtilJNI.releaseSemaphore(m_handle, releaseCount);
}
/**
* Releases 1 count of the semaphore.
*
* @return True on successful release, false on failure (e.g. release count would exceed maximum
* value, or handle invalid)
*/
public boolean release() {
return release(1);
}
private int m_handle;
}

View File

@@ -0,0 +1,199 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util;
/**
* This is a simple circular buffer so we don't need to "bucket brigade" copy old values.
*
* @param <T> Buffer element type.
*/
public class CircularBuffer<T> {
private T[] m_data;
// Index of element at front of buffer
private int m_front;
// Number of elements used in buffer
private int m_length;
/**
* Create a CircularBuffer with the provided size.
*
* @param size Maximum number of buffer elements.
*/
@SuppressWarnings("unchecked")
public CircularBuffer(int size) {
m_data = (T[]) new Object[size];
}
/**
* Returns number of elements in buffer.
*
* @return number of elements in buffer
*/
public int size() {
return m_length;
}
/**
* Get value at front of buffer.
*
* @return value at front of buffer
*/
public T getFirst() {
return m_data[m_front];
}
/**
* Get value at back of buffer.
*
* @return value at back of buffer
* @throws IndexOutOfBoundsException if the index is out of range (index &lt; 0 || index &gt;=
* size())
*/
public T getLast() {
// If there are no elements in the buffer, do nothing
if (m_length == 0) {
throw new IndexOutOfBoundsException("getLast() called on an empty container");
}
return m_data[(m_front + m_length - 1) % m_data.length];
}
/**
* Push new value onto front of the buffer. The value at the back is overwritten if the buffer is
* full.
*
* @param value The value to push.
*/
public void addFirst(T value) {
if (m_data.length == 0) {
return;
}
m_front = moduloDec(m_front);
m_data[m_front] = value;
if (m_length < m_data.length) {
m_length++;
}
}
/**
* Push new value onto back of the buffer. The value at the front is overwritten if the buffer is
* full.
*
* @param value The value to push.
*/
public void addLast(T value) {
if (m_data.length == 0) {
return;
}
m_data[(m_front + m_length) % m_data.length] = value;
if (m_length < m_data.length) {
m_length++;
} else {
// Increment front if buffer is full to maintain size
m_front = moduloInc(m_front);
}
}
/**
* Pop value at front of buffer.
*
* @return value at front of buffer
* @throws IndexOutOfBoundsException if the index is out of range (index &lt; 0 || index &gt;=
* size())
*/
public T removeFirst() {
// If there are no elements in the buffer, do nothing
if (m_length == 0) {
throw new IndexOutOfBoundsException("removeFirst() called on an empty container");
}
T temp = m_data[m_front];
m_front = moduloInc(m_front);
m_length--;
return temp;
}
/**
* Pop value at back of buffer.
*
* @return value at back of buffer
* @throws IndexOutOfBoundsException if the index is out of range (index &lt; 0 || index &gt;=
* size())
*/
public T removeLast() {
// If there are no elements in the buffer, do nothing
if (m_length == 0) {
throw new IndexOutOfBoundsException("removeLast() called on an empty container");
}
m_length--;
return m_data[(m_front + m_length) % m_data.length];
}
/**
* Resizes internal buffer to given size.
*
* <p>A new buffer is allocated because arrays are not resizable.
*
* @param size New buffer size.
*/
@SuppressWarnings("unchecked")
public void resize(int size) {
var newBuffer = (T[]) new Object[size];
m_length = Math.min(m_length, size);
for (int i = 0; i < m_length; i++) {
newBuffer[i] = m_data[(m_front + i) % m_data.length];
}
m_data = newBuffer;
m_front = 0;
}
/** Sets internal buffer contents to zero. */
public void clear() {
m_front = 0;
m_length = 0;
}
/**
* Get the element at the provided index relative to the start of the buffer.
*
* @param index Index into the buffer.
* @return Element at index starting from front of buffer.
*/
public T get(int index) {
return m_data[(m_front + index) % m_data.length];
}
/**
* Increment an index modulo the length of the buffer.
*
* @param index Index into the buffer.
* @return The incremented index.
*/
private int moduloInc(int index) {
return (index + 1) % m_data.length;
}
/**
* Decrement an index modulo the length of the buffer.
*
* @param index Index into the buffer.
* @return The decremented index.
*/
private int moduloDec(int index) {
if (index == 0) {
return m_data.length - 1;
} else {
return index - 1;
}
}
}

View File

@@ -0,0 +1,191 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util;
import java.util.Arrays;
/** This is a simple circular buffer so we don't need to "bucket brigade" copy old values. */
public class DoubleCircularBuffer {
private double[] m_data;
// Index of element at front of buffer
private int m_front;
// Number of elements used in buffer
private int m_length;
/**
* Create a CircularBuffer with the provided size.
*
* @param size The size of the circular buffer.
*/
public DoubleCircularBuffer(int size) {
m_data = new double[size];
Arrays.fill(m_data, 0.0);
}
/**
* Returns number of elements in buffer.
*
* @return number of elements in buffer
*/
public int size() {
return m_length;
}
/**
* Get value at front of buffer.
*
* @return value at front of buffer
*/
public double getFirst() {
return m_data[m_front];
}
/**
* Get value at back of buffer.
*
* @return value at back of buffer
*/
public double getLast() {
// If there are no elements in the buffer, do nothing
if (m_length == 0) {
return 0.0;
}
return m_data[(m_front + m_length - 1) % m_data.length];
}
/**
* Push new value onto front of the buffer. The value at the back is overwritten if the buffer is
* full.
*
* @param value The value to push.
*/
public void addFirst(double value) {
if (m_data.length == 0) {
return;
}
m_front = moduloDec(m_front);
m_data[m_front] = value;
if (m_length < m_data.length) {
m_length++;
}
}
/**
* Push new value onto back of the buffer. The value at the front is overwritten if the buffer is
* full.
*
* @param value The value to push.
*/
public void addLast(double value) {
if (m_data.length == 0) {
return;
}
m_data[(m_front + m_length) % m_data.length] = value;
if (m_length < m_data.length) {
m_length++;
} else {
// Increment front if buffer is full to maintain size
m_front = moduloInc(m_front);
}
}
/**
* Pop value at front of buffer.
*
* @return value at front of buffer
*/
public double removeFirst() {
// If there are no elements in the buffer, do nothing
if (m_length == 0) {
return 0.0;
}
double temp = m_data[m_front];
m_front = moduloInc(m_front);
m_length--;
return temp;
}
/**
* Pop value at back of buffer.
*
* @return value at back of buffer
*/
public double removeLast() {
// If there are no elements in the buffer, do nothing
if (m_length == 0) {
return 0.0;
}
m_length--;
return m_data[(m_front + m_length) % m_data.length];
}
/**
* Resizes internal buffer to given size.
*
* <p>A new buffer is allocated because arrays are not resizable.
*
* @param size New buffer size.
*/
public void resize(int size) {
double[] newBuffer = new double[size];
m_length = Math.min(m_length, size);
for (int i = 0; i < m_length; i++) {
newBuffer[i] = m_data[(m_front + i) % m_data.length];
}
m_data = newBuffer;
m_front = 0;
}
/** Sets internal buffer contents to zero. */
public void clear() {
Arrays.fill(m_data, 0.0);
m_front = 0;
m_length = 0;
}
/**
* Get the element at the provided index relative to the start of the buffer.
*
* @param index Index into the buffer.
* @return Element at index starting from front of buffer.
*/
public double get(int index) {
return m_data[(m_front + index) % m_data.length];
}
/**
* Increment an index modulo the length of the buffer.
*
* @param index Index into the buffer.
* @return The incremented index.
*/
private int moduloInc(int index) {
return (index + 1) % m_data.length;
}
/**
* Decrement an index modulo the length of the buffer.
*
* @param index Index into the buffer.
* @return The decremented index.
*/
private int moduloDec(int index) {
if (index == 0) {
return m_data.length - 1;
} else {
return index - 1;
}
}
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util.function;
/**
* Represents an operation that accepts a single boolean-valued argument and returns no result. This
* is the primitive type specialization of {@link java.util.function.Consumer} for boolean. Unlike
* most other functional interfaces, BooleanConsumer is expected to operate via side effects.
*
* <p>This is a functional interface whose functional method is {@link #accept(boolean)}.
*/
@FunctionalInterface
public interface BooleanConsumer {
/**
* Performs this operation on the given argument.
*
* @param value the input argument
*/
void accept(boolean value);
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util.function;
/**
* Represents an operation that accepts a single float-valued argument and returns no result. This
* is the primitive type specialization of {@link java.util.function.Consumer} for float. Unlike
* most other functional interfaces, BooleanConsumer is expected to operate via side effects.
*
* <p>This is a functional interface whose functional method is {@link #accept(float)}.
*/
@FunctionalInterface
public interface FloatConsumer {
/**
* Performs this operation on the given argument.
*
* @param value the input argument
*/
void accept(float value);
}

View File

@@ -0,0 +1,24 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util.function;
/**
* Represents a supplier of float-valued results.
*
* <p>This is the float-producing primitive specialization of {@link java.util.function.Supplier}.
*
* <p>There is no requirement that a distinct result be returned each time the supplier is invoked.
*
* <p>This is a functional interface whose functional method is {@link #getAsFloat()}.
*/
@FunctionalInterface
public interface FloatSupplier {
/**
* Gets a result.
*
* @return a result
*/
float getAsFloat();
}

View File

@@ -0,0 +1,212 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util.protobuf;
import java.lang.reflect.Array;
import java.util.function.BiConsumer;
import java.util.function.Predicate;
import us.hebi.quickbuf.Descriptors.Descriptor;
import us.hebi.quickbuf.Descriptors.FileDescriptor;
import us.hebi.quickbuf.ProtoMessage;
import us.hebi.quickbuf.RepeatedDouble;
import us.hebi.quickbuf.RepeatedMessage;
/**
* Interface for Protobuf serialization.
*
* <p>This is designed for serialization of more complex data structures including forward/backwards
* compatibility and repeated/nested/variable length members, etc. Serialization and deserialization
* code is auto-generated from .proto interface descriptions (the MessageType generic parameter).
*
* <p>Idiomatically, classes that support protobuf serialization should provide a static final
* member named "proto" that provides an instance of an implementation of this interface, or a
* static final method named "getProto" if the class is generic.
*
* @param <T> object type
* @param <MessageType> protobuf message type
*/
public interface Protobuf<T, MessageType extends ProtoMessage<?>> {
/**
* Gets the Class object for the stored value.
*
* @return Class
*/
Class<T> getTypeClass();
/**
* Gets the type string (e.g. for NetworkTables). This should be globally unique and start with
* "proto:".
*
* @return type string
*/
default String getTypeString() {
return "proto:" + getDescriptor().getFullName();
}
/**
* Gets the protobuf descriptor.
*
* @return descriptor
*/
Descriptor getDescriptor();
/**
* Creates protobuf message.
*
* @return protobuf message
*/
MessageType createMessage();
/**
* Deserializes an object from a protobuf message.
*
* @param msg protobuf message
* @return New object
*/
T unpack(MessageType msg);
/**
* Copies the object contents into a protobuf message. Implementations should call either
* msg.setMember(member) or member.copyToProto(msg.getMutableMember()) for each member.
*
* @param msg protobuf message
* @param value object to serialize
*/
void pack(MessageType msg, T value);
/**
* Updates the object contents from a protobuf message. Implementations should call
* msg.getMember(member), MemberClass.makeFromProto(msg.getMember()), or
* member.updateFromProto(msg.getMember()) for each member.
*
* <p>Immutable classes cannot and should not implement this function. The default implementation
* throws UnsupportedOperationException.
*
* @param out object to update
* @param msg protobuf message
* @throws UnsupportedOperationException if the object is immutable
*/
default void unpackInto(T out, MessageType msg) {
throw new UnsupportedOperationException("object does not support unpackInto");
}
/**
* Returns whether or not objects are immutable. Immutable objects must also be comparable using
* the equals() method. Default implementation returns false.
*
* @return True if object is immutable
*/
default boolean isImmutable() {
return false;
}
/**
* Returns whether or not objects are cloneable using the clone() method. Cloneable objects must
* also be comparable using the equals() method. Default implementation returns false.
*
* @return True if object is cloneable
*/
default boolean isCloneable() {
return false;
}
/**
* Creates a (deep) clone of the object. May also return the object directly if the object is
* immutable. Default implementation throws CloneNotSupportedException. Typically this should be
* implemented by implementing clone() on the object itself, and calling it from here.
*
* @param obj object to clone
* @return Clone of object (if immutable, may be same object)
* @throws CloneNotSupportedException if clone not supported
*/
default T clone(T obj) throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
/**
* Loops over all protobuf descriptors including nested/referenced descriptors.
*
* @param exists function that returns false if fn should be called for the given type string
* @param fn function to call for each descriptor
*/
default void forEachDescriptor(Predicate<String> exists, BiConsumer<String, byte[]> fn) {
forEachDescriptorImpl(getDescriptor().getFile(), exists, fn);
}
private static void forEachDescriptorImpl(
FileDescriptor desc, Predicate<String> exists, BiConsumer<String, byte[]> fn) {
String name = "proto:" + desc.getFullName();
if (exists.test(name)) {
return;
}
for (FileDescriptor dep : desc.getDependencies()) {
forEachDescriptorImpl(dep, exists, fn);
}
fn.accept(name, desc.toProtoBytes());
}
/**
* Unpack a serialized protobuf array message.
*
* @param <T> object type
* @param <MessageType> element type of the protobuf array
* @param msg protobuf array message
* @param proto protobuf implementation
* @return Deserialized array
*/
static <T, MessageType extends ProtoMessage<MessageType>> T[] unpackArray(
RepeatedMessage<MessageType> msg, Protobuf<T, MessageType> proto) {
@SuppressWarnings("unchecked")
T[] result = (T[]) Array.newInstance(proto.getTypeClass(), msg.length());
for (int i = 0; i < result.length; i++) {
result[i] = proto.unpack(msg.get(i));
}
return result;
}
/**
* Unpack a serialized protobuf double array message.
*
* @param msg protobuf double array message
* @return Deserialized array
*/
static double[] unpackArray(RepeatedDouble msg) {
double[] result = new double[msg.length()];
for (int i = 0; i < result.length; i++) {
result[i] = msg.get(i);
}
return result;
}
/**
* Pack a serialized protobuf array message.
*
* @param <T> object type
* @param <MessageType> element type of the protobuf array
* @param msg protobuf array message
* @param arr array of objects
* @param proto protobuf implementation
*/
static <T, MessageType extends ProtoMessage<MessageType>> void packArray(
RepeatedMessage<MessageType> msg, T[] arr, Protobuf<T, MessageType> proto) {
msg.clear();
msg.reserve(arr.length);
for (var obj : arr) {
proto.pack(msg.next(), obj);
}
}
/**
* Pack a serialized protobuf double array message.
*
* @param msg protobuf double array message
* @param arr array of objects
*/
static void packArray(RepeatedDouble msg, double[] arr) {
msg.clear();
msg.reserve(arr.length);
msg.addAll(arr);
}
}

View File

@@ -0,0 +1,172 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util.protobuf;
import java.io.IOException;
import java.nio.ByteBuffer;
import us.hebi.quickbuf.ProtoMessage;
import us.hebi.quickbuf.ProtoSink;
import us.hebi.quickbuf.ProtoSource;
/**
* Reusable buffer for serialization/deserialization to/from a protobuf.
*
* @param <T> object type
* @param <MessageType> protobuf message type
*/
public final class ProtobufBuffer<T, MessageType extends ProtoMessage<?>> {
private ProtobufBuffer(Protobuf<T, MessageType> proto) {
m_buf = ByteBuffer.allocateDirect(1024);
m_sink = ProtoSink.newDirectSink();
m_sink.setOutput(m_buf);
m_source = ProtoSource.newDirectSource();
m_msg = proto.createMessage();
m_proto = proto;
}
/**
* Creates a ProtobufBuffer for the given Protobuf object.
*
* @param <T> The type to serialize.
* @param <MessageType> The Protobuf message type.
* @param proto The Protobuf object.
* @return A ProtobufBuffer for the given Protobuf object.
*/
public static <T, MessageType extends ProtoMessage<?>> ProtobufBuffer<T, MessageType> create(
Protobuf<T, MessageType> proto) {
return new ProtobufBuffer<>(proto);
}
/**
* Gets the protobuf object of the stored type.
*
* @return protobuf object
*/
public Protobuf<T, MessageType> getProto() {
return m_proto;
}
/**
* Gets the type string.
*
* @return type string
*/
public String getTypeString() {
return m_proto.getTypeString();
}
/**
* Serializes a value to a ByteBuffer. The returned ByteBuffer is a direct byte buffer with the
* position set to the end of the serialized data.
*
* @param value value
* @return byte buffer
* @throws IOException if serialization failed
*/
public ByteBuffer write(T value) throws IOException {
m_msg.clearQuick();
m_proto.pack(m_msg, value);
int size = m_msg.getSerializedSize();
if (size > m_buf.capacity()) {
m_buf = ByteBuffer.allocateDirect(size * 2);
m_sink.setOutput(m_buf);
}
m_sink.reset();
m_msg.writeTo(m_sink);
m_buf.position(m_sink.getTotalBytesWritten());
return m_buf;
}
/**
* Deserializes a value from a byte array, creating a new object.
*
* @param buf byte array
* @param start starting location within byte array
* @param len length of serialized data
* @return new object
* @throws IOException if deserialization failed
*/
public T read(byte[] buf, int start, int len) throws IOException {
m_msg.clearQuick();
m_source.setInput(buf, start, len);
m_msg.mergeFrom(m_source);
return m_proto.unpack(m_msg);
}
/**
* Deserializes a value from a byte array, creating a new object.
*
* @param buf byte array
* @return new object
* @throws IOException if deserialization failed
*/
public T read(byte[] buf) throws IOException {
return read(buf, 0, buf.length);
}
/**
* Deserializes a value from a ByteBuffer, creating a new object.
*
* @param buf byte buffer
* @return new object
* @throws IOException if deserialization failed
*/
public T read(ByteBuffer buf) throws IOException {
m_msg.clearQuick();
m_source.setInput(buf);
m_msg.mergeFrom(m_source);
return m_proto.unpack(m_msg);
}
/**
* Deserializes a value from a byte array into a mutable object.
*
* @param out object (will be updated with deserialized contents)
* @param buf byte array
* @param start starting location within byte array
* @param len length of serialized data
* @throws IOException if deserialization failed
* @throws UnsupportedOperationException if the object is immutable
*/
public void readInto(T out, byte[] buf, int start, int len) throws IOException {
m_msg.clearQuick();
m_source.setInput(buf, start, len);
m_msg.mergeFrom(m_source);
m_proto.unpackInto(out, m_msg);
}
/**
* Deserializes a value from a byte array into a mutable object.
*
* @param out object (will be updated with deserialized contents)
* @param buf byte array
* @throws IOException if deserialization failed
* @throws UnsupportedOperationException if the object is immutable
*/
public void readInto(T out, byte[] buf) throws IOException {
readInto(out, buf, 0, buf.length);
}
/**
* Deserializes a value from a ByteBuffer into a mutable object.
*
* @param out object (will be updated with deserialized contents)
* @param buf byte buffer
* @throws IOException if deserialization failed
* @throws UnsupportedOperationException if the object is immutable
*/
public void readInto(T out, ByteBuffer buf) throws IOException {
m_msg.clearQuick();
m_source.setInput(buf);
m_msg.mergeFrom(m_source);
m_proto.unpackInto(out, m_msg);
}
private ByteBuffer m_buf;
private final ProtoSink m_sink;
private final ProtoSource m_source;
private final MessageType m_msg;
private final Protobuf<T, MessageType> m_proto;
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util.protobuf;
import edu.wpi.first.util.WPISerializable;
/**
* Marker interface to indicate a class is serializable using Protobuf serialization.
*
* <p>While this cannot be enforced by the interface, any class implementing this interface should
* provide a public final static `proto` member variable, or a static final `getProto()` method if
* the class is generic.
*/
public interface ProtobufSerializable extends WPISerializable {}

View File

@@ -0,0 +1,119 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Paths;
/**
* Loads classes by name. Can be used at any time, but is most commonly used to preload classes at
* the start of the program to avoid unpredictable delays due to lazy classloading later in program
* execution.
*/
public final class ClassPreloader {
private ClassPreloader() {}
/**
* Loads classes from an iterable.
*
* @param classNames iterable of class names
* @return Number of classes loaded.
*/
public static int preload(Iterable<String> classNames) {
int count = 0;
for (String i : classNames) {
try {
Class.forName(i);
count++;
} catch (ClassNotFoundException e) {
System.out.println("Could not preload " + i);
}
}
return count;
}
/**
* Loads classes.
*
* @param classNames array of class names
* @return Number of classes loaded.
*/
public static int preload(String... classNames) {
int count = 0;
for (String i : classNames) {
try {
Class.forName(i);
count++;
} catch (ClassNotFoundException e) {
System.out.println("Could not preload " + i);
}
}
return count;
}
/**
* Loads classes from a buffered reader. The input is expected to be one class name per line.
* Blank lines and lines starting with a semicolon are ignored.
*
* @param reader Reader
* @return Number of classes loaded.
*/
public static int preload(BufferedReader reader) {
int count = 0;
try {
String line = reader.readLine();
while (line != null) {
if (!line.isEmpty() && !line.startsWith(";")) {
try {
Class.forName(line);
count++;
} catch (ClassNotFoundException e) {
System.out.println("Could not preload " + line);
}
}
line = reader.readLine();
}
} catch (IOException e) {
System.out.println("Error when reading preload file: " + e);
}
return count;
}
/**
* Loads classes from an input stream. The input is expected to be one class name per line. Blank
* lines and lines starting with a semicolon are ignored.
*
* @param stream input stream
* @return Number of classes loaded.
*/
public static int preload(InputStream stream) {
return preload(new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8)));
}
/**
* Loads classes from a file. The input is expected to be one class name per line. Blank lines and
* lines starting with a semicolon are ignored.
*
* @param filename filename
* @return Number of classes loaded.
*/
public static int preloadFile(String filename) {
try (BufferedReader reader =
Files.newBufferedReader(Paths.get(filename), StandardCharsets.UTF_8)) {
return preload(reader);
} catch (NoSuchFileException e) {
System.out.println("Could not open preload file " + filename + ": " + e);
} catch (IOException e) {
System.out.println("Could not close preload file " + filename + ": " + e);
}
return 0;
}
}

View File

@@ -0,0 +1,226 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/** Loads dynamic libraries for all platforms. */
public final class CombinedRuntimeLoader {
private CombinedRuntimeLoader() {}
private static String extractionDirectory;
/**
* Returns library extraction directory.
*
* @return Library extraction directory.
*/
public static synchronized String getExtractionDirectory() {
return extractionDirectory;
}
private static synchronized void setExtractionDirectory(String directory) {
extractionDirectory = directory;
}
private static String defaultExtractionRoot;
/**
* Gets the default extraction root location (~/.wpilib/nativecache) for use if
* setExtractionDirectory is not set.
*
* @return The default extraction root location.
*/
public static synchronized String getDefaultExtractionRoot() {
if (defaultExtractionRoot != null) {
return defaultExtractionRoot;
}
String home = System.getProperty("user.home");
defaultExtractionRoot = Paths.get(home, ".wpilib", "nativecache").toString();
return defaultExtractionRoot;
}
/**
* Returns platform path.
*
* @return The current platform path.
* @throws IllegalStateException Thrown if the operating system is unknown.
*/
public static String getPlatformPath() {
String filePath;
String arch = System.getProperty("os.arch");
boolean intel32 = "x86".equals(arch) || "i386".equals(arch);
boolean intel64 = "amd64".equals(arch) || "x86_64".equals(arch);
if (System.getProperty("os.name").startsWith("Windows")) {
if (intel32) {
filePath = "/windows/x86/";
} else {
filePath = "/windows/x86-64/";
}
} else if (System.getProperty("os.name").startsWith("Mac")) {
filePath = "/osx/universal/";
} else if (System.getProperty("os.name").startsWith("Linux")) {
if (intel32) {
filePath = "/linux/x86/";
} else if (intel64) {
filePath = "/linux/x86-64/";
} else if (new File("/usr/local/frc/bin/frcRunRobot.sh").exists()) {
filePath = "/linux/athena/";
} else if ("arm".equals(arch) || "arm32".equals(arch)) {
filePath = "/linux/arm32/";
} else if ("aarch64".equals(arch) || "arm64".equals(arch)) {
filePath = "/linux/arm64/";
} else {
filePath = "/linux/nativearm/";
}
} else {
throw new IllegalStateException();
}
return filePath;
}
private static String getLoadErrorMessage(String libraryName, UnsatisfiedLinkError ule) {
StringBuilder msg = new StringBuilder(512);
msg.append(libraryName)
.append(" could not be loaded from path\n" + "\tattempted to load for platform ")
.append(getPlatformPath())
.append("\nLast Load Error: \n")
.append(ule.getMessage())
.append('\n');
if (System.getProperty("os.name").startsWith("Windows")) {
msg.append(
"A common cause of this error is missing the C++ runtime.\n"
+ "Download the latest at https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads\n");
}
return msg.toString();
}
/**
* Extract a list of native libraries.
*
* @param <T> The class where the resources would be located
* @param clazz The actual class object
* @param resourceName The resource name on the classpath to use for file lookup
* @return List of all libraries that were extracted
* @throws IOException Thrown if resource not found or file could not be extracted
*/
@SuppressWarnings("unchecked")
public static <T> List<String> extractLibraries(Class<T> clazz, String resourceName)
throws IOException {
TypeReference<HashMap<String, Object>> typeRef = new TypeReference<>() {};
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> map;
try (var stream = clazz.getResourceAsStream(resourceName)) {
map = mapper.readValue(stream, typeRef);
}
var platformPath = Paths.get(getPlatformPath());
var platform = platformPath.getName(0).toString();
var arch = platformPath.getName(1).toString();
var platformMap = (Map<String, List<String>>) map.get(platform);
var fileList = platformMap.get(arch);
var extractionPathString = getExtractionDirectory();
if (extractionPathString == null) {
String hash = (String) map.get("hash");
var defaultExtractionRoot = getDefaultExtractionRoot();
var extractionPath = Paths.get(defaultExtractionRoot, platform, arch, hash);
extractionPathString = extractionPath.toString();
setExtractionDirectory(extractionPathString);
}
List<String> extractedFiles = new ArrayList<>();
byte[] buffer = new byte[0x10000]; // 64K copy buffer
for (var file : fileList) {
try (var stream = clazz.getResourceAsStream(file)) {
Objects.requireNonNull(stream);
var outputFile = Paths.get(extractionPathString, new File(file).getName());
extractedFiles.add(outputFile.toString());
if (outputFile.toFile().exists()) {
continue;
}
var parent = outputFile.getParent();
if (parent == null) {
throw new IOException("Output file has no parent");
}
parent.toFile().mkdirs();
try (var os = Files.newOutputStream(outputFile)) {
int readBytes;
while ((readBytes = stream.read(buffer)) != -1) { // NOPMD
os.write(buffer, 0, readBytes);
}
}
}
}
return extractedFiles;
}
/**
* Load a single library from a list of extracted files.
*
* @param libraryName The library name to load
* @param extractedFiles The extracted files to search
* @throws IOException If library was not found
*/
public static void loadLibrary(String libraryName, List<String> extractedFiles)
throws IOException {
String currentPath = null;
try {
for (var extractedFile : extractedFiles) {
if (extractedFile.contains(libraryName)) {
// Load it
currentPath = extractedFile;
System.load(extractedFile);
return;
}
}
throw new IOException("Could not find library " + libraryName);
} catch (UnsatisfiedLinkError ule) {
throw new IOException(getLoadErrorMessage(currentPath, ule));
}
}
/**
* Load a list of native libraries out of a single directory.
*
* @param <T> The class where the resources would be located
* @param clazz The actual class object
* @param librariesToLoad List of libraries to load
* @throws IOException Throws an IOException if not found
*/
public static <T> void loadLibraries(Class<T> clazz, String... librariesToLoad)
throws IOException {
// Extract everything
var extractedFiles = extractLibraries(clazz, "/ResourceInformation.json");
for (var library : librariesToLoad) {
loadLibrary(library, extractedFiles);
}
}
}

View File

@@ -0,0 +1,51 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util;
/** Exception thrown due to a bad MSVC Runtime. */
public class MsvcRuntimeException extends RuntimeException {
private static final long serialVersionUID = -9155939328084105142L;
private static String generateMessage(
int foundMajor, int foundMinor, int expectedMajor, int expectedMinor, String runtimePath) {
String jvmLocation = ProcessHandle.current().info().command().orElse("Unknown");
StringBuilder builder = new StringBuilder(100);
builder
.append("Invalid MSVC Runtime Detected.\n")
.append(
String.format(
"Expected at least %d.%d, but found %d.%d\n",
expectedMajor, expectedMinor, foundMajor, foundMinor))
.append(String.format("JVM Location: %s\n", jvmLocation))
.append(String.format("Runtime DLL Location: %s\n", runtimePath))
.append("See https://wpilib.org/jvmruntime for more information\n");
return builder.toString();
}
/**
* Constructs a runtime exception.
*
* @param foundMajor found major
* @param foundMinor found minor
* @param expectedMajor expected major
* @param expectedMinor expected minor
* @param runtimePath path of runtime
*/
public MsvcRuntimeException(
int foundMajor, int foundMinor, int expectedMajor, int expectedMinor, String runtimePath) {
super(generateMessage(foundMajor, foundMinor, expectedMajor, expectedMinor, runtimePath));
}
/**
* Constructs a runtime exception.
*
* @param msg message
*/
public MsvcRuntimeException(String msg) {
super(msg);
}
}

View File

@@ -0,0 +1,197 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util;
import java.io.File;
/**
* A utility class for detecting and providing platform-specific such as OS and CPU architecture.
*
* @deprecated platform detection is brittle and may be removed in the future.
*/
@Deprecated(since = "2025", forRemoval = true)
public final class RuntimeDetector {
private static String filePrefix;
private static String fileExtension;
private static String filePath;
private static synchronized void computePlatform() {
if (fileExtension != null && filePath != null && filePrefix != null) {
return;
}
boolean intel32 = is32BitIntel();
boolean intel64 = is64BitIntel();
boolean arm64 = isArm64();
if (isWindows()) {
filePrefix = "";
fileExtension = ".dll";
if (intel32) {
filePath = "/windows/x86/";
} else {
filePath = "/windows/x86-64/";
}
} else if (isMac()) {
filePrefix = "lib";
fileExtension = ".dylib";
filePath = "/osx/universal/";
} else if (isLinux()) {
filePrefix = "lib";
fileExtension = ".so";
if (intel32) {
filePath = "/linux/x86/";
} else if (intel64) {
filePath = "/linux/x86-64/";
} else if (isAthena()) {
filePath = "/linux/athena/";
} else if (isArm32()) {
filePath = "/linux/arm32/";
} else if (arm64) {
filePath = "/linux/arm64/";
} else {
filePath = "/linux/nativearm/";
}
} else {
throw new IllegalStateException("Failed to determine OS");
}
}
/**
* Get the file prefix for the current system.
*
* @return The file prefix.
*/
public static synchronized String getFilePrefix() {
computePlatform();
return filePrefix;
}
/**
* Get the file extension for the current system.
*
* @return The file extension.
*/
public static synchronized String getFileExtension() {
computePlatform();
return fileExtension;
}
/**
* Get the platform path for the current system.
*
* @return The platform path.
*/
public static synchronized String getPlatformPath() {
computePlatform();
return filePath;
}
/**
* Get the path to the requested resource.
*
* @param libName Library name.
* @return The path to the requested resource.
*/
public static synchronized String getLibraryResource(String libName) {
computePlatform();
return filePath + filePrefix + libName + fileExtension;
}
/**
* Get the path to the hash to the requested resource.
*
* @param libName Library name.
* @return The path to the hash to the requested resource.
*/
public static synchronized String getHashLibraryResource(String libName) {
computePlatform();
return filePath + libName + ".hash";
}
/**
* Check if hardware platform is Athena.
*
* @return True if hardware platform is Athena.
*/
public static boolean isAthena() {
File runRobotFile = new File("/usr/local/frc/bin/frcRunRobot.sh");
return runRobotFile.exists();
}
/**
* Check if OS is Arm32.
*
* @return True if OS is Arm32.
*/
public static boolean isArm32() {
String arch = System.getProperty("os.arch");
return "arm".equals(arch) || "arm32".equals(arch);
}
/**
* Check if architecture is Arm64.
*
* @return if architecture is Arm64.
*/
public static boolean isArm64() {
String arch = System.getProperty("os.arch");
return "aarch64".equals(arch) || "arm64".equals(arch);
}
/**
* Check if OS is Linux.
*
* @return if OS is Linux.
*/
public static boolean isLinux() {
return System.getProperty("os.name").startsWith("Linux");
}
/**
* Check if OS is Windows.
*
* @return if OS is Windows.
*/
public static boolean isWindows() {
return System.getProperty("os.name").startsWith("Windows");
}
/**
* Check if OS is Mac.
*
* @return if OS is Mac.
*/
public static boolean isMac() {
return System.getProperty("os.name").startsWith("Mac");
}
/**
* Check if OS is 32bit Intel.
*
* @return if OS is 32bit Intel.
*/
public static boolean is32BitIntel() {
String arch = System.getProperty("os.arch");
return "x86".equals(arch) || "i386".equals(arch);
}
/**
* Check if OS is 64bit Intel.
*
* @return if OS is 64bit Intel.
*/
public static boolean is64BitIntel() {
String arch = System.getProperty("os.arch");
return "amd64".equals(arch) || "x86_64".equals(arch);
}
private RuntimeDetector() {}
}

View File

@@ -0,0 +1,54 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util;
import java.io.IOException;
/** Loads a native library at runtime. */
public final class RuntimeLoader {
/**
* Returns a load error message given the information in the provided UnsatisfiedLinkError.
*
* @param libraryName the name of the library that failed to load.
* @param ule UnsatisfiedLinkError object.
* @return A load error message.
*/
private static String getLoadErrorMessage(String libraryName, UnsatisfiedLinkError ule) {
String jvmLocation = ProcessHandle.current().info().command().orElse("Unknown");
StringBuilder msg = new StringBuilder(512);
msg.append(libraryName)
.append(" could not be loaded from path.\n" + "\tattempted to load for platform ")
.append(CombinedRuntimeLoader.getPlatformPath())
.append("\nLast Load Error: \n")
.append(ule.getMessage())
.append('\n')
.append(String.format("JVM Location: %s\n", jvmLocation));
if (System.getProperty("os.name").startsWith("Windows")) {
msg.append(
"A common cause of this error is using a JVM with an incorrect MSVC runtime.\n"
+ "Ensure you are using the WPILib JVM (The current running JVM is listed above)\n"
+ "See https://wpilib.org/jvmruntime for more information\n");
}
return msg.toString();
}
/**
* Loads a native library.
*
* @param libraryName the name of the library to load.
* @throws IOException if the library fails to load
*/
public static void loadLibrary(String libraryName) throws IOException {
try {
System.loadLibrary(libraryName);
} catch (UnsatisfiedLinkError ule) {
throw new IOException(getLoadErrorMessage(libraryName, ule));
}
}
private RuntimeLoader() {
throw new IllegalStateException("This class shouldn't be instantiated");
}
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util.sendable;
/** The base interface for objects that can be sent over the network. */
@SuppressWarnings("PMD.ImplicitFunctionalInterface")
public interface Sendable {
/**
* Initializes this {@link Sendable} object.
*
* @param builder sendable builder
*/
void initSendable(SendableBuilder builder);
}

View File

@@ -0,0 +1,261 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util.sendable;
import edu.wpi.first.util.function.BooleanConsumer;
import edu.wpi.first.util.function.FloatConsumer;
import edu.wpi.first.util.function.FloatSupplier;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.DoubleConsumer;
import java.util.function.DoubleSupplier;
import java.util.function.LongConsumer;
import java.util.function.LongSupplier;
import java.util.function.Supplier;
/** Helper class for building Sendable dashboard representations. */
public interface SendableBuilder extends AutoCloseable {
/** The backend kinds used for the sendable builder. */
enum BackendKind {
/** Unknown. */
kUnknown,
/** NetworkTables. */
kNetworkTables
}
/**
* Set the string representation of the named data type that will be used by the smart dashboard
* for this sendable.
*
* @param type data type
*/
void setSmartDashboardType(String type);
/**
* Set a flag indicating if this Sendable should be treated as an actuator. By default, this flag
* is false.
*
* @param value true if actuator, false if not
*/
void setActuator(boolean value);
/**
* Add a boolean property.
*
* @param key property name
* @param getter getter function (returns current value)
* @param setter setter function (sets new value)
*/
void addBooleanProperty(String key, BooleanSupplier getter, BooleanConsumer setter);
/**
* Add a constant boolean property.
*
* @param key property name
* @param value the value
*/
void publishConstBoolean(String key, boolean value);
/**
* Add an integer property.
*
* @param key property name
* @param getter getter function (returns current value)
* @param setter setter function (sets new value)
*/
void addIntegerProperty(String key, LongSupplier getter, LongConsumer setter);
/**
* Add a constant integer property.
*
* @param key property name
* @param value the value
*/
void publishConstInteger(String key, long value);
/**
* Add a float property.
*
* @param key property name
* @param getter getter function (returns current value)
* @param setter setter function (sets new value)
*/
void addFloatProperty(String key, FloatSupplier getter, FloatConsumer setter);
/**
* Add a constant float property.
*
* @param key property name
* @param value the value
*/
void publishConstFloat(String key, float value);
/**
* Add a double property.
*
* @param key property name
* @param getter getter function (returns current value)
* @param setter setter function (sets new value)
*/
void addDoubleProperty(String key, DoubleSupplier getter, DoubleConsumer setter);
/**
* Add a constant double property.
*
* @param key property name
* @param value the value
*/
void publishConstDouble(String key, double value);
/**
* Add a string property.
*
* @param key property name
* @param getter getter function (returns current value)
* @param setter setter function (sets new value)
*/
void addStringProperty(String key, Supplier<String> getter, Consumer<String> setter);
/**
* Add a constant string property.
*
* @param key property name
* @param value the value
*/
void publishConstString(String key, String value);
/**
* Add a boolean array property.
*
* @param key property name
* @param getter getter function (returns current value)
* @param setter setter function (sets new value)
*/
void addBooleanArrayProperty(String key, Supplier<boolean[]> getter, Consumer<boolean[]> setter);
/**
* Add a constant boolean array property.
*
* @param key property name
* @param value the value
*/
void publishConstBooleanArray(String key, boolean[] value);
/**
* Add an integer array property.
*
* @param key property name
* @param getter getter function (returns current value)
* @param setter setter function (sets new value)
*/
void addIntegerArrayProperty(String key, Supplier<long[]> getter, Consumer<long[]> setter);
/**
* Add a constant integer property.
*
* @param key property name
* @param value the value
*/
void publishConstIntegerArray(String key, long[] value);
/**
* Add a float array property.
*
* @param key property name
* @param getter getter function (returns current value)
* @param setter setter function (sets new value)
*/
void addFloatArrayProperty(String key, Supplier<float[]> getter, Consumer<float[]> setter);
/**
* Add a constant float array property.
*
* @param key property name
* @param value the value
*/
void publishConstFloatArray(String key, float[] value);
/**
* Add a double array property.
*
* @param key property name
* @param getter getter function (returns current value)
* @param setter setter function (sets new value)
*/
void addDoubleArrayProperty(String key, Supplier<double[]> getter, Consumer<double[]> setter);
/**
* Add a constant double array property.
*
* @param key property name
* @param value the value
*/
void publishConstDoubleArray(String key, double[] value);
/**
* Add a string array property.
*
* @param key property name
* @param getter getter function (returns current value)
* @param setter setter function (sets new value)
*/
void addStringArrayProperty(String key, Supplier<String[]> getter, Consumer<String[]> setter);
/**
* Add a constant string array property.
*
* @param key property name
* @param value the value
*/
void publishConstStringArray(String key, String[] value);
/**
* Add a raw property.
*
* @param key property name
* @param typeString type string
* @param getter getter function (returns current value)
* @param setter setter function (sets new value)
*/
void addRawProperty(
String key, String typeString, Supplier<byte[]> getter, Consumer<byte[]> setter);
/**
* Add a constant raw property.
*
* @param key property name
* @param typeString type string
* @param value the value
*/
void publishConstRaw(String key, String typeString, byte[] value);
/**
* Gets the kind of backend being used.
*
* @return Backend kind
*/
BackendKind getBackendKind();
/**
* Return whether this sendable has been published.
*
* @return True if it has been published, false if not.
*/
boolean isPublished();
/** Update the published values by calling the getters for all properties. */
void update();
/** Clear properties. */
void clearProperties();
/**
* Adds a closeable. The closeable.close() will be called when close() is called.
*
* @param closeable closeable object
*/
void addCloseable(AutoCloseable closeable);
}

View File

@@ -0,0 +1,354 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util.sendable;
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.Map;
import java.util.WeakHashMap;
/**
* The SendableRegistry class is the public interface for registering sensors and actuators for use
* on dashboards.
*/
@SuppressWarnings("PMD.AvoidCatchingGenericException")
public final class SendableRegistry {
private static class Component implements AutoCloseable {
Component() {}
Component(Sendable sendable) {
m_sendable = new WeakReference<>(sendable);
}
@Override
public void close() throws Exception {
m_builder.close();
for (AutoCloseable data : m_data) {
if (data != null) {
data.close();
}
}
}
WeakReference<Sendable> m_sendable;
SendableBuilder m_builder;
String m_name;
String m_subsystem = "Ungrouped";
AutoCloseable[] m_data;
void setName(String moduleType, int channel) {
m_name = moduleType + "[" + channel + "]";
}
void setName(String moduleType, int moduleNumber, int channel) {
m_name = moduleType + "[" + moduleNumber + "," + channel + "]";
}
}
private static final Map<Object, Component> components = new WeakHashMap<>();
private static int nextDataHandle;
private static Component getOrAdd(Sendable sendable) {
Component comp = components.get(sendable);
if (comp == null) {
comp = new Component(sendable);
components.put(sendable, comp);
} else {
if (comp.m_sendable == null) {
comp.m_sendable = new WeakReference<>(sendable);
}
}
return comp;
}
private SendableRegistry() {
throw new UnsupportedOperationException("This is a utility class!");
}
/**
* Adds an object to the registry.
*
* @param sendable object to add
* @param name component name
*/
public static synchronized void add(Sendable sendable, String name) {
Component comp = getOrAdd(sendable);
comp.m_name = name;
}
/**
* Adds an object to the registry.
*
* @param sendable object to add
* @param moduleType A string that defines the module name in the label for the value
* @param channel The channel number the device is plugged into
*/
public static synchronized void add(Sendable sendable, String moduleType, int channel) {
Component comp = getOrAdd(sendable);
comp.setName(moduleType, channel);
}
/**
* Adds an object to the registry.
*
* @param sendable object to add
* @param moduleType A string that defines the module name in the label for the value
* @param moduleNumber The number of the particular module type
* @param channel The channel number the device is plugged into
*/
public static synchronized void add(
Sendable sendable, String moduleType, int moduleNumber, int channel) {
Component comp = getOrAdd(sendable);
comp.setName(moduleType, moduleNumber, channel);
}
/**
* Adds an object to the registry.
*
* @param sendable object to add
* @param subsystem subsystem name
* @param name component name
*/
public static synchronized void add(Sendable sendable, String subsystem, String name) {
Component comp = getOrAdd(sendable);
comp.m_name = name;
comp.m_subsystem = subsystem;
}
/**
* Adds a child object to an object. Adds the child object to the registry if it's not already
* present.
*
* @param parent parent object
* @param child child object
*/
public static synchronized void addChild(Sendable parent, Object child) {
Component comp = components.get(child);
if (comp == null) {
comp = new Component();
components.put(child, comp);
}
// comp.m_parent = new WeakReference<>(parent);
}
/**
* Removes an object from the registry.
*
* @param sendable object to remove
* @return true if the object was removed; false if it was not present
*/
public static synchronized boolean remove(Sendable sendable) {
Component comp = components.remove(sendable);
if (comp != null) {
try {
comp.close();
} catch (Exception e) {
// ignore
}
}
return comp != null;
}
/**
* Determines if an object is in the registry.
*
* @param sendable object to check
* @return True if in registry, false if not.
*/
public static synchronized boolean contains(Sendable sendable) {
return components.containsKey(sendable);
}
/**
* Gets the name of an object.
*
* @param sendable object
* @return Name (empty if object is not in registry)
*/
public static synchronized String getName(Sendable sendable) {
Component comp = components.get(sendable);
if (comp == null) {
return "";
}
return comp.m_name;
}
/**
* Sets the name of an object.
*
* @param sendable object
* @param name name
*/
public static synchronized void setName(Sendable sendable, String name) {
Component comp = components.get(sendable);
if (comp != null) {
comp.m_name = name;
}
}
/**
* Sets the name of an object with a channel number.
*
* @param sendable object
* @param moduleType A string that defines the module name in the label for the value
* @param channel The channel number the device is plugged into
*/
public static synchronized void setName(Sendable sendable, String moduleType, int channel) {
Component comp = components.get(sendable);
if (comp != null) {
comp.setName(moduleType, channel);
}
}
/**
* Sets the name of an object with a module and channel number.
*
* @param sendable object
* @param moduleType A string that defines the module name in the label for the value
* @param moduleNumber The number of the particular module type
* @param channel The channel number the device is plugged into
*/
public static synchronized void setName(
Sendable sendable, String moduleType, int moduleNumber, int channel) {
Component comp = components.get(sendable);
if (comp != null) {
comp.setName(moduleType, moduleNumber, channel);
}
}
/**
* Sets both the subsystem name and device name of an object.
*
* @param sendable object
* @param subsystem subsystem name
* @param name device name
*/
public static synchronized void setName(Sendable sendable, String subsystem, String name) {
Component comp = components.get(sendable);
if (comp != null) {
comp.m_name = name;
comp.m_subsystem = subsystem;
}
}
/**
* Gets the subsystem name of an object.
*
* @param sendable object
* @return Subsystem name (empty if object is not in registry)
*/
public static synchronized String getSubsystem(Sendable sendable) {
Component comp = components.get(sendable);
if (comp == null) {
return "";
}
return comp.m_subsystem;
}
/**
* Sets the subsystem name of an object.
*
* @param sendable object
* @param subsystem subsystem name
*/
public static synchronized void setSubsystem(Sendable sendable, String subsystem) {
Component comp = components.get(sendable);
if (comp != null) {
comp.m_subsystem = subsystem;
}
}
/**
* Gets a unique handle for setting/getting data with setData() and getData().
*
* @return Handle
*/
public static synchronized int getDataHandle() {
return nextDataHandle++;
}
/**
* Associates arbitrary data with an object in the registry.
*
* @param sendable object
* @param handle data handle returned by getDataHandle()
* @param data data to set
* @return Previous data (may be null). If non-null, caller is responsible for calling close().
*/
@SuppressWarnings("PMD.CompareObjectsWithEquals")
public static synchronized AutoCloseable setData(
Sendable sendable, int handle, AutoCloseable data) {
Component comp = components.get(sendable);
if (comp == null) {
return null;
}
AutoCloseable rv = null;
if (comp.m_data == null) {
comp.m_data = new AutoCloseable[handle + 1];
} else if (handle < comp.m_data.length) {
rv = comp.m_data[handle];
} else {
comp.m_data = Arrays.copyOf(comp.m_data, handle + 1);
}
if (comp.m_data[handle] != data) {
if (comp.m_data[handle] != null) {
try {
comp.m_data[handle].close();
} catch (Exception e) {
// ignore
}
}
comp.m_data[handle] = data;
}
return rv;
}
/**
* Gets arbitrary data associated with an object in the registry.
*
* @param sendable object
* @param handle data handle returned by getDataHandle()
* @return data (may be null if none associated)
*/
public static synchronized Object getData(Sendable sendable, int handle) {
Component comp = components.get(sendable);
if (comp == null || comp.m_data == null || handle >= comp.m_data.length) {
return null;
}
return comp.m_data[handle];
}
/**
* Publishes an object in the registry to a builder.
*
* @param sendable object
* @param builder sendable builder
*/
public static synchronized void publish(Sendable sendable, SendableBuilder builder) {
Component comp = getOrAdd(sendable);
if (comp.m_builder != null) {
try {
comp.m_builder.close();
} catch (Exception e) {
// ignore
}
}
comp.m_builder = builder; // clear any current builder
sendable.initSendable(comp.m_builder);
comp.m_builder.update();
}
/**
* Updates network table information from an object.
*
* @param sendable object
*/
public static synchronized void update(Sendable sendable) {
Component comp = components.get(sendable);
if (comp != null && comp.m_builder != null) {
comp.m_builder.update();
}
}
}

View File

@@ -0,0 +1,79 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util.struct;
/** Exception thrown when encountering a bad schema. */
public class BadSchemaException extends Exception {
/** The bad schema field. */
private final String m_field;
/**
* Constructs a BadSchemaException.
*
* @param message the detail message.
*/
public BadSchemaException(String message) {
super(message);
m_field = "";
}
/**
* Constructs a BadSchemaException.
*
* @param message the detail message.
* @param cause The cause (which is saved for later retrieval by the {@link #getCause()} method).
*/
public BadSchemaException(String message, Throwable cause) {
super(message, cause);
m_field = "";
}
/**
* Constructs a BadSchemaException.
*
* @param cause The cause (which is saved for later retrieval by the {@link #getCause()} method).
*/
public BadSchemaException(Throwable cause) {
super(cause);
m_field = "";
}
/**
* Constructs a BadSchemaException.
*
* @param field The bad schema field.
* @param message the detail message.
*/
public BadSchemaException(String field, String message) {
super(message);
m_field = field;
}
/**
* Constructs a BadSchemaException.
*
* @param field The bad schema field.
* @param message the detail message.
* @param cause The cause (which is saved for later retrieval by the {@link #getCause()} method).
*/
public BadSchemaException(String field, String message, Throwable cause) {
super(message, cause);
m_field = field;
}
/**
* Gets the name of the bad schema field.
*
* @return The name of the bad schema field, or an empty string if not applicable.
*/
public String getField() {
return m_field;
}
@Override
public String getMessage() {
return m_field.isEmpty() ? super.getMessage() : "field " + m_field + ": " + super.getMessage();
}
}

View File

@@ -0,0 +1,658 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util.struct;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ReadOnlyBufferException;
import java.nio.charset.StandardCharsets;
/** Dynamic (run-time) access to a serialized raw struct. */
public final class DynamicStruct {
private DynamicStruct(StructDescriptor desc, ByteBuffer data) {
m_desc = desc;
m_data = data.order(ByteOrder.LITTLE_ENDIAN);
}
/**
* Constructs a new dynamic struct object with internal storage. The descriptor must be valid. The
* internal storage is allocated using ByteBuffer.allocate().
*
* @param desc struct descriptor
* @return dynamic struct object
* @throws IllegalStateException if struct descriptor is invalid
*/
public static DynamicStruct allocate(StructDescriptor desc) {
return new DynamicStruct(desc, ByteBuffer.allocate(desc.getSize()));
}
/**
* Constructs a new dynamic struct object with internal storage. The descriptor must be valid. The
* internal storage is allocated using ByteBuffer.allocateDirect().
*
* @param desc struct descriptor
* @return dynamic struct object
* @throws IllegalStateException if struct descriptor is invalid
*/
public static DynamicStruct allocateDirect(StructDescriptor desc) {
return new DynamicStruct(desc, ByteBuffer.allocateDirect(desc.getSize()));
}
/**
* Constructs a new dynamic struct object. Note: the passed data buffer is not copied.
* Modifications to the passed buffer will be reflected in the struct and vice-versa.
*
* @param desc struct descriptor
* @param data byte buffer containing serialized data starting at current position
* @return dynamic struct object
*/
public static DynamicStruct wrap(StructDescriptor desc, ByteBuffer data) {
return new DynamicStruct(desc, data.slice());
}
/**
* Gets the struct descriptor.
*
* @return struct descriptor
*/
public StructDescriptor getDescriptor() {
return m_desc;
}
/**
* Gets the serialized backing data buffer.
*
* @return data buffer
*/
public ByteBuffer getBuffer() {
return m_data.duplicate().position(0);
}
/**
* Overwrites the entire serialized struct by copying data from a byte array.
*
* @param data replacement data for the struct
* @throws BufferUnderflowException if data is smaller than the struct size
* @throws ReadOnlyBufferException if the underlying buffer is read-only
* @throws IllegalStateException if struct descriptor is invalid
*/
public void setData(byte[] data) {
if (data.length < m_desc.getSize()) {
throw new BufferUnderflowException();
}
m_data.position(0).put(data);
}
/**
* Overwrites the entire serialized struct by copying data from a byte buffer.
*
* @param data replacement data for the struct; copy starts from current position
* @throws BufferUnderflowException if remaining data is smaller than the struct size
* @throws ReadOnlyBufferException if the underlying buffer is read-only
* @throws IllegalStateException if struct descriptor is invalid
*/
public void setData(ByteBuffer data) {
if (data.remaining() < m_desc.getSize()) {
throw new BufferUnderflowException();
}
int oldLimit = data.limit();
m_data.position(0).put(data.limit(m_desc.getSize()));
data.limit(oldLimit);
}
/**
* Gets a struct field descriptor by name.
*
* @param name field name
* @return field descriptor, or null if no field with that name exists
*/
public StructFieldDescriptor findField(String name) {
return m_desc.findFieldByName(name);
}
/**
* Gets the value of a boolean field.
*
* @param field field descriptor
* @param arrIndex array index (must be less than field array size)
* @return boolean field value
* @throws UnsupportedOperationException if field is not bool type
* @throws IllegalArgumentException if field is not a member of this struct
* @throws IllegalStateException if struct descriptor is invalid
* @throws ArrayIndexOutOfBoundsException if array index is out of bounds
*/
public boolean getBoolField(StructFieldDescriptor field, int arrIndex) {
if (field.getType() != StructFieldType.kBool) {
throw new UnsupportedOperationException("field is not bool type");
}
return getFieldImpl(field, arrIndex) != 0;
}
/**
* Gets the value of a boolean field.
*
* @param field field descriptor
* @return boolean field value
* @throws UnsupportedOperationException if field is not bool type
* @throws IllegalArgumentException if field is not a member of this struct
* @throws IllegalStateException if struct descriptor is invalid
*/
public boolean getBoolField(StructFieldDescriptor field) {
return getBoolField(field, 0);
}
/**
* Sets the value of a boolean field.
*
* @param field field descriptor
* @param value boolean value
* @param arrIndex array index (must be less than field array size)
* @throws UnsupportedOperationException if field is not bool type
* @throws IllegalArgumentException if field is not a member of this struct
* @throws IllegalStateException if struct descriptor is invalid
* @throws ArrayIndexOutOfBoundsException if array index is out of bounds
* @throws ReadOnlyBufferException if the underlying buffer is read-only
*/
public void setBoolField(StructFieldDescriptor field, boolean value, int arrIndex) {
if (field.getType() != StructFieldType.kBool) {
throw new UnsupportedOperationException("field is not bool type");
}
setFieldImpl(field, value ? 1 : 0, arrIndex);
}
/**
* Sets the value of a boolean field.
*
* @param field field descriptor
* @param value boolean value
* @throws UnsupportedOperationException if field is not bool type
* @throws IllegalArgumentException if field is not a member of this struct
* @throws IllegalStateException if struct descriptor is invalid
* @throws ReadOnlyBufferException if the underlying buffer is read-only
*/
public void setBoolField(StructFieldDescriptor field, boolean value) {
setBoolField(field, value, 0);
}
/**
* Gets the value of an integer field.
*
* @param field field descriptor
* @param arrIndex array index (must be less than field array size)
* @return integer field value
* @throws UnsupportedOperationException if field is not integer type
* @throws IllegalArgumentException if field is not a member of this struct
* @throws IllegalStateException if struct descriptor is invalid
* @throws ArrayIndexOutOfBoundsException if array index is out of bounds
*/
public long getIntField(StructFieldDescriptor field, int arrIndex) {
if (!field.isInt() && !field.isUint()) {
throw new UnsupportedOperationException("field is not integer type");
}
return getFieldImpl(field, arrIndex);
}
/**
* Gets the value of an integer field.
*
* @param field field descriptor
* @return integer field value
* @throws UnsupportedOperationException if field is not integer type
* @throws IllegalArgumentException if field is not a member of this struct
* @throws IllegalStateException if struct descriptor is invalid
*/
public long getIntField(StructFieldDescriptor field) {
return getIntField(field, 0);
}
/**
* Sets the value of an integer field.
*
* @param field field descriptor
* @param value integer value
* @param arrIndex array index (must be less than field array size)
* @throws UnsupportedOperationException if field is not integer type
* @throws IllegalArgumentException if field is not a member of this struct
* @throws IllegalStateException if struct descriptor is invalid
* @throws ArrayIndexOutOfBoundsException if array index is out of bounds
* @throws ReadOnlyBufferException if the underlying buffer is read-only
*/
public void setIntField(StructFieldDescriptor field, long value, int arrIndex) {
if (!field.isInt() && !field.isUint()) {
throw new UnsupportedOperationException("field is not integer type");
}
setFieldImpl(field, value, arrIndex);
}
/**
* Sets the value of an integer field.
*
* @param field field descriptor
* @param value integer value
* @throws UnsupportedOperationException if field is not integer type
* @throws IllegalArgumentException if field is not a member of this struct
* @throws IllegalStateException if struct descriptor is invalid
* @throws ReadOnlyBufferException if the underlying buffer is read-only
*/
public void setIntField(StructFieldDescriptor field, long value) {
setIntField(field, value, 0);
}
/**
* Gets the value of a float field.
*
* @param field field descriptor
* @param arrIndex array index (must be less than field array size)
* @return float field value
* @throws UnsupportedOperationException if field is not float type
* @throws IllegalArgumentException if field is not a member of this struct
* @throws IllegalStateException if struct descriptor is invalid
* @throws ArrayIndexOutOfBoundsException if array index is out of bounds
*/
public float getFloatField(StructFieldDescriptor field, int arrIndex) {
if (field.getType() != StructFieldType.kFloat) {
throw new UnsupportedOperationException("field is not float type");
}
return Float.intBitsToFloat((int) getFieldImpl(field, arrIndex));
}
/**
* Gets the value of a float field.
*
* @param field field descriptor
* @return float field value
* @throws UnsupportedOperationException if field is not float type
* @throws IllegalArgumentException if field is not a member of this struct
* @throws IllegalStateException if struct descriptor is invalid
*/
public float getFloatField(StructFieldDescriptor field) {
return getFloatField(field, 0);
}
/**
* Sets the value of a float field.
*
* @param field field descriptor
* @param value float value
* @param arrIndex array index (must be less than field array size)
* @throws UnsupportedOperationException if field is not float type
* @throws IllegalArgumentException if field is not a member of this struct
* @throws IllegalStateException if struct descriptor is invalid
* @throws ArrayIndexOutOfBoundsException if array index is out of bounds
* @throws ReadOnlyBufferException if the underlying buffer is read-only
*/
public void setFloatField(StructFieldDescriptor field, float value, int arrIndex) {
if (field.getType() != StructFieldType.kFloat) {
throw new UnsupportedOperationException("field is not float type");
}
setFieldImpl(field, Float.floatToIntBits(value), arrIndex);
}
/**
* Sets the value of a float field.
*
* @param field field descriptor
* @param value float value
* @throws UnsupportedOperationException if field is not float type
* @throws IllegalArgumentException if field is not a member of this struct
* @throws IllegalStateException if struct descriptor is invalid
* @throws ReadOnlyBufferException if the underlying buffer is read-only
*/
public void setFloatField(StructFieldDescriptor field, float value) {
setFloatField(field, value, 0);
}
/**
* Gets the value of a double field.
*
* @param field field descriptor
* @param arrIndex array index (must be less than field array size)
* @return double field value
* @throws UnsupportedOperationException if field is not double type
* @throws IllegalArgumentException if field is not a member of this struct
* @throws IllegalStateException if struct descriptor is invalid
* @throws ArrayIndexOutOfBoundsException if array index is out of bounds
*/
public double getDoubleField(StructFieldDescriptor field, int arrIndex) {
if (field.getType() != StructFieldType.kDouble) {
throw new UnsupportedOperationException("field is not double type");
}
return Double.longBitsToDouble(getFieldImpl(field, arrIndex));
}
/**
* Gets the value of a double field.
*
* @param field field descriptor
* @return double field value
* @throws UnsupportedOperationException if field is not double type
* @throws IllegalArgumentException if field is not a member of this struct
* @throws IllegalStateException if struct descriptor is invalid
*/
public double getDoubleField(StructFieldDescriptor field) {
return getDoubleField(field, 0);
}
/**
* Sets the value of a double field.
*
* @param field field descriptor
* @param value double value
* @param arrIndex array index (must be less than field array size)
* @throws UnsupportedOperationException if field is not double type
* @throws IllegalArgumentException if field is not a member of this struct
* @throws IllegalStateException if struct descriptor is invalid
* @throws ArrayIndexOutOfBoundsException if array index is out of bounds
* @throws ReadOnlyBufferException if the underlying buffer is read-only
*/
public void setDoubleField(StructFieldDescriptor field, double value, int arrIndex) {
if (field.getType() != StructFieldType.kDouble) {
throw new UnsupportedOperationException("field is not double type");
}
setFieldImpl(field, Double.doubleToLongBits(value), arrIndex);
}
/**
* Sets the value of a double field.
*
* @param field field descriptor
* @param value double value
* @throws UnsupportedOperationException if field is not double type
* @throws IllegalArgumentException if field is not a member of this struct
* @throws IllegalStateException if struct descriptor is invalid
* @throws ReadOnlyBufferException if the underlying buffer is read-only
*/
public void setDoubleField(StructFieldDescriptor field, double value) {
setDoubleField(field, value, 0);
}
/**
* Gets the value of a character or character array field.
*
* @param field field descriptor
* @return field value
* @throws UnsupportedOperationException if field is not char type
* @throws IllegalArgumentException if field is not a member of this struct
* @throws IllegalStateException if struct descriptor is invalid
*/
@SuppressWarnings({"PMD.CollapsibleIfStatements", "PMD.AvoidDeeplyNestedIfStmts"})
public String getStringField(StructFieldDescriptor field) {
if (field.getType() != StructFieldType.kChar) {
throw new UnsupportedOperationException("field is not char type");
}
if (!field.getParent().equals(m_desc)) {
throw new IllegalArgumentException("field is not part of this struct");
}
if (!m_desc.isValid()) {
throw new IllegalStateException("struct descriptor is not valid");
}
byte[] bytes = new byte[field.m_arraySize];
m_data.position(field.m_offset).get(bytes, 0, field.m_arraySize);
// Find last non zero character
int stringLength = bytes.length;
for (; stringLength > 0; stringLength--) {
if (bytes[stringLength - 1] != 0) {
break;
}
}
// If string is all zeroes, its empty and return an empty string.
if (stringLength == 0) {
return "";
}
// Check if the end of the string is in the middle of a continuation byte or
// not.
if ((bytes[stringLength - 1] & 0x80) != 0) {
// This is a UTF8 continuation byte. Make sure its valid.
// Walk back until initial byte is found
int utf8StartByte = stringLength;
for (; utf8StartByte > 0; utf8StartByte--) {
if ((bytes[utf8StartByte - 1] & 0x40) != 0) {
// Having 2nd bit set means start byte
break;
}
}
if (utf8StartByte == 0) {
// This case means string only contains continuation bytes
return "";
}
utf8StartByte--;
// Check if its a 2, 3, or 4 byte
byte checkByte = bytes[utf8StartByte];
if ((checkByte & 0xE0) == 0xC0) {
// 2 byte, need 1 more byte
if (utf8StartByte != stringLength - 2) {
stringLength = utf8StartByte;
}
} else if ((checkByte & 0xF0) == 0xE0) {
// 3 byte, need 2 more bytes
if (utf8StartByte != stringLength - 3) {
stringLength = utf8StartByte;
}
} else if ((checkByte & 0xF8) == 0xF0) {
// 4 byte, need 3 more bytes
if (utf8StartByte != stringLength - 4) {
stringLength = utf8StartByte;
}
}
// If we get here, the string is either completely garbage or fine.
}
return new String(bytes, 0, stringLength, StandardCharsets.UTF_8);
}
/**
* Sets the value of a character or character array field.
*
* @param field field descriptor
* @param value field value
* @return true if the full value fit in the struct, false if truncated
* @throws UnsupportedOperationException if field is not char type
* @throws IllegalArgumentException if field is not a member of this struct
* @throws IllegalStateException if struct descriptor is invalid
*/
public boolean setStringField(StructFieldDescriptor field, String value) {
if (field.getType() != StructFieldType.kChar) {
throw new UnsupportedOperationException("field is not char type");
}
if (!field.getParent().equals(m_desc)) {
throw new IllegalArgumentException("field is not part of this struct");
}
if (!m_desc.isValid()) {
throw new IllegalStateException("struct descriptor is not valid");
}
ByteBuffer bb = StandardCharsets.UTF_8.encode(value);
int len = Math.min(bb.remaining(), field.m_arraySize);
boolean copiedFull = len == bb.remaining();
m_data.position(field.m_offset).put(bb.limit(len));
for (int i = len; i < field.m_arraySize; i++) {
m_data.put((byte) 0);
}
return copiedFull;
}
/**
* Gets the value of a struct field.
*
* @param field field descriptor
* @param arrIndex array index (must be less than field array size)
* @return field value
* @throws UnsupportedOperationException if field is not of struct type
* @throws IllegalArgumentException if field is not a member of this struct
* @throws IllegalStateException if struct descriptor is invalid
* @throws ArrayIndexOutOfBoundsException if array index is out of bounds
*/
public DynamicStruct getStructField(StructFieldDescriptor field, int arrIndex) {
if (field.getType() != StructFieldType.kStruct) {
throw new UnsupportedOperationException("field is not struct type");
}
if (!field.getParent().equals(m_desc)) {
throw new IllegalArgumentException("field is not part of this struct");
}
if (!m_desc.isValid()) {
throw new IllegalStateException("struct descriptor is not valid");
}
if (arrIndex < 0 || arrIndex >= field.m_arraySize) {
throw new ArrayIndexOutOfBoundsException(
"arrIndex (" + arrIndex + ") is larger than array size (" + field.m_arraySize + ")");
}
StructDescriptor struct = field.getStruct();
return wrap(struct, m_data.position(field.m_offset + arrIndex * struct.m_size));
}
/**
* Gets the value of a struct field.
*
* @param field field descriptor
* @return field value
* @throws UnsupportedOperationException if field is not of struct type
* @throws IllegalArgumentException if field is not a member of this struct
* @throws IllegalStateException if struct descriptor is invalid
*/
public DynamicStruct getStructField(StructFieldDescriptor field) {
return getStructField(field, 0);
}
/**
* Sets the value of a struct field.
*
* @param field field descriptor
* @param value struct value
* @param arrIndex array index (must be less than field array size)
* @throws UnsupportedOperationException if field is not struct type
* @throws IllegalArgumentException if field is not a member of this struct
* @throws IllegalStateException if struct descriptor is invalid
* @throws ArrayIndexOutOfBoundsException if array index is out of bounds
* @throws ReadOnlyBufferException if the underlying buffer is read-only
*/
public void setStructField(StructFieldDescriptor field, DynamicStruct value, int arrIndex) {
if (field.getType() != StructFieldType.kStruct) {
throw new UnsupportedOperationException("field is not struct type");
}
if (!field.getParent().equals(m_desc)) {
throw new IllegalArgumentException("field is not part of this struct");
}
if (!m_desc.isValid()) {
throw new IllegalStateException("struct descriptor is not valid");
}
StructDescriptor struct = field.getStruct();
if (!value.getDescriptor().equals(struct)) {
throw new IllegalArgumentException("value's struct type does not match field struct type");
}
if (!value.getDescriptor().isValid()) {
throw new IllegalStateException("value's struct descriptor is not valid");
}
if (arrIndex < 0 || arrIndex >= field.m_arraySize) {
throw new ArrayIndexOutOfBoundsException(
"arrIndex (" + arrIndex + ") is larger than array size (" + field.m_arraySize + ")");
}
m_data
.position(field.m_offset + arrIndex * struct.m_size)
.put(value.m_data.position(0).limit(value.getDescriptor().getSize()));
}
/**
* Sets the value of a struct field.
*
* @param field field descriptor
* @param value struct value
* @throws UnsupportedOperationException if field is not struct type
* @throws IllegalArgumentException if field is not a member of this struct
* @throws IllegalStateException if struct descriptor is invalid
* @throws ReadOnlyBufferException if the underlying buffer is read-only
*/
public void setStructField(StructFieldDescriptor field, DynamicStruct value) {
setStructField(field, value, 0);
}
private long getFieldImpl(StructFieldDescriptor field, int arrIndex) {
if (!field.getParent().equals(m_desc)) {
throw new IllegalArgumentException("field is not part of this struct");
}
if (!m_desc.isValid()) {
throw new IllegalStateException("struct descriptor is not valid");
}
if (arrIndex < 0 || arrIndex >= field.m_arraySize) {
throw new ArrayIndexOutOfBoundsException(
"arrIndex (" + arrIndex + ") is larger than array size (" + field.m_arraySize + ")");
}
long val =
switch (field.m_size) {
case 1 -> m_data.get(field.m_offset + arrIndex);
case 2 -> m_data.getShort(field.m_offset + arrIndex * 2);
case 4 -> m_data.getInt(field.m_offset + arrIndex * 4);
case 8 -> m_data.getLong(field.m_offset + arrIndex * 8);
default -> throw new IllegalStateException("invalid field size");
};
if (field.isUint() || field.getType() == StructFieldType.kBool) {
// for unsigned fields, we can simply logical shift and mask
return (val >>> field.m_bitShift) & field.getBitMask();
} else {
// to get sign extension, shift so the sign bit within the bitfield goes to the long's sign
// bit (also clearing all higher bits), then shift back down (also clearing all lower bits);
// since upper and lower bits are cleared with the shifts, the bitmask is unnecessary
return (val << (64 - field.m_bitShift - field.getBitWidth())) >> (64 - field.getBitWidth());
}
}
private void setFieldImpl(StructFieldDescriptor field, long value, int arrIndex) {
if (!field.getParent().equals(m_desc)) {
throw new IllegalArgumentException("field is not part of this struct");
}
if (!m_desc.isValid()) {
throw new IllegalStateException("struct descriptor is not valid");
}
if (arrIndex < 0 || arrIndex >= field.m_arraySize) {
throw new ArrayIndexOutOfBoundsException(
"arrIndex (" + arrIndex + ") is larger than array size (" + field.m_arraySize + ")");
}
// common case is no bit shift and no masking
if (!field.isBitField()) {
switch (field.m_size) {
case 1 -> m_data.put(field.m_offset + arrIndex, (byte) value);
case 2 -> m_data.putShort(field.m_offset + arrIndex * 2, (short) value);
case 4 -> m_data.putInt(field.m_offset + arrIndex * 4, (int) value);
case 8 -> m_data.putLong(field.m_offset + arrIndex * 8, value);
default -> throw new IllegalStateException("invalid field size");
}
return;
}
// handle bit shifting and masking into current value
switch (field.m_size) {
case 1 -> {
byte val = m_data.get(field.m_offset + arrIndex);
val &= (byte) ~(field.getBitMask() << field.m_bitShift);
val |= (byte) ((value & field.getBitMask()) << field.m_bitShift);
m_data.put(field.m_offset + arrIndex, val);
}
case 2 -> {
short val = m_data.getShort(field.m_offset + arrIndex * 2);
val &= (short) ~(field.getBitMask() << field.m_bitShift);
val |= (short) ((value & field.getBitMask()) << field.m_bitShift);
m_data.putShort(field.m_offset + arrIndex * 2, val);
}
case 4 -> {
int val = m_data.getInt(field.m_offset + arrIndex * 4);
val &= (int) ~(field.getBitMask() << field.m_bitShift);
val |= (int) ((value & field.getBitMask()) << field.m_bitShift);
m_data.putInt(field.m_offset + arrIndex * 4, val);
}
case 8 -> {
long val = m_data.getLong(field.m_offset + arrIndex * 8);
val &= ~(field.getBitMask() << field.m_bitShift);
val |= (value & field.getBitMask()) << field.m_bitShift;
m_data.putLong(field.m_offset + arrIndex * 8, val);
}
default -> throw new IllegalStateException("invalid field size");
}
}
private final StructDescriptor m_desc;
private final ByteBuffer m_data;
}

View File

@@ -0,0 +1,227 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util.struct;
import java.lang.reflect.Array;
import java.nio.ByteBuffer;
/**
* Interface for raw struct serialization.
*
* <p>This is designed for serializing small fixed-size data structures in the fastest and most
* compact means possible. Serialization consists of making relative put() calls to a ByteBuffer and
* deserialization consists of making relative get() calls from a ByteBuffer.
*
* <p>Idiomatically, classes that support raw struct serialization should provide a static final
* member named "struct" that provides an instance of an implementation of this interface.
*
* @param <T> object type
*/
public interface Struct<T> {
/** Serialized size of a "bool" value. */
int kSizeBool = 1;
/** Serialized size of an "int8" or "uint8" value. */
int kSizeInt8 = 1;
/** Serialized size of an "int16" or "uint16" value. */
int kSizeInt16 = 2;
/** Serialized size of an "int32" or "uint32" value. */
int kSizeInt32 = 4;
/** Serialized size of an "int64" or "uint64" value. */
int kSizeInt64 = 8;
/** Serialized size of an "float" or "float32" value. */
int kSizeFloat = 4;
/** Serialized size of an "double" or "float64" value. */
int kSizeDouble = 8;
/**
* Gets the Class object for the stored value.
*
* @return Class
*/
Class<T> getTypeClass();
/**
* Gets the type name (e.g. for schemas of other structs). This should be globally unique among
* structs.
*
* @return type name
*/
String getTypeName();
/**
* Gets the type string (e.g. for NetworkTables). This should be globally unique and start with
* "struct:".
*
* @return type string
*/
default String getTypeString() {
return "struct:" + getTypeName();
}
/**
* Gets the serialized size (in bytes). This should always be a constant.
*
* @return serialized size
*/
int getSize();
/**
* Gets the schema.
*
* @return schema
*/
String getSchema();
/**
* Gets the list of struct types referenced by this struct.
*
* @return list of struct types
*/
default Struct<?>[] getNested() {
return new Struct<?>[] {};
}
/**
* Deserializes an object from a raw struct serialized ByteBuffer starting at the current
* position. Will increment the ByteBuffer position by getStructSize() bytes. Will not otherwise
* modify the ByteBuffer (e.g. byte order will not be changed).
*
* @param bb ByteBuffer
* @return New object
*/
T unpack(ByteBuffer bb);
/**
* Puts object contents to a ByteBuffer starting at the current position. Will increment the
* ByteBuffer position by getStructSize() bytes. Will not otherwise modify the ByteBuffer (e.g.
* byte order will not be changed).
*
* @param bb ByteBuffer
* @param value object to serialize
*/
void pack(ByteBuffer bb, T value);
/**
* Updates object contents from a raw struct serialized ByteBuffer starting at the current
* position. Will increment the ByteBuffer position by getStructSize() bytes. Will not otherwise
* modify the ByteBuffer (e.g. byte order will not be changed).
*
* <p>Immutable classes cannot and should not implement this function. The default implementation
* throws UnsupportedOperationException.
*
* @param out object to update
* @param bb ByteBuffer
* @throws UnsupportedOperationException if the object is immutable
*/
default void unpackInto(T out, ByteBuffer bb) {
throw new UnsupportedOperationException("object does not support unpackInto");
}
/**
* Deserializes an array from a raw struct serialized ByteBuffer starting at the current position.
* Will increment the ByteBuffer position by size * struct.size() bytes. Will not otherwise modify
* the ByteBuffer (e.g. byte order will not be changed).
*
* @param <T> Object type
* @param bb ByteBuffer
* @param size Size of the array
* @param struct Struct implementation
* @return Deserialized array
*/
static <T> T[] unpackArray(ByteBuffer bb, int size, Struct<T> struct) {
@SuppressWarnings("unchecked")
T[] arr = (T[]) Array.newInstance(struct.getTypeClass(), size);
for (int i = 0; i < arr.length; i++) {
arr[i] = struct.unpack(bb);
}
return arr;
}
/**
* Deserializes a double array from a raw struct serialized ByteBuffer starting at the current
* position. Will increment the ByteBuffer position by size * kSizeDouble bytes. Will not
* otherwise modify the ByteBuffer (e.g. byte order will not be changed).
*
* @param bb ByteBuffer
* @param size Size of the array
* @return Double array
*/
static double[] unpackDoubleArray(ByteBuffer bb, int size) {
double[] arr = new double[size];
for (int i = 0; i < size; i++) {
arr[i] = bb.getDouble();
}
return arr;
}
/**
* Puts array contents to a ByteBuffer starting at the current position. Will increment the
* ByteBuffer position by size * struct.size() bytes. Will not otherwise modify the ByteBuffer
* (e.g. byte order will not be changed).
*
* @param <T> Object type
* @param bb ByteBuffer
* @param arr Array to serialize
* @param struct Struct implementation
*/
static <T> void packArray(ByteBuffer bb, T[] arr, Struct<T> struct) {
for (T obj : arr) {
struct.pack(bb, obj);
}
}
/**
* Puts array contents to a ByteBuffer starting at the current position. Will increment the
* ByteBuffer position by size * kSizeDouble bytes. Will not otherwise modify the ByteBuffer (e.g.
* byte order will not be changed).
*
* @param bb ByteBuffer
* @param arr Array to serialize
*/
static void packArray(ByteBuffer bb, double[] arr) {
for (double obj : arr) {
bb.putDouble(obj);
}
}
/**
* Returns whether or not objects are immutable. Immutable objects must also be comparable using
* the equals() method. Default implementation returns false.
*
* @return True if object is immutable
*/
default boolean isImmutable() {
return false;
}
/**
* Returns whether or not objects are cloneable using the clone() method. Cloneable objects must
* also be comparable using the equals() method. Default implementation returns false.
*
* @return True if object is cloneable
*/
default boolean isCloneable() {
return false;
}
/**
* Creates a (deep) clone of the object. May also return the object directly if the object is
* immutable. Default implementation throws CloneNotSupportedException. Typically this should be
* implemented by implementing clone() on the object itself, and calling it from here.
*
* @param obj object to clone
* @return Clone of object (if immutable, may be same object)
* @throws CloneNotSupportedException if clone not supported
*/
default T clone(T obj) throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
}

View File

@@ -0,0 +1,231 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util.struct;
import java.lang.reflect.Array;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Collection;
/**
* Reusable buffer for serialization/deserialization to/from a raw struct.
*
* @param <T> Object type.
*/
public final class StructBuffer<T> {
private StructBuffer(Struct<T> struct) {
m_structSize = struct.getSize();
m_buf = ByteBuffer.allocateDirect(m_structSize).order(ByteOrder.LITTLE_ENDIAN);
m_struct = struct;
}
/**
* Returns a StructBuffer for the given struct.
*
* @param struct A struct.
* @param <T> Object type.
* @return A StructBuffer for the given struct.
*/
public static <T> StructBuffer<T> create(Struct<T> struct) {
return new StructBuffer<>(struct);
}
/**
* Gets the struct object of the stored type.
*
* @return struct object
*/
public Struct<T> getStruct() {
return m_struct;
}
/**
* Gets the type string.
*
* @return type string
*/
public String getTypeString() {
return m_struct.getTypeString();
}
/**
* Ensures sufficient buffer space is available for the given number of elements.
*
* @param nelem number of elements
*/
public void reserve(int nelem) {
if ((nelem * m_structSize) > m_buf.capacity()) {
m_buf = ByteBuffer.allocateDirect(nelem * m_structSize).order(ByteOrder.LITTLE_ENDIAN);
}
}
/**
* Serializes a value to a ByteBuffer. The returned ByteBuffer is a direct byte buffer with the
* position set to the end of the serialized data.
*
* @param value value
* @return byte buffer
*/
public ByteBuffer write(T value) {
m_buf.position(0);
m_struct.pack(m_buf, value);
return m_buf;
}
/**
* Deserializes a value from a byte array, creating a new object.
*
* @param buf byte array
* @param start starting location within byte array
* @param len length of serialized data
* @return new object
*/
public T read(byte[] buf, int start, int len) {
return read(ByteBuffer.wrap(buf, start, len));
}
/**
* Deserializes a value from a byte array, creating a new object.
*
* @param buf byte array
* @return new object
*/
public T read(byte[] buf) {
return read(buf, 0, buf.length);
}
/**
* Deserializes a value from a ByteBuffer, creating a new object.
*
* @param buf byte buffer
* @return new object
*/
public T read(ByteBuffer buf) {
buf.order(ByteOrder.LITTLE_ENDIAN);
return m_struct.unpack(buf);
}
/**
* Deserializes a value from a byte array into a mutable object.
*
* @param out object (will be updated with deserialized contents)
* @param buf byte array
* @param start starting location within byte array
* @param len length of serialized data
* @throws UnsupportedOperationException if T is immutable
*/
public void readInto(T out, byte[] buf, int start, int len) {
readInto(out, ByteBuffer.wrap(buf, start, len));
}
/**
* Deserializes a value from a byte array into a mutable object.
*
* @param out object (will be updated with deserialized contents)
* @param buf byte array
* @throws UnsupportedOperationException if T is immutable
*/
public void readInto(T out, byte[] buf) {
readInto(out, buf, 0, buf.length);
}
/**
* Deserializes a value from a ByteBuffer into a mutable object.
*
* @param out object (will be updated with deserialized contents)
* @param buf byte buffer
* @throws UnsupportedOperationException if T is immutable
*/
public void readInto(T out, ByteBuffer buf) {
m_struct.unpackInto(out, buf);
}
/**
* Serializes a collection of values to a ByteBuffer. The returned ByteBuffer is a direct byte
* buffer with the position set to the end of the serialized data.
*
* @param values values
* @return byte buffer
*/
public ByteBuffer writeArray(Collection<T> values) {
m_buf.position(0);
if ((values.size() * m_structSize) > m_buf.capacity()) {
m_buf =
ByteBuffer.allocateDirect(values.size() * m_structSize * 2)
.order(ByteOrder.LITTLE_ENDIAN);
}
for (T v : values) {
m_struct.pack(m_buf, v);
}
return m_buf;
}
/**
* Serializes an array of values to a ByteBuffer. The returned ByteBuffer is a direct byte buffer
* with the position set to the end of the serialized data.
*
* @param values values
* @return byte buffer
*/
public ByteBuffer writeArray(T[] values) {
m_buf.position(0);
if ((values.length * m_structSize) > m_buf.capacity()) {
m_buf =
ByteBuffer.allocateDirect(values.length * m_structSize * 2)
.order(ByteOrder.LITTLE_ENDIAN);
}
for (T v : values) {
m_struct.pack(m_buf, v);
}
return m_buf;
}
/**
* Deserializes an array of values from a byte array, creating an array of new objects.
*
* @param buf byte array
* @param start starting location within byte array
* @param len length of serialized data
* @return new object array
*/
public T[] readArray(byte[] buf, int start, int len) {
return readArray(ByteBuffer.wrap(buf, start, len));
}
/**
* Deserializes an array of values from a byte array, creating an array of new objects.
*
* @param buf byte array
* @return new object array
*/
public T[] readArray(byte[] buf) {
return readArray(buf, 0, buf.length);
}
/**
* Deserializes an array of values from a ByteBuffer, creating an array of new objects.
*
* @param buf byte buffer
* @return new object array
*/
public T[] readArray(ByteBuffer buf) {
buf.order(ByteOrder.LITTLE_ENDIAN);
int len = buf.limit() - buf.position();
if ((len % m_structSize) != 0) {
throw new RuntimeException("buffer size not a multiple of struct size");
}
int nelem = len / m_structSize;
@SuppressWarnings("unchecked")
T[] arr = (T[]) Array.newInstance(m_struct.getTypeClass(), nelem);
for (int i = 0; i < nelem; i++) {
arr[i] = m_struct.unpack(buf);
}
return arr;
}
private ByteBuffer m_buf;
private final Struct<T> m_struct;
private final int m_structSize;
}

View File

@@ -0,0 +1,158 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util.struct;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
/** Raw struct dynamic struct descriptor. */
public class StructDescriptor {
StructDescriptor(String name) {
m_name = name;
}
/**
* Gets the struct name.
*
* @return name
*/
public String getName() {
return m_name;
}
/**
* Gets the struct schema.
*
* @return schema
*/
public String getSchema() {
return m_schema;
}
/**
* Returns whether the struct is valid (e.g. the struct is fully defined and field offsets
* computed).
*
* @return true if valid
*/
public boolean isValid() {
return m_valid;
}
/**
* Returns the struct size, in bytes. Not valid unless IsValid() is true.
*
* @return size in bytes
* @throws IllegalStateException if descriptor is invalid
*/
public int getSize() {
if (!m_valid) {
throw new IllegalStateException("descriptor is invalid");
}
return m_size;
}
/**
* Gets a field descriptor by name. Note the field cannot be accessed until the struct is valid.
*
* @param name field name
* @return field descriptor, or nullptr if not found
*/
public StructFieldDescriptor findFieldByName(String name) {
return m_fieldsByName.get(name);
}
/**
* Gets all field descriptors. Note fields cannot be accessed until the struct is valid.
*
* @return field descriptors
*/
public List<StructFieldDescriptor> getFields() {
return m_fields;
}
boolean checkCircular(Stack<StructDescriptor> stack) {
stack.push(this);
for (StructDescriptor ref : m_references) {
if (stack.contains(ref)) {
return false;
}
if (!ref.checkCircular(stack)) {
return false;
}
}
stack.pop();
return true;
}
void calculateOffsets(Stack<StructDescriptor> stack) {
int offset = 0;
int shift = 0;
int prevBitfieldSize = 0;
for (StructFieldDescriptor field : m_fields) {
if (!field.isBitField()) {
shift = 0; // reset shift on non-bitfield element
offset += prevBitfieldSize; // finish bitfield if active
prevBitfieldSize = 0; // previous is now not bitfield
field.m_offset = offset;
StructDescriptor struct = field.getStruct();
if (struct != null) {
if (!struct.isValid()) {
m_valid = false;
return;
}
field.m_size = struct.m_size;
}
offset += field.m_size * field.m_arraySize;
} else {
int bitWidth = field.getBitWidth();
if (field.getType() == StructFieldType.kBool
&& prevBitfieldSize != 0
&& (shift + 1) <= (prevBitfieldSize * 8)) {
// bool takes on size of preceding bitfield type (if it fits)
field.m_size = prevBitfieldSize;
} else if (field.m_size != prevBitfieldSize || (shift + bitWidth) > (field.m_size * 8)) {
shift = 0;
offset += prevBitfieldSize;
}
prevBitfieldSize = field.m_size;
field.m_offset = offset;
field.m_bitShift = shift;
shift += bitWidth;
}
}
// update struct size
m_size = offset + prevBitfieldSize;
m_valid = true;
// now that we're valid, referring types may be too
stack.push(this);
for (StructDescriptor ref : m_references) {
if (stack.contains(ref)) {
throw new IllegalStateException(
"internal error (inconsistent data): circular struct reference between "
+ m_name
+ " and "
+ ref.m_name);
}
ref.calculateOffsets(stack);
}
stack.pop();
}
private final String m_name;
String m_schema;
final Set<StructDescriptor> m_references = new HashSet<>();
final List<StructFieldDescriptor> m_fields = new ArrayList<>();
final Map<String, StructFieldDescriptor> m_fieldsByName = new HashMap<>();
int m_size;
boolean m_valid;
}

View File

@@ -0,0 +1,150 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util.struct;
import edu.wpi.first.util.struct.parser.ParseException;
import edu.wpi.first.util.struct.parser.ParsedDeclaration;
import edu.wpi.first.util.struct.parser.ParsedSchema;
import edu.wpi.first.util.struct.parser.Parser;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
/** Database of raw struct dynamic descriptors. */
public class StructDescriptorDatabase {
/** Default constructor. */
public StructDescriptorDatabase() {}
/**
* Adds a structure schema to the database. If the struct references other structs that have not
* yet been added, it will not be valid until those structs are also added.
*
* @param name structure name
* @param schema structure schema
* @return Added struct dynamic descriptor
* @throws BadSchemaException if schema invalid
*/
public StructDescriptor add(String name, String schema) throws BadSchemaException {
Parser parser = new Parser(schema);
ParsedSchema parsed;
try {
parsed = parser.parse();
} catch (ParseException e) {
throw new BadSchemaException("parse error", e);
}
// turn parsed schema into descriptors
StructDescriptor theStruct = m_structs.computeIfAbsent(name, StructDescriptor::new);
theStruct.m_schema = schema;
theStruct.m_fields.clear();
boolean isValid = true;
for (ParsedDeclaration decl : parsed.declarations) {
StructFieldType type = StructFieldType.fromString(decl.typeString);
int size = type.size;
// bitfield checks
if (decl.bitWidth != 0) {
// only integer or boolean types are allowed
if (!type.isInt && !type.isUint && type != StructFieldType.kBool) {
throw new BadSchemaException(
decl.name, "type " + decl.typeString + " cannot be bitfield");
}
// bit width cannot be larger than field size
if (decl.bitWidth > (size * 8)) {
throw new BadSchemaException(
decl.name, "bit width " + decl.bitWidth + " exceeds type size");
}
// bit width must be 1 for booleans
if (type == StructFieldType.kBool && decl.bitWidth != 1) {
throw new BadSchemaException(decl.name, "bit width must be 1 for bool type");
}
// cannot combine array and bitfield (shouldn't parse, but double-check)
if (decl.arraySize > 1) {
throw new BadSchemaException(decl.name, "cannot combine array and bitfield");
}
}
// struct handling
StructDescriptor structDesc = null;
if (type == StructFieldType.kStruct) {
// recursive definitions are not allowed
if (decl.typeString.equals(name)) {
throw new BadSchemaException(decl.name, "recursive struct reference");
}
// cross-reference struct, creating a placeholder if necessary
StructDescriptor aStruct =
m_structs.computeIfAbsent(decl.typeString, StructDescriptor::new);
// if the struct isn't valid, we can't be valid either
if (aStruct.isValid()) {
size = aStruct.getSize();
} else {
isValid = false;
}
// add to cross-references for when the struct does become valid
aStruct.m_references.add(theStruct);
structDesc = aStruct;
}
// create field
StructFieldDescriptor fieldDesc =
new StructFieldDescriptor(
theStruct,
decl.name,
type,
size,
decl.arraySize,
decl.bitWidth,
decl.enumValues,
structDesc);
if (theStruct.m_fieldsByName.put(decl.name, fieldDesc) != null) {
throw new BadSchemaException(decl.name, "duplicate field name");
}
theStruct.m_fields.add(fieldDesc);
}
theStruct.m_valid = isValid;
Stack<StructDescriptor> stack = new Stack<>();
if (isValid) {
// we have all the info needed, so calculate field offset & shift
theStruct.calculateOffsets(stack);
} else {
// check for circular reference
if (!theStruct.checkCircular(stack)) {
StringBuilder builder = new StringBuilder("circular struct reference: ");
boolean first = true;
for (StructDescriptor elem : stack) {
if (!first) {
builder.append(" <- ");
} else {
first = false;
}
builder.append(elem.getName());
}
throw new BadSchemaException(builder.toString());
}
}
return theStruct;
}
/**
* Finds a structure in the database by name.
*
* @param name structure name
* @return struct descriptor, or null if not found
*/
public StructDescriptor find(String name) {
return m_structs.get(name);
}
private final Map<String, StructDescriptor> m_structs = new HashMap<>();
}

View File

@@ -0,0 +1,65 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util.struct;
import java.util.Optional;
/**
* A utility class for fetching the assigned struct of existing classes. These are usually public,
* static, and final properties with the Struct type.
*/
public final class StructFetcher {
private StructFetcher() {
throw new UnsupportedOperationException("This is a utility class!");
}
/**
* Returns a {@link Struct} for the given {@link StructSerializable} marked class. Due to the
* non-contractual nature of the marker this can fail. If the {@code struct} field could not be
* accessed for any reason, an empty {@link Optional} is returned.
*
* @param <T> The type of the class.
* @param clazz The class object to extract the struct from.
* @return An optional containing the struct if it could be extracted.
*/
@SuppressWarnings("unchecked")
public static <T extends StructSerializable> Optional<Struct<T>> fetchStruct(
Class<? extends T> clazz) {
try {
var possibleField = Optional.ofNullable(clazz.getDeclaredField("struct"));
return possibleField.flatMap(
field -> {
if (Struct.class.isAssignableFrom(field.getType())) {
try {
return Optional.ofNullable((Struct<T>) field.get(null));
} catch (IllegalAccessException e) {
return Optional.empty();
}
} else {
return Optional.empty();
}
});
} catch (NoSuchFieldException e) {
return Optional.empty();
}
}
/**
* Returns a {@link Struct} for the given class. This does not do compile time checking that the
* class is a {@link StructSerializable}. Whenever possible it is reccomended to use {@link
* #fetchStruct(Class)}.
*
* @param clazz The class object to extract the struct from.
* @return An optional containing the struct if it could be extracted.
*/
@SuppressWarnings("unchecked")
public static Optional<Struct<?>> fetchStructDynamic(Class<?> clazz) {
if (StructSerializable.class.isAssignableFrom(clazz)) {
return fetchStruct((Class<? extends StructSerializable>) clazz).map(struct -> struct);
} else {
return Optional.empty();
}
}
}

View File

@@ -0,0 +1,241 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util.struct;
import java.util.Map;
/** Raw struct dynamic field descriptor. */
public class StructFieldDescriptor {
private static int toBitWidth(int size, int bitWidth) {
if (bitWidth == 0) {
return size * 8;
} else {
return bitWidth;
}
}
private static long toBitMask(int size, int bitWidth) {
if (size == 0) {
return 0;
} else {
return -1L >>> (64 - toBitWidth(size, bitWidth));
}
}
// does not fill in offset, shift
StructFieldDescriptor(
StructDescriptor parent,
String name,
StructFieldType type,
int size,
int arraySize,
int bitWidth,
Map<String, Long> enumValues,
StructDescriptor structDesc) {
m_parent = parent;
m_name = name;
m_size = size;
m_arraySize = arraySize;
m_enum = enumValues;
m_struct = structDesc;
m_bitMask = toBitMask(size, bitWidth);
m_type = type;
m_bitWidth = toBitWidth(size, bitWidth);
}
/**
* Gets the dynamic struct this field is contained in.
*
* @return struct descriptor
*/
public StructDescriptor getParent() {
return m_parent;
}
/**
* Gets the field name.
*
* @return field name
*/
public String getName() {
return m_name;
}
/**
* Gets the field type.
*
* @return field type
*/
public StructFieldType getType() {
return m_type;
}
/**
* Returns whether the field type is a signed integer.
*
* @return true if signed integer, false otherwise
*/
public boolean isInt() {
return m_type.isInt;
}
/**
* Returns whether the field type is an unsigned integer.
*
* @return true if unsigned integer, false otherwise
*/
public boolean isUint() {
return m_type.isUint;
}
/**
* Gets the underlying storage size of the field, in bytes.
*
* @return number of bytes
*/
public int getSize() {
return m_size;
}
/**
* Gets the storage offset of the field, in bytes.
*
* @return number of bytes from the start of the struct
*/
public int getOffset() {
return m_offset;
}
/**
* Gets the bit width of the field, in bits.
*
* @return number of bits
*/
public int getBitWidth() {
return m_bitWidth == 0 ? m_size * 8 : m_bitWidth;
}
/**
* Gets the bit mask for the field. The mask is always the least significant bits (it is not
* shifted).
*
* @return bit mask
*/
public long getBitMask() {
return m_bitMask;
}
/**
* Gets the bit shift for the field (LSB=0).
*
* @return number of bits
*/
public int getBitShift() {
return m_bitShift;
}
/**
* Returns whether the field is an array.
*
* @return true if array
*/
public boolean isArray() {
return m_arraySize > 1;
}
/**
* Gets the array size. Returns 1 if non-array.
*
* @return number of elements
*/
public int getArraySize() {
return m_arraySize;
}
/**
* Returns whether the field has enumerated values.
*
* @return true if there are enumerated values
*/
public boolean hasEnum() {
return m_enum != null;
}
/**
* Gets the enumerated values.
*
* @return set of enumerated values
*/
public Map<String, Long> getEnumValues() {
return m_enum;
}
/**
* Gets the struct descriptor for a struct data type.
*
* @return struct descriptor; returns null for non-struct
*/
public StructDescriptor getStruct() {
return m_struct;
}
/**
* Gets the minimum unsigned integer value that can be stored in this field.
*
* @return minimum value
*/
public long getUintMin() {
return 0;
}
/**
* Gets the maximum unsigned integer value that can be stored in this field. Note this is not the
* actual maximum for uint64 (due to Java lacking support for 64-bit unsigned integers).
*
* @return maximum value
*/
public long getUintMax() {
return m_bitMask;
}
/**
* Gets the minimum signed integer value that can be stored in this field.
*
* @return minimum value
*/
public long getIntMin() {
return -(m_bitMask >> 1) - 1;
}
/**
* Gets the maximum signed integer value that can be stored in this field.
*
* @return maximum value
*/
public long getIntMax() {
return m_bitMask >> 1;
}
/**
* Returns whether the field is a bitfield.
*
* @return true if bitfield
*/
public boolean isBitField() {
return (m_bitShift != 0 || m_bitWidth != (m_size * 8)) && m_struct == null;
}
private final StructDescriptor m_parent;
private final String m_name;
int m_size;
int m_offset;
final int m_arraySize; // 1 for non-arrays
private final Map<String, Long> m_enum;
private final StructDescriptor m_struct; // null for non-structs
private final long m_bitMask;
private final StructFieldType m_type;
private final int m_bitWidth;
int m_bitShift;
}

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.util.struct;
/** Known data types for raw struct dynamic fields (see StructFieldDescriptor). */
public enum StructFieldType {
/** bool. */
kBool("bool", false, false, 1),
/** char. */
kChar("char", false, false, 1),
/** int8. */
kInt8("int8", true, false, 1),
/** int16. */
kInt16("int16", true, false, 2),
/** int32. */
kInt32("int32", true, false, 4),
/** int64. */
kInt64("int64", true, false, 8),
/** uint8. */
kUint8("uint8", false, true, 1),
/** uint16. */
kUint16("uint16", false, true, 2),
/** uint32. */
kUint32("uint32", false, true, 4),
/** uint64. */
kUint64("uint64", false, true, 8),
/** float. */
kFloat("float", false, false, 4),
/** double. */
kDouble("double", false, false, 8),
/** struct. */
kStruct("struct", false, false, 0);
/** The name of the data type. */
@SuppressWarnings("MemberName")
public final String name;
/** Indicates if the data type is a signed integer. */
@SuppressWarnings("MemberName")
public final boolean isInt;
/** Indicates if the data type is an unsigned integer. */
@SuppressWarnings("MemberName")
public final boolean isUint;
/** The size (in bytes) of the data type. */
@SuppressWarnings("MemberName")
public final int size;
StructFieldType(String name, boolean isInt, boolean isUint, int size) {
this.name = name;
this.isInt = isInt;
this.isUint = isUint;
this.size = size;
}
@Override
public String toString() {
return name;
}
/**
* Get field type from string.
*
* @param str string
* @return field type
*/
public static StructFieldType fromString(String str) {
for (StructFieldType type : StructFieldType.values()) {
if (type.name.equals(str)) {
return type;
}
}
if ("float32".equals(str)) {
return kFloat;
} else if ("float64".equals(str)) {
return kDouble;
} else {
return kStruct;
}
}
}

View File

@@ -0,0 +1,573 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util.struct;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.lang.reflect.RecordComponent;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
/** A utility class for procedurally generating {@link Struct}s from records and enums. */
public final class StructGenerator {
private StructGenerator() {
throw new UnsupportedOperationException("This is a utility class!");
}
/**
* A functional interface representing a method that retrives a value from a {@link ByteBuffer}.
*/
@FunctionalInterface
private interface Unpacker<T> {
T unpack(ByteBuffer buffer);
}
/** A functional interface representing a method that packs a value into a {@link ByteBuffer}. */
@FunctionalInterface
private interface Packer<T> {
ByteBuffer pack(ByteBuffer buffer, T value);
static <T> Packer<T> fromStruct(Struct<T> struct) {
return (buffer, value) -> {
struct.pack(buffer, value);
return buffer;
};
}
}
private record PrimType<T>(String name, int size, Unpacker<T> unpacker, Packer<T> packer) {}
/** A map of primitive types to their schema types. */
private static final HashMap<Class<?>, PrimType<?>> primitiveTypeMap = new HashMap<>();
private static <T> void addPrimType(
Class<T> boxedClass,
Class<T> primitiveClass,
String name,
int size,
Unpacker<T> unpacker,
Packer<T> packer) {
PrimType<T> primType = new PrimType<>(name, size, unpacker, packer);
primitiveTypeMap.put(boxedClass, primType);
primitiveTypeMap.put(primitiveClass, primType);
}
// Add primitive types to the map
static {
addPrimType(
Integer.class, int.class, "int32", Integer.BYTES, ByteBuffer::getInt, ByteBuffer::putInt);
addPrimType(
Double.class,
double.class,
"float64",
Double.BYTES,
ByteBuffer::getDouble,
ByteBuffer::putDouble);
addPrimType(
Float.class,
float.class,
"float32",
Float.BYTES,
ByteBuffer::getFloat,
ByteBuffer::putFloat);
addPrimType(
Boolean.class,
boolean.class,
"bool",
Byte.BYTES,
buffer -> buffer.get() != 0,
(buffer, value) -> buffer.put((byte) (value ? 1 : 0)));
addPrimType(
Character.class,
char.class,
"char",
Character.BYTES,
ByteBuffer::getChar,
ByteBuffer::putChar);
addPrimType(Byte.class, byte.class, "uint8", Byte.BYTES, ByteBuffer::get, ByteBuffer::put);
addPrimType(
Short.class, short.class, "int16", Short.BYTES, ByteBuffer::getShort, ByteBuffer::putShort);
addPrimType(
Long.class, long.class, "int64", Long.BYTES, ByteBuffer::getLong, ByteBuffer::putLong);
}
/**
* A map of types to their custom struct schemas.
*
* <p>This allows adding custom struct implementations for types that are not supported by
* default. Think of vendor-specific.
*/
private static final HashMap<Class<?>, Struct<?>> customStructTypeMap = new HashMap<>();
/**
* Add a custom struct to the structifier.
*
* @param <T> The type the struct is for.
* @param clazz The class of the type.
* @param struct The struct to add.
* @param override Whether to override an existing struct. An existing struct could mean the type
* already has a {@code struct} field and implemnts {@link StructSerializable} or that the
* type is already in the custom struct map.
*/
public static <T> void addCustomStruct(Class<T> clazz, Struct<T> struct, boolean override) {
if (override) {
customStructTypeMap.put(clazz, struct);
} else if (!StructSerializable.class.isAssignableFrom(clazz)) {
customStructTypeMap.putIfAbsent(clazz, struct);
}
}
/** A utility for building schema syntax in a procedural manner. */
@SuppressWarnings("PMD.AvoidStringBufferField")
public static class SchemaBuilder {
/** A utility for building enum fields in a procedural manner. */
public static final class EnumFieldBuilder {
private final StringBuilder m_builder = new StringBuilder();
private final String m_fieldName;
private boolean m_firstVariant = true;
/**
* Creates a new enum field builder.
*
* @param fieldName The name of the field.
*/
public EnumFieldBuilder(String fieldName) {
this.m_fieldName = fieldName;
m_builder.append("enum {");
}
/**
* Adds a variant to the enum field.
*
* @param name The name of the variant.
* @param value The value of the variant.
* @return The builder for chaining.
*/
public EnumFieldBuilder addVariant(String name, int value) {
if (!m_firstVariant) {
m_builder.append(',');
}
m_firstVariant = false;
m_builder.append(name).append('=').append(value);
return this;
}
/**
* Builds the enum field. If this object is being used with {@link SchemaBuilder#addEnumField}
* then {@link #build()} does not have to be called by the user.
*
* @return The built enum field.
*/
public String build() {
m_builder.append("} int8 ").append(m_fieldName).append(';');
return m_builder.toString();
}
}
/** Creates a new schema builder. */
public SchemaBuilder() {}
private final StringBuilder m_builder = new StringBuilder();
/**
* Adds a field to the schema.
*
* @param name The name of the field.
* @param type The type of the field.
* @return The builder for chaining.
*/
public SchemaBuilder addField(String name, String type) {
m_builder.append(type).append(' ').append(name).append(';');
return this;
}
/**
* Adds an inline enum field to the schema.
*
* @param enumFieldBuilder The builder for the enum field.
* @return The builder for chaining.
*/
public SchemaBuilder addEnumField(EnumFieldBuilder enumFieldBuilder) {
m_builder.append(enumFieldBuilder.build());
return this;
}
/**
* Builds the schema.
*
* @return The built schema.
*/
public String build() {
return m_builder.toString();
}
}
private static <T> Struct<T> noopStruct(Class<T> cls) {
return new Struct<>() {
@Override
public Class<T> getTypeClass() {
return cls;
}
@Override
public String getTypeName() {
return cls.getSimpleName();
}
@Override
public String getSchema() {
return "";
}
@Override
public int getSize() {
return 0;
}
@Override
public void pack(ByteBuffer buffer, T value) {}
@Override
public T unpack(ByteBuffer buffer) {
return null;
}
@Override
public boolean isImmutable() {
return true;
}
};
}
/**
* Generates a {@link Struct} for the given {@link Record} class. If a {@link Struct} cannot be
* generated from the {@link Record}, the errors encountered will be printed and a no-op {@link
* Struct} will be returned.
*
* @param <R> The type of the record.
* @param recordClass The class of the record.
* @return The generated struct.
*/
@SuppressWarnings({"unchecked", "PMD.AvoidAccessibilityAlteration"})
public static <R extends Record> Struct<R> genRecord(final Class<R> recordClass) {
final RecordComponent[] components = recordClass.getRecordComponents();
final SchemaBuilder schemaBuilder = new SchemaBuilder();
final ArrayList<Struct<?>> nestedStructs = new ArrayList<>();
final ArrayList<Unpacker<?>> unpackers = new ArrayList<>();
final ArrayList<Packer<?>> packers = new ArrayList<>();
int size = 0;
boolean failed = false;
for (final RecordComponent component : components) {
final Class<?> type = component.getType();
final String name = component.getName();
component.getAccessor().setAccessible(true);
if (primitiveTypeMap.containsKey(type)) {
PrimType<?> primType = primitiveTypeMap.get(type);
schemaBuilder.addField(name, primType.name);
size += primType.size;
unpackers.add(primType.unpacker);
packers.add(primType.packer);
} else {
Struct<?> struct;
if (customStructTypeMap.containsKey(type)) {
struct = customStructTypeMap.get(type);
} else if (StructSerializable.class.isAssignableFrom(type)) {
var optStruct = StructFetcher.fetchStructDynamic(type);
if (optStruct.isPresent()) {
struct = optStruct.get();
} else {
System.err.println(
"Could not structify record component: "
+ recordClass.getSimpleName()
+ "#"
+ name
+ "\n Could not extract struct from marked class: "
+ type.getName());
failed = true;
continue;
}
} else {
System.err.println(
"Could not structify record component: " + recordClass.getSimpleName() + "#" + name);
failed = true;
continue;
}
schemaBuilder.addField(name, struct.getTypeName());
size += struct.getSize();
nestedStructs.add(struct);
nestedStructs.addAll(List.of(struct.getNested()));
unpackers.add(struct::unpack);
packers.add(Packer.fromStruct(struct));
}
}
if (failed) {
return noopStruct(recordClass);
}
final int frozenSize = size;
final String schema = schemaBuilder.build();
return new Struct<>() {
@Override
public Class<R> getTypeClass() {
return recordClass;
}
@Override
public String getTypeName() {
return recordClass.getSimpleName();
}
@Override
public String getSchema() {
return schema;
}
@Override
public int getSize() {
return frozenSize;
}
@Override
public void pack(ByteBuffer buffer, R value) {
boolean failed = false;
int startingPosition = buffer.position();
for (int i = 0; i < components.length; i++) {
Packer<Object> packer = (Packer<Object>) packers.get(i);
try {
Object componentValue = components[i].getAccessor().invoke(value);
if (componentValue == null) {
throw new IllegalArgumentException("Component is null");
}
packer.pack(buffer, componentValue);
} catch (IllegalAccessException
| IllegalArgumentException
| InvocationTargetException e) {
System.err.println(
"Could not pack record component: "
+ recordClass.getSimpleName()
+ "#"
+ components[i].getName()
+ "\n "
+ e.getMessage());
failed = true;
break;
}
}
if (failed) {
buffer.position(startingPosition);
for (int i = 0; i < frozenSize; i++) {
buffer.put((byte) 0);
}
}
}
@Override
public R unpack(ByteBuffer buffer) {
try {
Object[] args = new Object[components.length];
Class<?>[] argTypes = new Class<?>[components.length];
for (int i = 0; i < components.length; i++) {
args[i] = unpackers.get(i).unpack(buffer);
argTypes[i] = components[i].getType();
}
return recordClass.getConstructor(argTypes).newInstance(args);
} catch (InstantiationException
| IllegalAccessException
| InvocationTargetException
| NoSuchMethodException
| SecurityException e) {
System.err.println(
"Could not unpack record: "
+ recordClass.getSimpleName()
+ "\n "
+ e.getMessage());
return null;
}
}
@Override
public Struct<?>[] getNested() {
return nestedStructs.toArray(new Struct<?>[0]);
}
@Override
public boolean isImmutable() {
return true;
}
};
}
/**
* Generates a {@link Struct} for the given {@link Enum} class. If a {@link Struct} cannot be
* generated from the {@link Enum}, the errors encountered will be printed and a no-op {@link
* Struct} will be returned.
*
* @param <E> The type of the enum.
* @param enumClass The class of the enum.
* @return The generated struct.
*/
@SuppressWarnings({"unchecked", "PMD.AvoidAccessibilityAlteration"})
public static <E extends Enum<E>> Struct<E> genEnum(Class<E> enumClass) {
final E[] enumVariants = enumClass.getEnumConstants();
final Field[] allEnumFields = enumClass.getDeclaredFields();
final SchemaBuilder schemaBuilder = new SchemaBuilder();
final SchemaBuilder.EnumFieldBuilder enumFieldBuilder =
new SchemaBuilder.EnumFieldBuilder("variant");
final HashMap<Integer, E> enumMap = new HashMap<>();
final ArrayList<Packer<?>> packers = new ArrayList<>();
if (enumVariants == null || enumVariants.length == 0) {
System.err.println(
"Could not structify enum: "
+ enumClass.getSimpleName()
+ "\n "
+ "Enum has no constants");
return noopStruct(enumClass);
}
int size = 0;
for (final E constant : enumVariants) {
final String name = constant.name();
final int ordinal = constant.ordinal();
enumFieldBuilder.addVariant(name, ordinal);
enumMap.put(ordinal, constant);
}
schemaBuilder.addEnumField(enumFieldBuilder);
size += 1;
final List<Field> enumFields =
List.of(allEnumFields).stream()
.filter(f -> !f.isEnumConstant() && !Modifier.isStatic(f.getModifiers()))
.toList();
boolean failed = false;
for (final Field field : enumFields) {
final Class<?> type = field.getType();
final String name = field.getName();
field.setAccessible(true);
if (primitiveTypeMap.containsKey(type)) {
PrimType<?> primType = primitiveTypeMap.get(type);
schemaBuilder.addField(name, primType.name);
size += primType.size;
packers.add(primType.packer);
} else {
Struct<?> struct;
if (customStructTypeMap.containsKey(type)) {
struct = customStructTypeMap.get(type);
} else if (StructSerializable.class.isAssignableFrom(type)) {
var optStruct = StructFetcher.fetchStructDynamic(type);
if (optStruct.isPresent()) {
struct = optStruct.get();
} else {
System.err.println(
"Could not structify record component: "
+ enumClass.getSimpleName()
+ "#"
+ name
+ "\n Could not extract struct from marked class: "
+ type.getName());
failed = true;
continue;
}
} else {
System.err.println(
"Could not structify record component: " + enumClass.getSimpleName() + "#" + name);
failed = true;
continue;
}
schemaBuilder.addField(name, struct.getTypeName());
size += struct.getSize();
packers.add(Packer.fromStruct(struct));
}
}
if (failed) {
return noopStruct(enumClass);
}
final int frozenSize = size;
final String schema = schemaBuilder.build();
return new Struct<>() {
@Override
public Class<E> getTypeClass() {
return enumClass;
}
@Override
public String getTypeName() {
return enumClass.getSimpleName();
}
@Override
public String getSchema() {
return schema;
}
@Override
public int getSize() {
return frozenSize;
}
@Override
public void pack(ByteBuffer buffer, E value) {
boolean failed = false;
int startingPosition = buffer.position();
buffer.put((byte) value.ordinal());
for (int i = 0; i < enumFields.size(); i++) {
Packer<Object> packer = (Packer<Object>) packers.get(i);
Field field = enumFields.get(i);
try {
Object fieldValue = field.get(value);
if (fieldValue == null) {
throw new IllegalArgumentException("Field is null");
}
packer.pack(buffer, fieldValue);
} catch (IllegalArgumentException | IllegalAccessException e) {
System.err.println(
"Could not pack enum field: "
+ enumClass.getSimpleName()
+ "#"
+ field.getName()
+ "\n "
+ e.getMessage());
failed = true;
break;
}
}
if (failed) {
buffer.position(startingPosition);
for (int i = 0; i < frozenSize; i++) {
buffer.put((byte) 0);
}
}
}
final byte[] m_spongeBuffer = new byte[frozenSize - 1];
@Override
public E unpack(ByteBuffer buffer) {
int ordinal = buffer.get();
buffer.get(m_spongeBuffer);
return enumMap.getOrDefault(ordinal, null);
}
@Override
public boolean isImmutable() {
return true;
}
};
}
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util.struct;
import edu.wpi.first.util.WPISerializable;
/**
* Marker interface to indicate a class is serializable using Struct serialization.
*
* <p>While this cannot be enforced by the interface, any class implementing this interface should
* provide a public final static `struct` member variable, or a static final `getStruct()` method if
* the class is generic.
*/
public interface StructSerializable extends WPISerializable {}

View File

@@ -0,0 +1,113 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util.struct.parser;
/** Raw struct schema lexer. */
public class Lexer {
/**
* Construct a raw struct schema lexer.
*
* @param in schema
*/
public Lexer(String in) {
m_in = in;
}
/**
* Gets the next token.
*
* @return Token kind; the token text can be retrieved using getTokenText()
*/
public TokenKind scan() {
// skip whitespace
do {
get();
} while (m_current == ' ' || m_current == '\t' || m_current == '\n' || m_current == '\r');
m_tokenStart = m_pos - 1;
return switch (m_current) {
case '[' -> TokenKind.kLeftBracket;
case ']' -> TokenKind.kRightBracket;
case '{' -> TokenKind.kLeftBrace;
case '}' -> TokenKind.kRightBrace;
case ':' -> TokenKind.kColon;
case ';' -> TokenKind.kSemicolon;
case ',' -> TokenKind.kComma;
case '=' -> TokenKind.kEquals;
case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' -> scanInteger();
case '\0' -> TokenKind.kEndOfInput;
default -> {
if (Character.isLetter(m_current) || m_current == '_') {
yield scanIdentifier();
}
yield TokenKind.kUnknown;
}
};
}
/**
* Gets the text of the last lexed token.
*
* @return token text
*/
public String getTokenText() {
if (m_tokenStart >= m_in.length()) {
return "";
}
return m_in.substring(m_tokenStart, m_pos);
}
/**
* Gets the starting position of the last lexed token.
*
* @return position (0 = first character)
*/
public int getPosition() {
return m_tokenStart;
}
private TokenKind scanInteger() {
do {
get();
} while (Character.isDigit(m_current));
unget();
return TokenKind.kInteger;
}
private TokenKind scanIdentifier() {
do {
get();
} while (Character.isLetterOrDigit(m_current) || m_current == '_');
unget();
return TokenKind.kIdentifier;
}
private void get() {
if (m_pos < m_in.length()) {
m_current = m_in.charAt(m_pos);
} else {
m_current = '\0';
}
++m_pos;
}
private void unget() {
if (m_pos > 0) {
m_pos--;
if (m_pos < m_in.length()) {
m_current = m_in.charAt(m_pos);
} else {
m_current = '\0';
}
} else {
m_current = '\0';
}
}
final String m_in;
char m_current;
int m_tokenStart;
int m_pos;
}

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.
package edu.wpi.first.util.struct.parser;
/** Exception for parsing errors. */
public class ParseException extends Exception {
/** The parser position. */
private final int m_pos;
/**
* Constructs a ParseException.
*
* @param pos The parser position.
* @param s Reason for parse failure.
*/
public ParseException(int pos, String s) {
super(s);
m_pos = pos;
}
/**
* Constructs a ParseException.
*
* @param pos The parser position.
* @param message Reason for parse failure.
* @param cause Exception that caused the parser failure.
*/
public ParseException(int pos, String message, Throwable cause) {
super(message, cause);
m_pos = pos;
}
/**
* Constructs a ParseException.
*
* @param pos The parser position.
* @param cause Exception that caused the parser failure.
*/
public ParseException(int pos, Throwable cause) {
super(cause);
m_pos = pos;
}
/**
* Returns position in parsed string.
*
* @return Position in parsed string.
*/
public int getPosition() {
return m_pos;
}
@Override
public String getMessage() {
return m_pos + ": " + super.getMessage();
}
}

View File

@@ -0,0 +1,33 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util.struct.parser;
import java.util.Map;
/** Raw struct schema declaration. */
public class ParsedDeclaration {
/** Type string. */
@SuppressWarnings("MemberName")
public String typeString;
/** Name. */
@SuppressWarnings("MemberName")
public String name;
/** Enum values. */
@SuppressWarnings("MemberName")
public Map<String, Long> enumValues;
/** Array size. */
@SuppressWarnings("MemberName")
public int arraySize = 1;
/** Bit width. */
@SuppressWarnings("MemberName")
public int bitWidth;
/** Default constructor. */
public ParsedDeclaration() {}
}

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.
package edu.wpi.first.util.struct.parser;
import java.util.ArrayList;
import java.util.List;
/** Raw struct schema. */
public class ParsedSchema {
/** Declarations. */
@SuppressWarnings("MemberName")
public List<ParsedDeclaration> declarations = new ArrayList<>();
/** Default constructor. */
public ParsedSchema() {}
}

View File

@@ -0,0 +1,157 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util.struct.parser;
import java.util.HashMap;
import java.util.Map;
/** Raw struct schema parser. */
public class Parser {
/**
* Construct a raw struct schema parser.
*
* @param in schema
*/
public Parser(String in) {
m_lexer = new Lexer(in);
}
/**
* Parses the schema.
*
* @return parsed schema object
* @throws ParseException on parse error
*/
public ParsedSchema parse() throws ParseException {
ParsedSchema schema = new ParsedSchema();
do {
getNextToken();
if (m_token == TokenKind.kSemicolon) {
continue;
}
if (m_token == TokenKind.kEndOfInput) {
break;
}
schema.declarations.add(parseDeclaration());
} while (m_token != TokenKind.kEndOfInput);
return schema;
}
private ParsedDeclaration parseDeclaration() throws ParseException {
ParsedDeclaration decl = new ParsedDeclaration();
// optional enum specification
if (m_token == TokenKind.kIdentifier && "enum".equals(m_lexer.getTokenText())) {
getNextToken();
expect(TokenKind.kLeftBrace);
decl.enumValues = parseEnum();
getNextToken();
} else if (m_token == TokenKind.kLeftBrace) {
decl.enumValues = parseEnum();
getNextToken();
}
// type name
expect(TokenKind.kIdentifier);
decl.typeString = m_lexer.getTokenText();
getNextToken();
// identifier name
expect(TokenKind.kIdentifier);
decl.name = m_lexer.getTokenText();
getNextToken();
// array or bit field
if (m_token == TokenKind.kLeftBracket) {
getNextToken();
expect(TokenKind.kInteger);
String valueStr = m_lexer.getTokenText();
int value;
try {
value = Integer.parseInt(valueStr);
} catch (NumberFormatException e) {
value = 0;
}
if (value > 0) {
decl.arraySize = value;
} else {
throw new ParseException(
m_lexer.m_pos, "array size '" + valueStr + "' is not a positive integer");
}
getNextToken();
expect(TokenKind.kRightBracket);
getNextToken();
} else if (m_token == TokenKind.kColon) {
getNextToken();
expect(TokenKind.kInteger);
String valueStr = m_lexer.getTokenText();
int value;
try {
value = Integer.parseInt(valueStr);
} catch (NumberFormatException e) {
value = 0;
}
if (value > 0) {
decl.bitWidth = value;
} else {
throw new ParseException(
m_lexer.m_pos, "bitfield width '" + valueStr + "' is not a positive integer");
}
getNextToken();
}
// declaration must end with EOF or semicolon
if (m_token != TokenKind.kEndOfInput) {
expect(TokenKind.kSemicolon);
}
return decl;
}
private Map<String, Long> parseEnum() throws ParseException {
Map<String, Long> map = new HashMap<>();
// we start with current = '{'
getNextToken();
while (m_token != TokenKind.kRightBrace) {
expect(TokenKind.kIdentifier);
final String name = m_lexer.getTokenText();
getNextToken();
expect(TokenKind.kEquals);
getNextToken();
expect(TokenKind.kInteger);
String valueStr = m_lexer.getTokenText();
long value;
try {
value = Long.parseLong(valueStr);
} catch (NumberFormatException e) {
throw new ParseException(m_lexer.m_pos, "could not parse enum value '" + valueStr + "'");
}
map.put(name, value);
getNextToken();
if (m_token == TokenKind.kRightBrace) {
break;
}
expect(TokenKind.kComma);
getNextToken();
}
return map;
}
private TokenKind getNextToken() {
m_token = m_lexer.scan();
return m_token;
}
private void expect(TokenKind kind) throws ParseException {
if (m_token != kind) {
throw new ParseException(
m_lexer.m_pos, "expected " + kind + ", got '" + m_lexer.getTokenText() + "'");
}
}
final Lexer m_lexer;
TokenKind m_token;
}

View File

@@ -0,0 +1,55 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.util.struct.parser;
/** A lexed raw struct schema token. */
public enum TokenKind {
/** Unknown. */
kUnknown("unknown"),
/** Integer. */
kInteger("integer"),
/** Identifier. */
kIdentifier("identifier"),
/** Left square bracket. */
kLeftBracket("'['"),
/** Right square bracket. */
kRightBracket("']'"),
/** Left curly brace. */
kLeftBrace("'{'"),
/** Right curly brace. */
kRightBrace("'}'"),
/** Colon. */
kColon("':'"),
/** Semicolon. */
kSemicolon("';'"),
/** Comma. */
kComma("','"),
/** Equals. */
kEquals("'='"),
/** End of input. */
kEndOfInput("<EOF>");
private final String m_name;
TokenKind(String name) {
this.m_name = name;
}
@Override
public String toString() {
return m_name;
}
}