/*
* Copyright 2016 Cel Skeggs
*
* This file is part of the CCRE, the Common Chicken Runtime Engine.
*
* The CCRE is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* The CCRE is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with the CCRE. If not, see <http://www.gnu.org/licenses/>.
*/
package ccre.recording;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.zip.GZIPOutputStream;
import ccre.behaviors.BehaviorArbitrator;
import ccre.channel.BooleanInput;
import ccre.channel.BooleanOutput;
import ccre.channel.EventInput;
import ccre.channel.EventOutput;
import ccre.channel.FloatInput;
import ccre.channel.FloatOutput;
import ccre.ctrl.Faultable;
import ccre.ctrl.binding.ControlBindingCreator;
import ccre.discrete.DiscreteInput;
import ccre.discrete.DiscreteOutput;
import ccre.discrete.DiscreteType;
import ccre.log.Logger;
import ccre.storage.Storage;
import ccre.verifier.SetupPhase;
/**
* A class that handles channel-based data recording to an arbitrary
* OutputStream.
*
* @author skeggsc
*/
public class Recorder {
/**
* The possible recording types.
*
* @author skeggsc
*/
public static enum RawType {
/**
* A float channel.
*/
FLOAT,
/**
* A boolean channel.
*/
BOOLEAN,
/**
* An event channel.
*/
EVENT,
/**
* An OutputStream.
*/
OUTPUT_STREAM,
/**
* A discrete channel.
*/
DISCRETE
}
private final ChanneledRecorder rec;
private final AtomicInteger next_channel = new AtomicInteger();
private final AtomicBoolean closed = new AtomicBoolean(false);
/**
* Creates a new recorder writing to this OutputStream. Remember that
* writing to the stream will be synchronized, but still occur in a separate
* thread.
*
* @param stream the output stream.
* @throws IOException if the output stream fails.
*/
public Recorder(OutputStream stream) throws IOException {
this.rec = new ChanneledRecorder(stream);
}
/**
* Closes and shuts down this recorder, and waits for the operation to
* complete.
*
* @throws InterruptedException if the thread is interrupted while waiting
* for the recorder to close.
*/
@SetupPhase
public void close() throws InterruptedException {
if (closed.compareAndSet(false, true)) {
byte[] b = "\2ENDOFSTREAM".getBytes();
rec.recordBytes(0, b, 0, b.length);
rec.close();
}
}
@SetupPhase
private int initChannel(RawType rawtype, String name) {
if (closed.get()) {
throw new IllegalStateException("Recorder is closed!");
}
if (name.indexOf('\0') != -1) {
throw new IllegalArgumentException("Nulls not allowed.");
}
int channel_number = next_channel.incrementAndGet();
byte[] b = ("\0" + channel_number + "\0" + rawtype.name() + "\0" + name).getBytes();
rec.recordBytes(0, b, 0, b.length);
return channel_number;
}
@SetupPhase
private void freeChannel(int channel_number) {
if (closed.get()) {
return; // don't bother
}
byte[] b = ("\1" + channel_number).getBytes();
rec.recordBytes(0, b, 0, b.length);
}
/**
* Creates a float output logging to this recorder.
*
* @param name the channel name.
* @return the new float output.
*/
@SetupPhase
public FloatOutput createFloatOutput(String name) {
int channel = initChannel(RawType.FLOAT, name);
return (f) -> {
rec.recordInt(channel, Float.floatToIntBits(f));
};
}
/**
* Records a float input.
*
* @param input the input to record.
* @param name the channel name.
*/
@SetupPhase
public void recordFloatInput(FloatInput input, String name) {
input.send(createFloatOutput(name));
}
/**
* Creates a boolean output logging to this recorder.
*
* @param name the channel name.
* @return the new boolean output.
*/
@SetupPhase
public BooleanOutput createBooleanOutput(String name) {
int channel = initChannel(RawType.BOOLEAN, name);
return (b) -> {
rec.recordByte(channel, b ? (byte) 1 : (byte) 0);
};
}
/**
* Records a boolean input.
*
* @param input the input to record.
* @param name the channel name.
*/
@SetupPhase
public void recordBooleanInput(BooleanInput input, String name) {
input.send(createBooleanOutput(name));
}
/**
* Creates a discrete output logging to this recorder.
*
* @param <E> the discrete element type.
* @param name the channel name.
* @param type the discrete output's type.
* @return the new discrete output.
*/
@SetupPhase
public <E> DiscreteOutput<E> createDiscreteOutput(String name, DiscreteType<E> type) {
int channel = initChannel(RawType.DISCRETE, name);
return new DiscreteOutput<E>() {
@Override
public DiscreteType<E> getType() {
return type;
}
@Override
public void set(E e) {
// TODO: a more space-efficient coding
rec.recordString(channel, type.toString(e));
}
};
}
/**
* Records a discrete input.
*
* @param <E> the discrete element type.
* @param input the input to record.
* @param name the channel name.
*/
@SetupPhase
public <E> void recordDiscreteInput(DiscreteInput<E> input, String name) {
input.send(createDiscreteOutput(name, input.getType()));
}
/**
* Creates an event output logging to this recorder.
*
* @param name the channel name.
* @return the new event output.
*/
@SetupPhase
public EventOutput createEventOutput(String name) {
int channel = initChannel(RawType.EVENT, name);
return () -> {
rec.recordNull(channel);
};
}
/**
* Records an event input.
*
* @param input the input to record.
* @param name the channel name.
*/
@SetupPhase
public void recordEventInput(EventInput input, String name) {
input.send(createEventOutput(name));
}
/**
* Creates an OutputStream logging to this recorder.
*
* @param name the channel name.
* @return the new OutputStream.
*/
@SetupPhase
public OutputStream createOutputStream(String name) {
int channel = initChannel(RawType.OUTPUT_STREAM, name);
return new OutputStream() {
private byte[] b = new byte[1]; // TODO: synchronization?
private boolean closed = false;
@Override
public void write(int b) throws IOException {
if (closed) {
throw new IOException("File closed");
}
this.b[0] = (byte) b;
rec.recordBytes(channel, this.b, 0, 1);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
if (closed) {
throw new IOException("File closed");
}
rec.recordBytes(channel, b, off, len);
}
@Override
public void close() throws IOException {
super.close();
closed = true;
freeChannel(channel);
}
@Override
public void flush() throws IOException {
if (closed) {
throw new IOException("File closed");
}
super.flush();
}
};
}
/**
* Records a Faultable.
*
* @param <F> the faultable fault type.
* @param faults the faults to record.
* @param name the channel name.
*/
@SetupPhase
public <F> void recordFaultable(Faultable<F> faults, String name) {
for (F f : faults.getPossibleFaults()) {
recordBooleanInput(faults.getIsFaulting(f), name + ":" + f.toString());
recordBooleanInput(faults.getIsFaulting(f), name + ":Sticky" + f.toString());
}
}
/**
* Records and returns a float input.
*
* @param name the channel name.
* @param input the input to record.
* @return the same input.
*/
@SetupPhase
public FloatInput wrap(String name, FloatInput input) {
recordFloatInput(input, name);
return input;
}
/**
* Records and returns a boolean input.
*
* @param name the channel name.
* @param input the input to record.
* @return the same input.
*/
@SetupPhase
public BooleanInput wrap(String name, BooleanInput input) {
recordBooleanInput(input, name);
return input;
}
/**
* Records and returns an event input.
*
* @param name the channel name.
* @param input the input to record.
* @return the same input.
*/
@SetupPhase
public EventInput wrap(String name, EventInput input) {
recordEventInput(input, name);
return input;
}
/**
* Wraps a float output so that it also records anything written.
*
* @param name the channel name.
* @param output the output to propagate to.
* @return the output that records and writes through.
*/
@SetupPhase
public FloatOutput wrap(String name, FloatOutput output) {
return output.combine(createFloatOutput(name));
}
/**
* Wraps a boolean output so that it also records anything written.
*
* @param name the channel name.
* @param output the output to propagate to.
* @return the output that records and writes through.
*/
@SetupPhase
public BooleanOutput wrap(String name, BooleanOutput output) {
return output.combine(createBooleanOutput(name));
}
/**
* Wraps an event output so that it also records anything written.
*
* @param name the channel name.
* @param output the output to propagate to.
* @return the output that records and writes through.
*/
@SetupPhase
public EventOutput wrap(String name, EventOutput output) {
return output.combine(createEventOutput(name));
}
/**
* Wraps a ControlBindingCreator so that everything bound will also be
* recorded.
*
* @param cname the base name for the channels.
* @param cbc the original ControlBindingCreator.
* @return the wrapped ControlBindingCreator.
*/
@SetupPhase
public ControlBindingCreator wrap(String cname, ControlBindingCreator cbc) {
return new ControlBindingCreator() {
@Override
public FloatInput addFloat(String name) {
return wrap(cname + ":" + name, cbc.addFloat(name));
}
@Override
public void addFloat(String name, FloatOutput output) {
cbc.addFloat(name, wrap(cname + ":" + name, output));
}
@Override
public BooleanInput addBoolean(String name) {
return wrap(cname + ":" + name, cbc.addBoolean(name));
}
@Override
public void addBoolean(String name, BooleanOutput output) {
cbc.addBoolean(name, wrap(cname + ":" + name, output));
}
};
}
/**
* Records the state of a BehaviorArbitrator. The channel's name will be
* based on the arbitrator's name.
*
* @param behaviors the behaviors to record.
*/
@SetupPhase
public void recordBehaviors(BehaviorArbitrator behaviors) {
recordDiscreteInput(behaviors.getActiveBehavior(), "Behaviors:" + behaviors.getName());
}
@SetupPhase
static int[] listUsedNumbers() {
String[] files = Storage.list();
int[] found = new int[files.length];
int j = 0;
for (int i = 0; i < files.length; i++) {
String filename = files[i];
if (filename.startsWith("rec-")) {
try {
if (filename.endsWith(".gz")) {
found[j] = Integer.parseInt(filename.substring(4, filename.length() - 3));
j++;
} else {
found[j] = Integer.parseInt(filename.substring(4));
j++;
}
} catch (NumberFormatException ex) {
// not the right kind of file; skip forward
}
}
}
found = Arrays.copyOf(found, j); // compact
Arrays.sort(found);
return found;
}
@SetupPhase
static OutputStream openStream(boolean compressed, int maximum_records) throws IOException {
if (maximum_records < 1) {
throw new IllegalArgumentException("Must have at least one slot in record buffer!");
}
int[] used = listUsedNumbers();
if (used.length >= maximum_records) {
int to_remove = 1 + used.length - maximum_records;
// wipe out old entries in the buffer
for (int i = 0; i < to_remove; i++) {
String thisName = "rec-" + used[i];
if (Storage.exists(thisName)) {
Storage.delete(thisName);
} else {
Storage.delete(thisName + ".gz");
}
}
}
int next_id = used.length == 0 ? 0 : used[used.length - 1] + 1;
String next_name = "rec-" + next_id;
if (compressed) {
Logger.config("Opening recorder output at " + next_name + ".gz (compressed)");
return new GZIPOutputStream(Storage.openOutput(next_name + ".gz"));
} else {
Logger.config("Opening recorder output at " + next_name + " (uncompressed)");
return Storage.openOutput(next_name);
}
}
/**
* Opens a recorder from the limited buffer. This will delete old records,
* and have at most <code>maximum_records</code> recordings at any time.
*
* The recorder will be automatically closed when the JVM shuts down.
*
* @param compressed if the recording should be compressed
* @param maximum_recordings the maximum number of recordings
* @return the opened recorder
* @throws IOException if the recording cannot be set up.
*/
@SetupPhase
public static Recorder open(boolean compressed, int maximum_recordings) throws IOException {
OutputStream out = openStream(compressed, maximum_recordings);
boolean success = false;
try {
Recorder rc = new Recorder(out);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
rc.close();
} catch (Exception e) {
e.printStackTrace();
}
}, "Shutdown-Recorder"));
success = true;
return rc;
} finally {
if (!success) {
out.close();
}
}
}
}