From 237d21c75cdb2e6b13099ff9923cc3d64d716aa2 Mon Sep 17 00:00:00 2001 From: Randall Hauch Date: Mon, 2 Feb 2015 12:15:58 -0600 Subject: [PATCH] Added prototype for event and metric logging and basic executor framework. --- .../src/org/frc4931/acommand/Command.java | 16 ++ .../src/org/frc4931/acommand/CommandID.java | 88 ++++++ .../org/frc4931/acommand/CommandState.java | 16 ++ .../src/org/frc4931/executor/EventLog.java | 162 +++++++++++ .../src/org/frc4931/executor/EventLogger.java | 262 ++++++++++++++++++ .../src/org/frc4931/executor/Executor.java | 177 ++++++++++++ .../org/frc4931/executor/ExecutorOptions.java | 28 ++ .../src/org/frc4931/executor/Logger.java | 46 +++ .../src/org/frc4931/executor/Loggers.java | 103 +++++++ .../org/frc4931/executor/MappedWriter.java | 116 ++++++++ .../org/frc4931/executor/MetricLogger.java | 262 ++++++++++++++++++ .../src/org/frc4931/executor/Registry.java | 39 +++ .../org/frc4931/executor/SwitchListener.java | 154 ++++++++++ .../src/org/frc4931/robot/Robot.java | 10 +- .../src/org/frc4931/utils/FileUtil.java | 37 +++ .../src/org/frc4931/utils/Metronome.java | 5 + 16 files changed, 1519 insertions(+), 2 deletions(-) create mode 100644 RobotFramework/src/org/frc4931/acommand/Command.java create mode 100644 RobotFramework/src/org/frc4931/acommand/CommandID.java create mode 100644 RobotFramework/src/org/frc4931/acommand/CommandState.java create mode 100644 RobotFramework/src/org/frc4931/executor/EventLog.java create mode 100644 RobotFramework/src/org/frc4931/executor/EventLogger.java create mode 100644 RobotFramework/src/org/frc4931/executor/Executor.java create mode 100644 RobotFramework/src/org/frc4931/executor/ExecutorOptions.java create mode 100644 RobotFramework/src/org/frc4931/executor/Logger.java create mode 100644 RobotFramework/src/org/frc4931/executor/Loggers.java create mode 100644 RobotFramework/src/org/frc4931/executor/MappedWriter.java create mode 100644 RobotFramework/src/org/frc4931/executor/MetricLogger.java create mode 100644 RobotFramework/src/org/frc4931/executor/Registry.java create mode 100644 RobotFramework/src/org/frc4931/executor/SwitchListener.java create mode 100644 RobotFramework/src/org/frc4931/utils/FileUtil.java diff --git a/RobotFramework/src/org/frc4931/acommand/Command.java b/RobotFramework/src/org/frc4931/acommand/Command.java new file mode 100644 index 0000000..f6f9143 --- /dev/null +++ b/RobotFramework/src/org/frc4931/acommand/Command.java @@ -0,0 +1,16 @@ +/* + * FRC 4931 (http://www.evilletech.com) + * + * Open source software. Licensed under the FIRST BSD license file in the + * root directory of this project's Git repository. + */ +package org.frc4931.acommand; + +/** + * The public interface for a command. + */ +public interface Command { + + CommandID id(); + +} diff --git a/RobotFramework/src/org/frc4931/acommand/CommandID.java b/RobotFramework/src/org/frc4931/acommand/CommandID.java new file mode 100644 index 0000000..38f4205 --- /dev/null +++ b/RobotFramework/src/org/frc4931/acommand/CommandID.java @@ -0,0 +1,88 @@ +/* + * FRC 4931 (http://www.evilletech.com) + * + * Open source software. Licensed under the FIRST BSD license file in the + * root directory of this project's Git repository. + */ +package org.frc4931.acommand; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A immuntable unique identifier for a command. + */ +public final class CommandID implements Comparable{ + + private static final Map,CommandID> COMMANDS = new HashMap<>(); + private static final AtomicInteger ID = new AtomicInteger(); + + /** + * Get the identifier for the given command class with optional name. + * @param clazz the {@link Command} class; may not be null + * @param name the human-readable name of the class; if null, then the class' {@link Class#getSimpleName() simple name} will be used + * @return the identifier for the command; never null + */ + public static synchronized CommandID get( Class clazz, String name ) { + CommandID id = COMMANDS.get(clazz); + if ( id == null ) { + if ( clazz == null ) throw new IllegalArgumentException("The 'clazz' argument may not be null"); + id = new CommandID(clazz,ID.incrementAndGet(),name); + COMMANDS.put(clazz,id); + } + return id; + } + + private final Class commandClass; + private final int id; + private final String name; + + private CommandID(Class clazz, int id, String name) { + this.commandClass = clazz; + this.name = name; + this.id = id; + } + + /** + * The the name of the command. + * @return the command's name; never null + */ + public String getName() { + return name != null ? name : commandClass.getSimpleName(); + } + + /** + * Get the unique numeric identifier for this command. + * @return the numeric identifier + */ + public int asInt() { + return id; + } + + @Override + public int hashCode() { + return id; + } + + @Override + public boolean equals(Object obj) { + if ( obj instanceof CommandID ) { + return ((CommandID)obj).id == this.id; + } + return false; + } + + @Override + public String toString() { + return getName() + " (" + id + ")"; + } + + @Override + public int compareTo(CommandID that) { + if ( this == that ) return 0; + if ( that == null ) return 1; + return this.id - that.id; + } + +} diff --git a/RobotFramework/src/org/frc4931/acommand/CommandState.java b/RobotFramework/src/org/frc4931/acommand/CommandState.java new file mode 100644 index 0000000..bfcaec0 --- /dev/null +++ b/RobotFramework/src/org/frc4931/acommand/CommandState.java @@ -0,0 +1,16 @@ +/* + * FRC 4931 (http://www.evilletech.com) + * + * Open source software. Licensed under the FIRST BSD license file in the + * root directory of this project's Git repository. + */ +package org.frc4931.acommand; + +/** + * The possible states of a command. + */ +public enum CommandState { + + STARTED, RUNNING, COMPLETED, INTERRUPTED + +} diff --git a/RobotFramework/src/org/frc4931/executor/EventLog.java b/RobotFramework/src/org/frc4931/executor/EventLog.java new file mode 100644 index 0000000..5389897 --- /dev/null +++ b/RobotFramework/src/org/frc4931/executor/EventLog.java @@ -0,0 +1,162 @@ +/* + * FRC 4931 (http://www.evilletech.com) + * + * Open source software. Licensed under the FIRST BSD license file in the + * root directory of this project's Git repository. + */ +package org.frc4931.executor; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.RandomAccessFile; +import java.io.StringWriter; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.FileChannel.MapMode; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.time.ZonedDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +import org.frc4931.acommand.CommandID; +import org.frc4931.acommand.CommandState; + +/** + * + */ +public class EventLog implements AutoCloseable { + + private static final byte VERSION = 1; + + private static final byte HEADER = 1; + private static final byte SWITCH_EVENT = 2; + private static final byte MESSAGE_EVENT = 3; + private static final byte EXCEPTION_EVENT = 4; + private static final byte COMMAND_EVENT = 5; + private static final byte COMMAND_TYPE = 6; + + private final File outFile; + private final FileChannel channel; + private final MappedByteBuffer buffer; + private final long capacity; + private final Map commandsById = new HashMap<>(); + + /** + * Constructs a new {@link MappedWriter} to write to the file at the specified path. + * + * @param pathSupplier the supplier of the {@link Path path} that references the file to which this writer will write; may not + * be null + * @param size the maximum number of bytes that can be written + * @param startTime the start time of the match + * @throws IOException if an I/O error occurs + */ + public EventLog(Supplier pathSupplier, long size, LocalDateTime startTime) throws IOException { + outFile = pathSupplier.get().toFile(); + channel = new RandomAccessFile(outFile, "rw").getChannel(); + capacity = size; + buffer = channel.map(MapMode.READ_WRITE, 0, capacity); + // Write the header ... + writeHeader(startTime); + } + + private void writeHeader(LocalDateTime startTime) { + buffer.put(HEADER); + // Write the version of this format ... + buffer.put(VERSION); + // Write the starting date in seconds since epoch ... + buffer.putLong(startTime.toEpochSecond(ZonedDateTime.now().getOffset())); + } + + private void writeString(String str) { + if (str.isEmpty()) { + buffer.putShort((short) 0); + } else { + buffer.putShort((short) str.length()); + buffer.put(str.getBytes(StandardCharsets.UTF_8)); + } + } + + // private String readString() { + // int len = buffer.getShort(); + // if ( len == 0 ) return ""; + // byte[] bytes = new byte[len]; + // buffer.get(bytes, 0, len); + // return new String(bytes, 0, len); + // } + + private void writeBoolean(boolean value) { + buffer.put((byte) (value ? 1 : 0)); + } + + // private boolean readBoolean() { + // return buffer.get() == 1; + // } + + public void writeChangeInSwitch(int elapsedTimeInMillis, String switchName, boolean currentState) { + buffer.put(SWITCH_EVENT); + buffer.putInt(elapsedTimeInMillis); + writeString(switchName); + writeBoolean(currentState); + } + + public void writeMessage(int elapsedTimeInMillis, String message, Throwable error) { + if (error == null) { + buffer.put(MESSAGE_EVENT); + buffer.putInt(elapsedTimeInMillis); + writeString(message); + } else { + buffer.put(EXCEPTION_EVENT); + buffer.putInt(elapsedTimeInMillis); + writeString(message); + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + error.printStackTrace(pw); + writeString(sw.toString()); // stack trace as a string + } + } + + public void writeChangeInCommandState(int elapsedTimeInMillis, CommandID commandId, CommandState state) { + if (!commandsById.containsKey(commandId.asInt())) { + commandsById.put(commandId.asInt(), commandId); + buffer.put(COMMAND_TYPE); + buffer.putInt(commandId.asInt()); + writeString(commandId.getName()); + } + buffer.put(COMMAND_EVENT); + buffer.putInt(elapsedTimeInMillis); + buffer.putInt(commandId.asInt()); + buffer.put((byte) state.ordinal()); + } + + public int remaining() { + return buffer.remaining(); + } + + /** + * Writes the terminator, forces the OS to update the file + * and closes the {@code Channel}. + */ + @Override + public void close(){ + try { + // Write terminator + buffer.putInt(0xFFFFFFFF); + } finally { + try { + // And always force the buffer ... + buffer.force(); + } finally { + try{ + // And always close the channel ... + channel.close(); + } catch (IOException e) { + throw new RuntimeException("Failed to close channel",e); + } + } + } + } +} diff --git a/RobotFramework/src/org/frc4931/executor/EventLogger.java b/RobotFramework/src/org/frc4931/executor/EventLogger.java new file mode 100644 index 0000000..dcc3360 --- /dev/null +++ b/RobotFramework/src/org/frc4931/executor/EventLogger.java @@ -0,0 +1,262 @@ +/* + * FRC 4931 (http://www.evilletech.com) + * + * Open source software. Licensed under the FIRST BSD license file in the + * root directory of this project's Git repository. + */ +package org.frc4931.executor; + +import java.io.IOException; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.frc4931.acommand.CommandID; +import org.frc4931.acommand.CommandState; +import org.frc4931.executor.Executor.Executable; +import org.frc4931.robot.component.Switch; +import org.frc4931.utils.FileUtil; + +/** + * A public interface for recording events. + */ +final class EventLogger implements Executable { + + protected static int ESTIMATED_BYTES_PER_SECOND = 16 * 1024; + + static interface EventStream { + void writeChangeInSwitch(int elapsedTimeInMillis, String switchName, boolean currentState); + void writeMessage(int elapsedTimeInMillis, String message, Throwable error); + void writeChangeInCommandState(int elapsedTimeInMillis, CommandID commandId, CommandState state); + void close(); + } + + private final List switchCheckers = new ArrayList<>(); + private final Set registeredSwitches = new HashSet<>(); + private final EventStream stream; + + public EventLogger(ExecutorOptions options) { + assert options != null; + String logPrefix = options.getLogFilenamePrefix(); + if (logPrefix != null) { + stream = new LocalEventStream(options); + } else { + stream = new RemoteEventStream(options); + } + } + + public void shutdown() { + } + + @Override + public void run( int elapsedTimeInMillis ) { + for (Executable checker : switchCheckers) { + checker.run(elapsedTimeInMillis); + } + } + + /** + * Registers a {@link Switch} to be monitored so that changes in its {@link Switch#isTriggered() triggered state} are + * automatically recorded in the event log. + * + * @param name the name of the {@link Switch}; may not be null + * @param swtch the {@link Switch} to be logged; may not be null + */ + public synchronized void registerSwitch(String name, Switch swtch) { + assert name != null; + assert swtch != null; + if (!registeredSwitches.contains(swtch)) { + registeredSwitches.add(swtch); + String switchName = name; + switchCheckers.add(new Executable() { + boolean state = swtch.isTriggered(); + + @Override + public void run(int elapsedTimeInMillis) { + if (swtch.isTriggered() != state) { + state = !state; + // The state has changed, so log it ... + recordChangeInSwitch(elapsedTimeInMillis, switchName, state); + } + } + }); + } + } + + protected void recordChangeInSwitch(int currentTimeInNanos, String switchName, boolean currentState) { + stream.writeChangeInSwitch(currentTimeInNanos,switchName,currentState); + } + + public void record(int currentTimeInNanos, String message, Throwable error) { + stream.writeMessage(currentTimeInNanos,message,error); + } + + public void record(int currentTimeInNanos, CommandID commandId, CommandState state) { + stream.writeChangeInCommandState(currentTimeInNanos,commandId,state); + } + + protected static final class ConsoleEventStream implements EventStream { + private final EventStream delegate; + protected ConsoleEventStream(EventStream delegate ) { + this.delegate = delegate; + } + + private StringBuilder withElapsedTime( int elapsedTime ) { + float seconds = elapsedTime / 1000; + int minutes = 0; + while ( seconds > 60 ) { + ++minutes; + seconds -= 60; + } + StringBuilder sb = new StringBuilder(); + sb.append(minutes); + sb.append(":"); + sb.append(seconds); + sb.append(", "); + return sb; + } + + @Override + public void writeChangeInCommandState(int elapsedTimeInMillis, CommandID commandId, CommandState state) { + try { + try { + delegate.writeChangeInCommandState(elapsedTimeInMillis,commandId,state); + } finally { + StringBuilder sb = withElapsedTime(elapsedTimeInMillis); + sb.append(commandId.getName()).append(", "); + sb.append(state.toString()); + System.out.println(sb); + } + } catch ( Throwable t ) { + // Only write this to the console, since it failed when writing to the delegate ... + System.out.println("Failed to log event:"); + t.printStackTrace(System.out); + } + } + + @Override + public void writeChangeInSwitch(int elapsedTimeInMillis, String switchName, boolean currentState) { + try { + try { + delegate.writeChangeInSwitch(elapsedTimeInMillis,switchName,currentState); + } finally { + StringBuilder sb = withElapsedTime(elapsedTimeInMillis); + sb.append(switchName).append(", "); + sb.append(currentState?"on":"off"); + System.out.println(sb); + } + } catch ( Throwable t ) { + // Only write this to the console, since it failed when writing to the delegate ... + System.out.println("Failed to log event:"); + t.printStackTrace(System.out); + } + } + + @Override + public void writeMessage(int elapsedTimeInMillis, String message, Throwable error) { + try { + try { + delegate.writeMessage(elapsedTimeInMillis,message,error); + } finally { + StringBuilder sb = withElapsedTime(elapsedTimeInMillis); + if ( message != null ) { + sb.append(message); + } else if ( error != null ){ + sb.append(error.getMessage()); + } + System.out.println(sb); + if ( error != null ) error.printStackTrace(System.out); + } + } catch ( Throwable t ) { + // Only write this to the console, since it failed when writing to the delegate ... + System.out.println("Failed to log event:"); + t.printStackTrace(System.out); + } + } + @Override + public void close() { + } + } + + protected static final class LocalEventStream implements EventStream { + private EventLog writer; + private final String filePrefix; + private final long fileSize; + private final LocalDateTime started; + + protected LocalEventStream( ExecutorOptions options ) { + filePrefix = options.getLogFilenamePrefix(); + fileSize = options.getTotalEstimatedDurationInSeconds() * ESTIMATED_BYTES_PER_SECOND; + started = LocalDateTime.now(); + } + + private Path newFilename() { + return FileUtil.namedWithTimestamp(LocalTime.now(), filePrefix, "-events.log"); + } + + protected void open() { + try { + writer = new EventLog(this::newFilename, fileSize, started); + } catch (IOException e) { + System.err.println("Failed to open event log file."); + e.printStackTrace(); + } + } + + @Override + public void writeChangeInCommandState(int elapsedTimeInMillis, CommandID commandId, CommandState state) { + checkRemaining(); + writer.writeChangeInCommandState(elapsedTimeInMillis, commandId, state); + } + + @Override + public void writeChangeInSwitch(int elapsedTimeInMillis, String switchName, boolean currentState) { + checkRemaining(); + writer.writeChangeInSwitch(elapsedTimeInMillis, switchName, currentState); + } + + @Override + public void writeMessage(int elapsedTimeInMillis, String message, Throwable error) { + checkRemaining(); + writer.writeMessage(elapsedTimeInMillis, message, error); + } + @Override + public void close() { + writer.close(); + } + + private void checkRemaining() { + if ( writer.remaining() < ESTIMATED_BYTES_PER_SECOND ) { + close(); + open(); + } + } + } + + protected static final class RemoteEventStream implements EventStream { + protected RemoteEventStream( ExecutorOptions options ) { + + } + + @Override + public void writeChangeInCommandState(int elapsedTimeInMillis, CommandID commandId, CommandState state) { + } + + @Override + public void writeChangeInSwitch(int elapsedTimeInMillis, String switchName, boolean currentState) { + } + + @Override + public void writeMessage(int elapsedTimeInMillis, String message, Throwable error) { + } + + @Override + public void close() { + } + } + +} diff --git a/RobotFramework/src/org/frc4931/executor/Executor.java b/RobotFramework/src/org/frc4931/executor/Executor.java new file mode 100644 index 0000000..afc2a89 --- /dev/null +++ b/RobotFramework/src/org/frc4931/executor/Executor.java @@ -0,0 +1,177 @@ +/* + * FRC 4931 (http://www.evilletech.com) + * + * Open source software. Licensed under the FIRST BSD license file in the + * root directory of this project's Git repository. + */ +package org.frc4931.executor; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.frc4931.acommand.Command; +import org.frc4931.robot.Robot; +import org.frc4931.utils.Metronome; + +/** + * + */ +public final class Executor { + + @FunctionalInterface + public static interface ExecutorInitializer { + /** + * Initialization function that should be run before the executor is started. + * + * @param registry the logger registry that can be used to register motors, switches, and other components; never null + */ + void initialize(Registry registry); + } + + private static Executor INSTANCE; + + /** + * Begin the executor's startup sequence and operation. + * + * @param options the executor's configuration options; may be null if the default options are to be used + * @param initializer the initializer; may be null if nothing is to be initialized + */ + public static synchronized void initialize(ExecutorOptions options, ExecutorInitializer initializer) { + if (INSTANCE == null) { + INSTANCE = new Executor(options, initializer); + } + } + + /** + * Get the log associated with this executor. + * + * @return the log; never null + */ + public static Logger log() { + try { + return INSTANCE.loggers; + } catch (NullPointerException e) { + throw new IllegalStateException("The Executor must be initialized before 'log' can be called"); + } + } + + /** + * Submit the given command for execution. + * + * @param command the new command to be executed + */ + public static void submit(Command command) { + if (command != null) { + try { + INSTANCE.submitCommand(command); + } catch (NullPointerException e) { + throw new IllegalStateException("The Executor must be initialized before 'submit' can be called"); + } + } + } + + /** + * Shut down this executor, blocking until all resources have been stopped. + * + * @return {@code true} if the executor and all resources were stopped successfully, or {@code false} if the executor could + * not be stopped before the time limit elapsed or if this thread has been interrupted while waiting for the executor + * to stop + */ + public static synchronized boolean shutdown() { + try { + return INSTANCE.stop(); + } finally { + INSTANCE = null; + } + } + + /** + * Shut down this executor, blocking at most for the given timeout while all resources are stopped. + * + * @param timeout the maximum time to wait + * @param unit the time unit of the {@code timeout} argument + * @return {@code true} if the executor and all resources were stopped successfully, or {@code false} if the executor could + * not be stopped before the time limit elapsed or if this thread has been interrupted while waiting for the executor + * to stop + */ + public static synchronized boolean shutdown(long timeout, TimeUnit unit) { + try { + return INSTANCE.stop(timeout, unit); + } finally { + INSTANCE = null; + } + } + + static interface Executable { + /** + * Run this executable. + * + * @param elapsedTimeInMillis the elapsed match time in milliseconds + */ + void run(int elapsedTimeInMillis); + } + + private final Loggers loggers; + private final Metronome metronome; + private final Thread thread; + private final CountDownLatch latch = new CountDownLatch(1); + private volatile boolean keepRunning = true; + + private Executor(ExecutorOptions options, ExecutorInitializer initializer) { + metronome = new Metronome(options.getFrequency(), options.getFrequencyUnits()); + loggers = new Loggers(options, Robot::elapsedTimeInMillis); + if (initializer != null) initializer.initialize(loggers); + loggers.startup(); + thread = new Thread(this::run); + thread.setName("Robot exec"); + thread.start(); + } + + protected void run() { + try { + while (keepRunning) { + if (!metronome.pause()) return; + + // run the commands ... + + // Log the data ... + loggers.run(Robot.elapsedTimeInMillis()); + } + } finally { + latch.countDown(); + } + } + + /** + * Submit a command for execution. + * + * @param command the command; may not be null + */ + protected void submitCommand(Command command) { + // TODO complete + } + + protected boolean stop() { + keepRunning = false; + loggers.shutdown(); + try { + latch.await(); + return true; + } catch (InterruptedException e) { + Thread.interrupted(); + return false; + } + } + + protected boolean stop(long timeout, TimeUnit timeUnit) { + keepRunning = false; + loggers.shutdown(); + try { + return latch.await(timeout, timeUnit); + } catch (InterruptedException e) { + Thread.interrupted(); + return false; + } + } + +} diff --git a/RobotFramework/src/org/frc4931/executor/ExecutorOptions.java b/RobotFramework/src/org/frc4931/executor/ExecutorOptions.java new file mode 100644 index 0000000..3118db8 --- /dev/null +++ b/RobotFramework/src/org/frc4931/executor/ExecutorOptions.java @@ -0,0 +1,28 @@ +/* + * FRC 4931 (http://www.evilletech.com) + * + * Open source software. Licensed under the FIRST BSD license file in the + * root directory of this project's Git repository. + */ +package org.frc4931.executor; + +import java.util.concurrent.TimeUnit; + +/** + * + */ +public interface ExecutorOptions { + + public static final int WRITE_FREQUENCY = (int) (1000.0/30.0); // milliseconds per writes + public static final int RUNNING_TIME = 200; + + + int getFrequency(); + + TimeUnit getFrequencyUnits(); + + int getTotalEstimatedDurationInSeconds(); + + String getLogFilenamePrefix(); + +} diff --git a/RobotFramework/src/org/frc4931/executor/Logger.java b/RobotFramework/src/org/frc4931/executor/Logger.java new file mode 100644 index 0000000..a4c0d29 --- /dev/null +++ b/RobotFramework/src/org/frc4931/executor/Logger.java @@ -0,0 +1,46 @@ +/* + * FRC 4931 (http://www.evilletech.com) + * + * Open source software. Licensed under the FIRST BSD license file in the + * root directory of this project's Git repository. + */ +package org.frc4931.executor; + +import org.frc4931.acommand.CommandID; +import org.frc4931.acommand.CommandState; + +/** + * + */ +public interface Logger { + + /** + * Record a message to the log. This method does nothing if the {@code message} is {@code null}. + * + * @param message the message + */ + void record(String message); + + /** + * Record an exception and a message to the log. This method does nothing if the {@code message} and exception are both + * {@code null}. + * + * @param message the message + * @param error the exception + */ + void record(String message, Throwable error); + + /** + * Record an exception to the log. This method does nothing if the {@code error} is {@code null}. + * + * @param error the exception + */ + void record(Throwable error); + + /** + * Record the change in state of a command. + * @param command the identifier of the command; may not be null + * @param state the new state of the command; may not be null + */ + void record(CommandID command, CommandState state); +} diff --git a/RobotFramework/src/org/frc4931/executor/Loggers.java b/RobotFramework/src/org/frc4931/executor/Loggers.java new file mode 100644 index 0000000..0b63374 --- /dev/null +++ b/RobotFramework/src/org/frc4931/executor/Loggers.java @@ -0,0 +1,103 @@ +/* + * FRC 4931 (http://www.evilletech.com) + * + * Open source software. Licensed under the FIRST BSD license file in the + * root directory of this project's Git repository. + */ +package org.frc4931.executor; + +import java.util.function.IntSupplier; + +import org.frc4931.acommand.CommandID; +import org.frc4931.acommand.CommandState; +import org.frc4931.executor.Executor.Executable; +import org.frc4931.robot.component.Motor; +import org.frc4931.robot.component.Switch; +import org.frc4931.utils.Lifecycle; + +/** + * + */ +final class Loggers implements Lifecycle, Logger, Executable, Registry { + + private final MetricLogger dataLogger; + private final EventLogger eventLogger; + private final IntSupplier robotElapsedTimeSupplier; + + public Loggers( ExecutorOptions options, IntSupplier timeSupplier ) { + dataLogger = new MetricLogger(options); + eventLogger = new EventLogger(options); + robotElapsedTimeSupplier = timeSupplier; + } + + @Override + public void startup() { + } + + @Override + public void run( int elapsedTimeInMillis) { + dataLogger.run(elapsedTimeInMillis); + eventLogger.run(elapsedTimeInMillis); + } + + @Override + public void shutdown() { + try { + dataLogger.shutdown(); + } finally { + eventLogger.shutdown(); + } + } + + /** + * Registers the value of the specified {@link IntSupplier} to be logged. This method should be called before the + * @param name the name of this data point; may not be null + * @param supplier the {@link IntSupplier} of the value to be logged; may not be null + */ + @Override + public void register(String name, IntSupplier supplier) { + dataLogger.register(name,supplier); + } + + /** + * Registers a {@link Motor} to be logged. + * @param name the name of the {@link Motor}; may not be null + * @param motor the {@link Motor} to be logged; may not be null + */ + @Override + public void registerMotor(String name, Motor motor) { + dataLogger.registerMotor(name, motor); + } + + /** + * Registers a {@link Switch} to be logged. + * @param name the name of the {@link Switch}; may not be null + * @param swtch the {@link Switch} to be logged; may not be null + */ + @Override + public void registerSwitch(String name, Switch swtch) { + eventLogger.registerSwitch(name, swtch); + } + + @Override + public void record( String message ) { + eventLogger.record(robotElapsedTimeSupplier.getAsInt(), message,null); + } + + @Override + public void record( String message, Throwable error ) { + eventLogger.record(robotElapsedTimeSupplier.getAsInt(),message,error); + } + + @Override + public void record(Throwable error) { + eventLogger.record(robotElapsedTimeSupplier.getAsInt(),null,error); + } + + @Override + public void record( CommandID command, CommandState state ) { + eventLogger.record(robotElapsedTimeSupplier.getAsInt(),command,state); + } + + +} diff --git a/RobotFramework/src/org/frc4931/executor/MappedWriter.java b/RobotFramework/src/org/frc4931/executor/MappedWriter.java new file mode 100644 index 0000000..182a341 --- /dev/null +++ b/RobotFramework/src/org/frc4931/executor/MappedWriter.java @@ -0,0 +1,116 @@ +/* + * FRC 4931 (http://www.evilletech.com) + * + * Open source software. Licensed under the FIRST BSD license file in the + * root directory of this project's Git repository. + */ +package org.frc4931.executor; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.FileChannel.MapMode; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.function.Supplier; + +/** + * Provides easy methods for creation and writing to of a memory mapped file. + */ +public final class MappedWriter implements AutoCloseable { + + private final File outFile; + private final FileChannel channel; + private final MappedByteBuffer buffer; + private final long capacity; + + /** + * Constructs a new {@link MappedWriter} to write to the file at the specified path. + * @param pathSupplier the supplier of the {@link Path path} that references the file to which this writer will write; may not be null + * @param size the maximum number of bytes that can be written + * @throws IOException if an I/O error occurs + */ + public MappedWriter(Supplier pathSupplier, long size) throws IOException{ + outFile = pathSupplier.get().toFile(); + channel = new RandomAccessFile(outFile, "rw").getChannel(); + capacity = size; + buffer = channel.map(MapMode.READ_WRITE, 0, capacity); + } + + /** + * Constructs a new {@link MappedWriter} to write to the specified file. + * @param filename the path to the file to be written to + * @param size the maximum number of bytes that can be written + * @throws IOException if an I/O error occurs + */ + public MappedWriter(String filename, long size) throws IOException{ + this(()->Paths.get(filename),size); + } + + /** + * Writes the specified {@code byte} to the next position in the file. + * @param data the {@code byte} to write + */ + public void write(byte data) { + buffer.put(data); + } + + /** + * Writes the specified {@code byte}s to the next {@code data.length} positions in + * the file. + * @param data the {@code byte}s to write + */ + public void write(byte[] data) { + buffer.put(data); + } + + /** + * Writes the specified {@code short} to the next 2 positions in the file. + * @param data the {@code short} to write + */ + public void writeShort(short data) { + buffer.putShort(data); + } + + /** + * Writes the specified {@code int} to the next 4 positions in the file. + * @param d the {@code int} to write + */ + public void writeInt(int d) { + buffer.putInt(d); + } + + /** + * Gets the number of bytes left in this file. + * @return the number of writable bytes left + */ + public long getRemaining(){ + return buffer.remaining(); + } + + /** + * Writes the terminator, forces the OS to update the file + * and closes the {@code Channel}. + */ + @Override + public void close(){ + try { + // Write terminator + buffer.putInt(0xFFFFFFFF); + } finally { + try { + // And always force the buffer ... + buffer.force(); + } finally { + try{ + // And always close the channel ... + channel.close(); + } catch (IOException e) { + throw new RuntimeException("Failed to close channel",e); + } + } + } + } +} diff --git a/RobotFramework/src/org/frc4931/executor/MetricLogger.java b/RobotFramework/src/org/frc4931/executor/MetricLogger.java new file mode 100644 index 0000000..7f4776c --- /dev/null +++ b/RobotFramework/src/org/frc4931/executor/MetricLogger.java @@ -0,0 +1,262 @@ +/* + * FRC 4931 (http://www.evilletech.com) + * + * Open source software. Licensed under the FIRST BSD license file in the + * root directory of this project's Git repository. + */ +package org.frc4931.executor; + +import java.io.IOException; +import java.nio.file.Path; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.function.IntSupplier; + +import org.frc4931.executor.Executor.Executable; +import org.frc4931.robot.component.Motor; +import org.frc4931.robot.component.Switch; +import org.frc4931.utils.FileUtil; + +import edu.wpi.first.wpilibj.livewindow.LiveWindowSendable; +import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; +import edu.wpi.first.wpilibj.tables.ITable; + +/** + * The metrics logging service. + */ +class MetricLogger implements Executable { + + private final List suppliers = new ArrayList<>(); + private final List names = new ArrayList<>(); + private final DataLogWriter logger; + + protected MetricLogger(ExecutorOptions options) { + assert options != null; + String logPrefix = options.getLogFilenamePrefix(); + if (logPrefix != null) { + logger = new LocalDataLogWriter(options, names, suppliers); + } else { + logger = new RemoteDataLogWriter(options, names, suppliers); + } + } + + @Override + public void run(int elapsedTimeInMillis) { + logger.write(elapsedTimeInMillis); + } + + /** + * Stops logging data, kills the thread, closes files, and frees resources. + */ + public synchronized void shutdown() { + logger.close(); + } + + /** + * Registers the value of the specified {@link IntSupplier} to be logged + * + * @param name the name of this data point + * @param supplier the {@link IntSupplier} of the value to be logged + * @throws IllegalArgumentException if the {@code supplier} parameter is null + */ + public synchronized void register(String name, IntSupplier supplier) { + if (supplier == null) throw new IllegalArgumentException("The supplier may not be null"); + names.add(name); + suppliers.add(supplier); + } + + /** + * Registers a {@link Switch} to be logged. + * + * @param name the name of the {@link Switch} + * @param swtch the {@link Switch} to be logged + * @throws IllegalArgumentException if the {@code swtch} parameter is null + */ + public synchronized void registerSwitch(String name, Switch swtch) { + if (swtch == null) throw new IllegalArgumentException("The supplier may not be null"); + names.add(name); + suppliers.add(() -> swtch.isTriggered() ? 1 : 0); + } + + /** + * Registers a {@link Motor} to be logged. + * + * @param name the name of the {@link Motor} + * @param motor the {@link Motor} to be logged + * @throws IllegalArgumentException if the {@code motor} parameter is null + */ + public synchronized void registerMotor(String name, Motor motor) { + names.add(name + " speed"); + suppliers.add(() -> (short) (motor.getSpeed() * 1000)); + } + + /** + * Combines an array of {@code boolean}s into a single {@code short}. + * + * @param values the {@code boolean}s to be combined + * @return a {@code short} where the value of each byte represents a single boolean + */ + public static short bitmask(boolean[] values) { + if (values.length > 15) + throw new IllegalArgumentException("Cannot combine more than 15 booleans"); + short value = 0; + for (int i = 0; i < values.length; i++) + value = (short) (value | ((values[i] ? 1 : 0) << i)); + return value; + } + + protected static interface DataLogWriter extends AutoCloseable { + /** + * Writes the current status of the robot to a log. + * @param elapsedTimeInMillis the elapsed match time in milliseconds + */ + public void write( int elapsedTimeInMillis); + + /** + * Frees the resources used by this {@link DataLogWriter}. + */ + @Override + public void close(); + } + + protected static class LocalDataLogWriter implements DataLogWriter { + private final List suppliers; + private final List names; + private final ExecutorOptions options; + private long recordLength; + private MappedWriter writer; + + public LocalDataLogWriter(ExecutorOptions options, List names, + List suppliers) { + this.suppliers = suppliers; + this.names = names; + this.options = options; + } + + private Path newFilename() { + return FileUtil.namedWithTimestamp(LocalTime.now(), options.getLogFilenamePrefix(), "-metrics.log"); + } + + protected void open() { + try { + // Estimate minimum file size needed to run at WRITE_FREQUENCY for RUNNING_TIME + // Reciprocal of WRITE_FREQUENCY is frames per millisecond, time is seconds + long frequencyInMillis = options.getFrequencyUnits().toMillis(options.getFrequency()); + long runningTime = options.getTotalEstimatedDurationInSeconds(); + int numWrites = (int) ((1.0 / frequencyInMillis) * (runningTime * 1000)); + + // Infrastructure for variable element length + recordLength = Integer.BYTES; + recordLength += (Short.BYTES * suppliers.size()); + + long minSize = numWrites * recordLength; + + writer = new MappedWriter(this::newFilename, minSize + 1024); // Extra KB of room + + // Write the header + writer.write("log".getBytes()); + + // Write the number of elements + writer.write((byte) (suppliers.size() + 1)); + + // Write the size of each element (Infrastructure for variable length element) + writer.write((byte) Integer.BYTES); + + for (IntSupplier supplier : suppliers) { + assert supplier != null; + writer.write((byte) Short.BYTES); + } + + // Write length of each element name and the name itself + writer.write((byte) 4); + writer.write(("Time").getBytes()); + + for (String name : names) { + writer.write((byte) name.length()); + writer.write(name.getBytes()); + } + } catch (IOException e) { + System.err.println("Failed to open metrics log file."); + e.printStackTrace(); + } + } + + @Override + public void write( int elapsedTimeInMillis) { + if (writer.getRemaining() < recordLength) { + try { + writer.close(); + open(); + } catch ( Throwable t ) { + System.err.println("Insuffient space to write next all of next record, and error while closing file and opening new file"); + t.printStackTrace(System.err); + } + } + writer.writeInt(elapsedTimeInMillis); + suppliers.forEach((supplier) -> writer.writeShort((short) supplier.getAsInt())); + } + + @Override + public void close() { + writer.close(); + } + + } + + protected static class RemoteDataLogWriter implements DataLogWriter, LiveWindowSendable { + private final List names; + private final List suppliers; + + public RemoteDataLogWriter(ExecutorOptions options, List names, + List suppliers) { + this.names = names; + this.suppliers = suppliers; + } + + @Override + public void write( int elapsedTimeInMillis) { + SmartDashboard.putData("Metrics", this); + } + + // SmartDashboard stuff + private ITable table; + + @Override + public void initTable(ITable subtable) { + table = subtable; + updateTable(); + } + + @Override + public ITable getTable() { + return table; + } + + @Override + public void updateTable() { + Iterator nameIterator = names.iterator(); + Iterator supplierIterator = suppliers.iterator(); + while (nameIterator.hasNext() && supplierIterator.hasNext()) + table.putNumber(nameIterator.next(), supplierIterator.next().getAsInt()); + } + + @Override + public String getSmartDashboardType() { + return "DataLupdate();ogger"; + } + + @Override + public void startLiveWindowMode() { + } + + @Override + public void stopLiveWindowMode() { + } + + @Override + public void close() { + } + } +} diff --git a/RobotFramework/src/org/frc4931/executor/Registry.java b/RobotFramework/src/org/frc4931/executor/Registry.java new file mode 100644 index 0000000..c8524f7 --- /dev/null +++ b/RobotFramework/src/org/frc4931/executor/Registry.java @@ -0,0 +1,39 @@ +/* + * FRC 4931 (http://www.evilletech.com) + * + * Open source software. Licensed under the FIRST BSD license file in the + * root directory of this project's Git repository. + */ +package org.frc4931.executor; + +import java.util.function.IntSupplier; + +import org.frc4931.robot.component.Motor; +import org.frc4931.robot.component.Switch; + +/** + * + */ +public interface Registry { + + /** + * Registers the value of the specified {@link IntSupplier} to be logged. This method should be called before the + * @param name the name of this data point; may not be null + * @param supplier the {@link IntSupplier} of the value to be logged; may not be null + */ + void register(String name, IntSupplier supplier); + + /** + * Registers a {@link Motor} to be logged. + * @param name the name of the {@link Motor}; may not be null + * @param motor the {@link Motor} to be logged; may not be null + */ + void registerMotor(String name, Motor motor); + + /** + * Registers a {@link Switch} to be logged. + * @param name the name of the {@link Switch}; may not be null + * @param swtch the {@link Switch} to be logged; may not be null + */ + void registerSwitch(String name, Switch swtch); +} diff --git a/RobotFramework/src/org/frc4931/executor/SwitchListener.java b/RobotFramework/src/org/frc4931/executor/SwitchListener.java new file mode 100644 index 0000000..b6e5b79 --- /dev/null +++ b/RobotFramework/src/org/frc4931/executor/SwitchListener.java @@ -0,0 +1,154 @@ +/* + * FRC 4931 (http://www.evilletech.com) + * + * Open source software. Licensed under the FIRST BSD license file in the + * root directory of this project's Git repository. + */ +package org.frc4931.executor; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.frc4931.executor.Executor.Executable; +import org.frc4931.robot.component.Switch; + +/** + * A class that allows {@link ListenerFunction}s to be registered with {@link Switch}es + * to execute on a certain change of state. + */ +public class SwitchListener implements Executable { + private final ConcurrentMap listeners; + + protected SwitchListener() { + listeners = new ConcurrentHashMap<>(); + } + + @Override + public void run(int elapsedTimeInMillis) { + listeners.forEach((swtch, container)->container.update(swtch)); + } + + /** + * Stops monitoring the register listeners. + */ + public void shutdown(){ + } + + /** + * Register a {@link ListenerFunction} to be called the moment when the specified + * {@link Switch} is triggered. + * @param swtch the {@link Switch} to bind the command to + * @param function the {@link ListenerFunction} to execute + */ + public void onTriggered(Switch swtch, ListenerFunction function){ + listeners.putIfAbsent(swtch, new Container()).addWhenTriggered(function); + } + + /** + * Register a {@link ListenerFunction} to be called the moment when the specified + * {@link Switch} is untriggered. + * @param swtch the {@link Switch} to bind the command to + * @param function the {@link ListenerFunction} to execute + */ + public void onUntriggered(Switch swtch, ListenerFunction function){ + listeners.putIfAbsent(swtch, new Container()).addWhenUntriggered(function); + } + + /** + * Register a {@link ListenerFunction} to be called repeatedly while the specified + * {@link Switch} is triggered. + * @param swtch the {@link Switch} to bind the command to + * @param function the {@link ListenerFunction} to execute + */ + public void whileTriggered(Switch swtch, ListenerFunction function){ + listeners.putIfAbsent(swtch, new Container()).addWhileTriggered(function); + } + + /** + * Register a {@link ListenerFunction} to be called repeatedly while the specified + * {@link Switch} is not triggered. + * @param swtch the {@link Switch} to bind the command to + * @param function the {@link ListenerFunction} to execute + */ + public void whileUntriggered(Switch swtch, ListenerFunction function){ + listeners.putIfAbsent(swtch, new Container()).addWhileUntriggered(function); + } + + private static final class Container{ + private boolean previousState; + private Node whenTriggered; + private Node whenUntriggered; + private Node whileTriggered; + private Node whileUntriggered; + + public Container(){} + + public void update(Switch swtch) { + if(whenTriggered!=null) + // Went from not triggered to triggered + if(swtch.isTriggered()&&!previousState) + whenTriggered.fire(); + + if(whenUntriggered!=null) + // Went from triggered to not triggered + if(!swtch.isTriggered()&&previousState) + whenUntriggered.fire(); + + if(whileTriggered!=null) + // Switch was and is triggered + if(swtch.isTriggered()&&previousState) + whileTriggered.fire(); + + if(whileUntriggered!=null) + // Switch wasn't and still isn't triggered + if(!swtch.isTriggered()&&!previousState) + whileUntriggered.fire(); + + previousState = swtch.isTriggered(); + } + + public void addWhenTriggered(ListenerFunction function) { + whenTriggered = new Node(function, whenTriggered); + } + + public void addWhenUntriggered(ListenerFunction function) { + whenUntriggered = new Node(function, whenUntriggered); + } + + public void addWhileTriggered(ListenerFunction function) { + whileTriggered = new Node(function, whileTriggered); + } + + public void addWhileUntriggered(ListenerFunction function) { + whileUntriggered = new Node(function, whileUntriggered); + } + + + } + + protected static final class Node{ + private ListenerFunction function; + private Node next; + + public Node(ListenerFunction function, Node next){ + this.function = function; + this.next = next; + } + + public void fire(){ + function.execute(); + if(next!=null) + next.fire(); + } + } + + /** + * Functional interface that can be registered with {@link SwitchListener} to execute a + * statement on a certain event. + */ + @FunctionalInterface + public static interface ListenerFunction { + public void execute(); + } + +} diff --git a/RobotFramework/src/org/frc4931/robot/Robot.java b/RobotFramework/src/org/frc4931/robot/Robot.java index b4e6bd3..5e772ba 100644 --- a/RobotFramework/src/org/frc4931/robot/Robot.java +++ b/RobotFramework/src/org/frc4931/robot/Robot.java @@ -6,8 +6,6 @@ */ package org.frc4931.robot; -import edu.wpi.first.wpilibj.IterativeRobot; -import edu.wpi.first.wpilibj.PowerDistributionPanel; import org.frc4931.robot.component.DataStream; import org.frc4931.robot.component.Motor; import org.frc4931.robot.component.MotorWithAngle; @@ -22,10 +20,14 @@ import org.frc4931.robot.subsystem.VisionSystem; import org.frc4931.utils.Lifecycle; +import edu.wpi.first.wpilibj.IterativeRobot; +import edu.wpi.first.wpilibj.PowerDistributionPanel; + public class Robot extends IterativeRobot { public static final int NUMBER_OF_ADC_BITS = 12; private static Robot instance; private static long startTime = System.nanoTime(); + private static long startTimeInMillis = System.currentTimeMillis(); private Systems systems; private OperatorInterface operatorInterface; @@ -92,6 +94,10 @@ public static long time() { return System.nanoTime()-startTime; } + public static int elapsedTimeInMillis() { + return (int)(System.currentTimeMillis()-startTimeInMillis); + } + public static final class Systems implements Lifecycle { public final DriveSystem drive; public final LoaderArm grabber; diff --git a/RobotFramework/src/org/frc4931/utils/FileUtil.java b/RobotFramework/src/org/frc4931/utils/FileUtil.java new file mode 100644 index 0000000..0aeb11f --- /dev/null +++ b/RobotFramework/src/org/frc4931/utils/FileUtil.java @@ -0,0 +1,37 @@ +/* + * FRC 4931 (http://www.evilletech.com) + * + * Open source software. Licensed under the FIRST BSD license file in the + * root directory of this project's Git repository. + */ +package org.frc4931.utils; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; + +/** + * File utilities. + */ +public final class FileUtil { + + private static DateTimeFormatter MINUTE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-ddTHHmm"); + + public static Path namedWithTimestamp( LocalTime timestamp, String prefix, String suffix ) { + return namedWithTimestamp(timestamp,Paths.get("."), prefix, suffix); + } + + public static Path namedWithTimestamp( LocalTime timestamp, Path directory, String prefix, String suffix ) { + assert directory != null; + StringBuilder filename = new StringBuilder(); + if ( prefix != null ) filename.append(prefix); + filename.append(timestamp.format(MINUTE_FORMATTER)); + if ( suffix != null ) filename.append(suffix); + return directory.resolve(filename.toString()); + } + + private FileUtil() { + } + +} diff --git a/RobotFramework/src/org/frc4931/utils/Metronome.java b/RobotFramework/src/org/frc4931/utils/Metronome.java index 3419bb1..1ed2192 100644 --- a/RobotFramework/src/org/frc4931/utils/Metronome.java +++ b/RobotFramework/src/org/frc4931/utils/Metronome.java @@ -18,6 +18,11 @@ public final class Metronome { private long next; private long pauseTime; + /** + * Create a new metronome with a specified period. + * @param period the time of each interval; must be positive + * @param unit the time unit; may not be null + */ public Metronome( long period, TimeUnit unit ) { assert period >= 0; periodInMillis = unit.toMillis(period);