Recordable Autonomous

**Recordable autonomous** mode at its most basic form takes values from a controller and stores them in a file (in a JSON Object format). The file can then be played back, reproducing the output.

More specifically, for every time the `testPeriodic` method runs, controller data are sent to a `Recorder` object. The `Recorder` object stores these values if they are different from the previous values sent (this is to ensure that the file is not too large if the controller stays in the same position for a long time). Each set of values are stored with a time-stamp in milliseconds, relative to the beginning of the recording. When a `Player` object steps through the file, it searches linearly for the first data set with a time-stamp earlier than the current time (relative to the beginning of the playback).

## Requirements

First, you have to download the json.simple library from here. Then, you will have to add the library to eclipse. See here for more details.

## The Code behind the Recordings

Here is the implementation of the code (so you can copy/paste it into your own projects). Perhaps more importantly, there is an example implementation at the bottom.

### *Reader.java*

```
package org.usfirst.frc.team5066.library.playback;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.Iterator;

import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;

/\*\*
 \* Class for reading JSON files created by the
 \* {@link org.usfirst.frc.team5066.library.playback.Reader Reader} class.
 \*
 \* @author Saline Singularity 5066
 \*
 \*/
public class Reader {
 private BufferedReader br;
 private File file;
 private Iterator<JSONObject> iterator;
 private JSONArray data;
 private JSONObject completeObject, currentObject, previousObject;
 private JSONParser parser;

 /\*\*
 \* Constructor for {@link org.usfirst.frc.team5066.library.playback.Reader
 \* Reader}.
 \*
 \* @param fileURL
 \* Where the file is located
 \* @throws FileNotFoundException
 \* If the file does not exist
 \* @throws ParseException
 \* If the file is not in the proper format
 \*/
 @SuppressWarnings("unchecked")
 public Reader(String fileURL) throws FileNotFoundException, ParseException {
 parser = new JSONParser();

 try {
 file = new File(fileURL);
 if (!file.exists()) {
 throw new FileNotFoundException();
 }

 br = new BufferedReader(new FileReader(file));
 completeObject = (JSONObject) parser.parse(br);
 data = (JSONArray) ((JSONObject) completeObject.get("recording")).get("data");
 currentObject = (JSONObject) data.get(0);
 previousObject = currentObject;
 iterator = data.iterator();
 } catch (IOException ioe) {
 ioe.printStackTrace();
 }
 }

 /\*\*
 \* Reads global attribute from the JSON file
 \*
 \* @param key
 \* Which attribute to find
 \* @return The value generated by the key
 \*/
 public String readAttribute(String key) {
 return completeObject.get(key).toString();
 }

 /\*\*
 \* Sets iterator to the beginning (to reread file)
 \*/
 @SuppressWarnings("unchecked")
 public void resetIterator() {
 iterator = data.iterator();
 }

 /\*\*
 \* Finds the next piece of data to use based on the time elapsed. Use
 \* {@link org.usfirst.frc.team5066.library.playback.Reader#resetIterator
 \* resetIterator} to search from the beginning
 \*
 \* @return The next data
 \*/
 public JSONObject getDataAtTime(long time) {
 // If we have reached the end of the list, there are no more data
 if (currentObject == null) {
 return previousObject;
 }

 // If we have moved past the previous element's time, find the next
 // element
 if ((Long) currentObject.get("time") <= time) {
 // Repeat this until the time variable falls between the previous
 // and current elements' times
 do {
 previousObject = currentObject;

 // See if we have reached the end of the data
 if (iterator.hasNext()) {
 currentObject = iterator.next();
 } else {
 currentObject = null;
 }
 } while (currentObject != null && (Long) currentObject.get("time") <= time);

 // Return the object which the time has passed
 return previousObject;
 } else {
 // Return the object which the time has passed
 return previousObject;
 }
 }

 public boolean isDone(long time) {
 return (Long) ((JSONObject) data.get(data.size() - 1)).get("time") < time;
 }

 public void close() {
 try {
 br.close();
 } catch (IOException ioe) {
 ioe.printStackTrace();
 }
 }
}

```

### *Recorder.java*

```
package org.usfirst.frc.team5066.library.playback;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Date;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;

/\*\*
 \* Class for writing a list of values (in JSON) to a file
 \*
 \* @author Saline Singularity 5066
 \*
 \*/
public class Recorder {
 private BufferedWriter bw;
 private File file;
 private JSONArray data;
 private JSONObject completeObject;
 private long initialTime;
 private Object[] previousValues, defaults;
 private String fileURL, recordingID;
 private String[] keys;

 /\*\*
 \* Constructor for {@link org.usfirst.frc.team5066.library.playback.Recorder
 \* Recorder} class. This method uses the default file of "recording.json"
 \* and recording name of "recording".
 \*
 \* @param keys
 \* Keys to use for data
 \* @param defaults
 \* Defaults for the keys. Must have the same number of elements
 \* as {@code keys}
 \* @throws IllegalArgumentException
 \* If {@code keys.length} is not identical to
 \* {@code defaults.length}
 \*/
 public Recorder(String[] keys, Object[] defaults) throws IllegalArgumentException {
 if (keys.length != defaults.length) {
 throw new IllegalArgumentException();
 }
 recordingID = "recording";
 this.keys = keys;
 this.defaults = defaults;

 initialize("recording.json");
 }

 /\*\*
 \* Constructor for {@link org.usfirst.frc.team5066.library.playback.Recorder
 \* Recorder} class. This method uses the default recording name of
 \* "recording".
 \*
 \* @param keys
 \* Keys to use for data@param defaults Defaults for the keys.
 \* Must have the same number of elements as {@code keys}
 \* @param fileURL
 \* Location of the file in which to write the values
 \* @throws IllegalArgumentException
 \* If {@code keys.length} is not identical to
 \* {@code defaults.length}
 \*/
 public Recorder(String[] keys, Object[] defaults, String fileURL) {
 if (keys.length != defaults.length) {
 throw new IllegalArgumentException();
 }
 recordingID = "recording";
 this.keys = keys;
 this.defaults = defaults;

 initialize(fileURL);
 }

 /\*\*
 \* Constructor for {@link org.usfirst.frc.team5066.library.playback.Recorder
 \* Recorder} class.
 \*
 \* @param keys
 \* Keys to use for data@param defaults Defaults for the keys.
 \* Must have the same number of elements as {@code keys}
 \* @param fileURL
 \* Location of the file in which to write the values
 \* @param recordingID
 \* What to call the recording in the JSON object
 \* @throws IllegalArgumentException
 \* If {@code keys.length} is not identical to
 \* {@code defaults.length}
 \*/
 public Recorder(String keys[], Object[] defaults, String fileURL, String recordingID) {
 if (keys.length != defaults.length) {
 throw new IllegalArgumentException();
 }
 this.recordingID = recordingID;
 this.keys = keys;
 this.defaults = defaults;

 initialize(fileURL);
 }

 private boolean initialize(String fileURL) {
 completeObject = new JSONObject();
 data = new JSONArray();
 initialTime = System.currentTimeMillis();

 previousValues = new Object[keys.length];
 for (int i = 0; i < keys.length; i++) {
 previousValues[i] = defaults[i];
 }
 appendData(defaults, initialTime);

 return openFile(fileURL);
 }

 /\*\*
 \* Adds a global attribute to the recording
 \*
 \* @param key
 \* Key to use
 \* @param data
 \* What to associate with the key
 \*/
 @SuppressWarnings("unchecked")
 public void addAttribute(String key, String data) {
 completeObject.put(key, data);
 }

 /\*\*
 \* Adds the essentials to the json object
 \*
 \* @return The finalized json object
 \*/
 @SuppressWarnings("unchecked")
 private JSONObject makeFinalJSON() {
 JSONObject recording = new JSONObject();
 recording.put("id", recordingID);
 recording.put("time", (new Date(initialTime)).toString());
 recording.put("data", data);
 completeObject.put("recording", recording);

 return completeObject;
 }

 /\*\*
 \* Opens the file at {@code fileURL}. If a file already exists at that
 \* location, a number in parentheses is appended to the end of the filename.
 \*
 \* @param fileURL
 \* Which file to open
 \*/
 private boolean openFile(String fileURL) {
 try {
 // See if the BufferedWriter is open. If so, finish what it's doing.
 if (bw != null) {
 bw.close();
 }

 int i = 1;
 file = new File(fileURL);

 // Figures out what the file name will be
 while (file.exists()) {
 int index = fileURL.lastIndexOf('.');
 if (index != -1 && index != fileURL.length() - 1) {
 file = new File(fileURL.substring(0, index) + "(" + i + ")" + fileURL.substring(index));
 } else {
 file = new File(fileURL + " (" + i + ")");
 }
 i++;
 }
 this.fileURL = fileURL;

 bw = new BufferedWriter(new FileWriter(file));

 return true;
 } catch (IOException ioe) {
 bw = null;
 ioe.printStackTrace();
 return false;
 }

 }

 /\*\*
 \* Returns location of file
 \*
 \* @return fileURL
 \*/
 public String getFileURL() {
 return fileURL;
 }

 /\*\*
 \* Adds data to the JSON array. Don't forget to close the Recorder to
 \* actually save the data!
 \*
 \* @param values
 \* Name of values to store to keys (as initialized by
 \* constructor)
 \* @throws IllegalArgumentException
 \* If the number of keys does not match that of values.
 \*/
 public void appendData(Object[] values) throws IllegalArgumentException {
 if (keys.length != values.length) {
 throw new IllegalArgumentException("keys.legnth must equal values.length");
 }
 appendData(values, System.currentTimeMillis());
 }

 /\*\*
 \* Adds data to the JSON array. This method allows programmers to specify
 \* the absolute time in milliseconds at which the data is added.
 \*
 \* @param values
 \* Name of values to store to keys (as initialized by
 \* constructor)
 \* @param time
 \* When the recording happened
 \*/
 @SuppressWarnings("unchecked")
 private void appendData(Object[] values, long time) {
 JSONObject toAdd;
 boolean changed = false;

 toAdd = new JSONObject();

 // Tell when the action was recorded (relative to the initial time)
 toAdd.put("time", time - initialTime);
 for (int i = 0; i < keys.length; i++) {
 toAdd.put(keys[i], values[i]);

 // Check to see if the values have changed at all since the previous
 // iteration
 if (previousValues[i] == null || !values[i].equals(previousValues[i])) {
 changed = true;
 }
 }

 // Write to the array iff the values have been changed (saves space!)
 if (changed) {
 data.add(toAdd);
 }

 previousValues = values;
 }

 /\*\*
 \* Must be called at the end of use
 \*
 \* @param readable
 \* Whether or not to make the file more easily readable. Used
 \* mostly for testing purposes.
 \*/
 public void close(boolean readable) {
 appendData(defaults);

 try {
 // Actually writes the data to the file
 if (readable) {
 bw.write(quickFormat(makeFinalJSON().toString()));
 } else {
 bw.write(makeFinalJSON().toString());
 }
 bw.newLine();
 bw.close();
 bw = null;
 } catch (IOException ioe) {
 ioe.printStackTrace();
 }
 }

 /\*\*
 \* Must be called at the end of use
 \*/
 public void close() {
 close(false);
 }

 /\*\*
 \* Makes sure that the data is written to the file.
 \*/
 public void finalize() {
 if (bw != null) {
 close();
 }
 }

 /\*\*
 \* Makes the JSON more readable (assumes that it has good syntax). This is
 \* optional to use.
 \*
 \* @param input
 \* String of JSON
 \* @return Formatted String of JSON
 \*/
 private String quickFormat(String input) {
 int indent = 0;
 String formatted = "";

 // For every character in the input string
 for (int i = 0; i < input.length(); i++) {
 // Check what kind of character it is
 if (input.charAt(i) == '{' || input.charAt(i) == '[') {
 // For open braces/brackets, increase the indent, and insert a
 // new line
 indent++;
 formatted += input.charAt(i) + "\n" + (new String(new char[indent]).replace("\0", "\t"));
 } else if (input.charAt(i) == '}' || input.charAt(i) == ']') {
 // For closing braces/brackets, decrease the indent, and insert
 // a new line
 indent = Math.max(indent - 1, 0);
 formatted += "\n" + (new String(new char[indent]).replace("\0", "\t")) + input.charAt(i);
 } else if (input.charAt(i) == ',') {
 // For commas, insert a new line
 formatted += ",\n" + (new String(new char[indent]).replace("\0", "\t"));
 } else {
 // Otherwise, just add the character to the formatted string
 formatted += input.charAt(i);
 }
 }
 return formatted;
 }
}

```

### Excerpt from *Robot.java* to demonstrate implementation

```
/\*
 \* Many non-autonomous methods and variables not shown for compactness
 \*/
public class Robot extends IterativeRobot {
 // Here are the options for using recordable autonomous mode.
 boolean record, play;
 // To which location the recordings should be stored (if a file of the same
 // name already exists (such as foobar.json), a new name will be chosen
 // (foobar(1).json, etc.))
 String recordingURL;
 // An array of which files should be played back during autonomous
 String[] playbackURLs;

 // These variables are necessary, but need not be initialized
 long initialTime;
 Reader reader;
 Recorder recorder;
 int currentRecordingIndex;

 public void disabledInit() {
 // Closes all readers and recorder (allows files to close and/or save)
 if (recorder != null) {
 recorder.close();
 recorder = null;
 }
 if (reader != null) {
 reader.close();
 reader = null;
 }
 }

 public void autonomousInit() {
 // Chooses the first recording
 currentRecordingIndex = 0;

 // Recordable autonomous
 if (play) {
 reader = initializeReader(playbackURLs[currentRecordingIndex]);
 }
 }

 private Reader initializeReader(String playbackURL) {
 Reader reader;
 try {
 reader = new Reader(playbackURL);
 initialTime = System.currentTimeMillis();
 } catch (Exception e) {
 // This segment will execute if the file is missing or has the wrong
 // permissions
 reader = null;
 e.printStackTrace();
 }
 return reader;
 }

 public void autonomousPeriodic() {
 if (reader != null) {
 if (reader.isDone(System.currentTimeMillis() - initialTime)
 && currentRecordingIndex < playbackURLs.length - 1) {
 reader.close();
 // This will choose the next recording
 reader = initializeReader(playbackURLs[++currentRecordingIndex]);
 }

 JSONObject current = reader.getDataAtTime(System.currentTimeMillis() - initialTime);
 drive.arcade((Double) current.get("v"), (Double) current.get("omega"), true, 0);
 arm.setRawSpeed((Double) current.get("arm"));
 conveyor.setSpeed((Double) current.get("intake"));
 }
 }

 public void testInit() {
 if (record) {
 // This initializes the recorder. The former parameter is the keys,
 // and the latter is the defaults to use.
 recorder = new Recorder(new String[] { "v", "omega", "arm", "intake" }, new Object[] { 0.0, 0.0, 0.0, 0.0 },
 recordingURL);
 }
 }

 public void testPeriodic() {
 if (recorder != null) {
 // xbox and js are two input controllers. These methods just return
 // joystick values (in the form of doubles)
 Object[] input = new Object[] { xbox.getLS\_Y(), xbox.getRS\_X(), js.getY(),
 xbox.getTriggerLeft() - xbox.getTriggerRight() };

 // Do stuff to drive with the inputs.
 drive.arcade((Double) input[0], (Double) input[1], true, 0);
 arm.setRawSpeed((Double) input[2]);
 conveyor.setSpeed((Double) input[3]);

 recorder.appendData(input);
 }
 }
}

```