Merge "Add arm/elevator profile instances into the control loop."
diff --git a/aos/build/aos.gypi b/aos/build/aos.gypi
index 22e96a0..e049182 100644
--- a/aos/build/aos.gypi
+++ b/aos/build/aos.gypi
@@ -24,13 +24,13 @@
     ],
   },
   'conditions': [
-    ['PLATFORM=="linux-arm-gcc_frc"', {
+    ['PLATFORM=="linux-arm_frc-gcc"', {
         'make_global_settings': [
           ['CC', '<(ccache)<!(which arm-frc-linux-gnueabi-gcc-4.9)'],
           ['CXX', '<(ccache)<!(which arm-frc-linux-gnueabi-g++-4.9)'],
         ],
       },
-    ], ['PLATFORM=="linux-arm-clang_frc"', {
+    ], ['PLATFORM=="linux-arm_frc-clang"', {
         'variables': {
           'arm-clang-symlinks': '<!(realpath -s <(AOS)/build/arm-clang-symlinks)',
           'arm-clang-sysroot': '<(arm-clang-symlinks)/sysroot',
@@ -140,6 +140,7 @@
       '__STDC_CONSTANT_MACROS',
       '__STDC_LIMIT_MACROS',
       'AOS_COMPILER_<!(echo <(FULL_COMPILER) | sed \'s/\./_/g\')',
+      'AOS_ARCHITECTURE_<(ARCHITECTURE)',
       '_FILE_OFFSET_BITS=64',
     ],
     'ldflags': [
diff --git a/aos/build/build.py b/aos/build/build.py
index 21e0379..6281821 100755
--- a/aos/build/build.py
+++ b/aos/build/build.py
@@ -137,56 +137,24 @@
   """
   return os.path.join(os.path.dirname(__file__), '..')
 
-def get_ip_base():
-  """Retrieves the IP address base."""
+def get_ip():
+  """Retrieves the IP address to download code to."""
   FILENAME = os.path.normpath(os.path.join(aos_path(), '..',
-                                           'output', 'ip_base.txt'))
+                                           'output', 'ip_address.txt'))
   if not os.access(FILENAME, os.R_OK):
     os.makedirs(os.path.dirname(FILENAME), exist_ok=True)
     with open(FILENAME, 'w') as f:
       f.write('roboRIO-971.local')
   with open(FILENAME, 'r') as f:
-    base = f.readline().strip()
-  return base
+    return f.readline().strip()
 
-def get_ip(device):
-  """Retrieves the IP address for a given device."""
-  base = get_ip_base()
-  if device == 'prime':
-    return base + '.179'
-  elif device == 'robot':
-    return base + '.2'
-  elif device == 'roboRIO':
-    return base
-  else:
-    raise Exception('Unknown device %s to get an IP address for.' % device)
+def get_temp_dir():
+  """Retrieves the temporary directory to use when downloading."""
+  return '/home/admin/tmp/aos_downloader'
 
-def get_user(device):
-  """Retrieves the user for a given device."""
-  if device == 'prime':
-    return 'driver'
-  elif device == 'roboRIO':
-    return 'admin'
-  else:
-    raise Exception('Unknown device %s to get a user for.' % device)
-
-def get_temp_dir(device):
-  """Retrieves the temporary download directory for a given device."""
-  if device == 'prime':
-    return '/tmp/aos_downloader'
-  elif device == 'roboRIO':
-    return '/home/admin/tmp/aos_downloader'
-  else:
-    raise Exception('Unknown device %s to get a temp_dir for.' % device)
-
-def get_target_dir(device):
-  """Retrieves the tempory deploy directory for a given device."""
-  if device == 'prime':
-    return '/home/driver/robot_code/bin'
-  elif device == 'roboRIO':
-    return '/home/admin/robot_code'
-  else:
-    raise Exception('Unknown device %s to get a temp_dir for.' % device)
+def get_target_dir():
+  """Retrieves the tempory deploy directory for downloading code."""
+  return '/home/admin/robot_code'
 
 def user_output(message):
   """Prints message to the user."""
@@ -434,16 +402,15 @@
       return r
 
     def deploy(self, dry_run):
-      # Downloads code to the prime in a way that avoids clashing too badly with
-      # starter (like the naive download everything one at a time).
-      if self.compiler().endswith('_frc'):
-        device = 'roboRIO'
-      else:
-        device = 'prime'
+      """Downloads code to the prime in a way that avoids clashing too badly with
+      starter (like the naive download everything one at a time)."""
+      if not self.architecture().endswith('_frc'):
+        raise Exception("Don't know how to download code to a %s." %
+                        self.architecture())
       SUM = 'md5sum'
-      TARGET_DIR = get_target_dir(device)
-      TEMP_DIR = get_temp_dir(device)
-      TARGET = get_user(device) + '@' + get_ip(device)
+      TARGET_DIR = get_target_dir()
+      TEMP_DIR = get_temp_dir()
+      TARGET = 'admin@' + get_ip()
 
       from_dir = os.path.join(self.outdir(), 'outputs')
       sums = subprocess.check_output((SUM,) + tuple(os.listdir(from_dir)),
@@ -467,8 +434,7 @@
           + (('%s:%s' % (TARGET, TEMP_DIR)),))
       if not dry_run:
         mv_cmd = ['mv {TMPDIR}/* {TO_DIR} ']
-        if device == 'roboRIO':
-          mv_cmd.append('&& chmod u+s {TO_DIR}/starter_exe ')
+        mv_cmd.append('&& chmod u+s {TO_DIR}/starter_exe ')
         mv_cmd.append('&& echo \'Done moving new executables into place\' ')
         mv_cmd.append('&& bash -c \'sync && sync && sync\'')
         subprocess.check_call(
@@ -511,8 +477,8 @@
 
       return r
 
-  ARCHITECTURES = ('arm', 'amd64')
-  COMPILERS = ('clang', 'gcc_frc', 'clang_frc')
+  ARCHITECTURES = ('arm_frc', 'amd64')
+  COMPILERS = ('clang', 'gcc')
   SANITIZERS = ('address', 'undefined', 'integer', 'memory', 'thread', 'none')
   SANITIZER_TEST_WARNINGS = {
       'memory': (True,
@@ -529,8 +495,7 @@
     for architecture in PrimeProcessor.ARCHITECTURES:
       for compiler in PrimeProcessor.COMPILERS:
         for debug in [True, False]:
-          if ((architecture == 'arm' and not compiler.endswith('_frc')) or
-              (architecture == 'amd64' and compiler.endswith('_frc'))):
+          if architecture == 'amd64' and compiler == 'gcc':
             # We don't have a compiler to use here.
             continue
           platforms.append(
@@ -556,8 +521,8 @@
         if warning[0]:
           default_platforms -= self.select_platforms(sanitizer=sanitizer)
     elif is_deploy:
-      default_platforms = self.select_platforms(architecture='arm',
-                                                compiler='gcc_frc',
+      default_platforms = self.select_platforms(architecture='arm_frc',
+                                                compiler='gcc',
                                                 debug=False)
     else:
       default_platforms = self.select_platforms(debug=False)
@@ -578,13 +543,8 @@
       if platforms & pie_sanitizers:
         to_download.add(architecture + '-fPIE')
 
-      frc_platforms = self.select_platforms(architecture=architecture,
-                                            compiler='gcc_frc')
-      if platforms & frc_platforms:
-        to_download.add(architecture + '_frc')
-
       if platforms & (self.select_platforms(architecture=architecture) -
-                      pie_sanitizers - frc_platforms):
+                      pie_sanitizers):
         to_download.add(architecture)
 
     for download_target in to_download:
@@ -651,14 +611,13 @@
     packages.add('clang-3.5')
     packages.add('clang-format-3.5')
     for platform in platforms:
-      if (platform.compiler() == 'clang' or platform.compiler() == 'gcc_4.8' or
-          platform.compiler() == 'clang_frc'):
+      if platform.compiler() == 'clang' or platform.compiler() == 'gcc_4.8':
         packages.add('clang-3.5')
       if platform.compiler() == 'gcc_4.8':
         packages.add('libcloog-isl3:amd64')
       if is_deploy:
         packages.add('openssh-client')
-      elif platform.compiler() == 'gcc_frc' or platform.compiler() == 'clang_frc':
+      elif platform.architecture == 'arm_frc':
         packages.add('gcc-4.9-arm-frc-linux-gnueabi')
         packages.add('g++-4.9-arm-frc-linux-gnueabi')
 
@@ -933,7 +892,7 @@
              '-DSANITIZER=%s' % platform.sanitizer(),
              '-DEXTERNALS_EXTRA=%s' %
              ('-fPIE' if platform.sanitizer() in PrimeProcessor.PIE_SANITIZERS
-              else ('_frc' if platform.compiler().endswith('_frc') else ''))) +
+              else '')) +
             processor.extra_gyp_flags() + (args.main_gyp,),
             stdin=subprocess.PIPE)
         gyp.communicate(("""
diff --git a/aos/common/controls/controls.gyp b/aos/common/controls/controls.gyp
index e24a121..4e0bc79 100644
--- a/aos/common/controls/controls.gyp
+++ b/aos/common/controls/controls.gyp
@@ -1,6 +1,27 @@
 {
   'targets': [
     {
+      'target_name': 'replay_control_loop',
+      'type': 'static_library',
+      'sources': [
+        #'replay_control_loop.h',
+      ],
+      'dependencies': [
+        '<(AOS)/common/common.gyp:queues',
+        'control_loop',
+        '<(AOS)/linux_code/logging/logging.gyp:log_replay',
+        '<(AOS)/common/logging/logging.gyp:queue_logging',
+        '<(AOS)/common/common.gyp:time',
+      ],
+      'export_dependent_settings': [
+        '<(AOS)/common/common.gyp:queues',
+        'control_loop',
+        '<(AOS)/linux_code/logging/logging.gyp:log_replay',
+        '<(AOS)/common/logging/logging.gyp:queue_logging',
+        '<(AOS)/common/common.gyp:time',
+      ],
+    },
+    {
       'target_name': 'control_loop_test',
       'type': 'static_library',
       'sources': [
diff --git a/aos/common/controls/replay_control_loop.h b/aos/common/controls/replay_control_loop.h
new file mode 100644
index 0000000..4791973
--- /dev/null
+++ b/aos/common/controls/replay_control_loop.h
@@ -0,0 +1,179 @@
+#ifndef AOS_COMMON_CONTROLS_REPLAY_CONTROL_LOOP_H_
+#define AOS_COMMON_CONTROLS_REPLAY_CONTROL_LOOP_H_
+
+#include <fcntl.h>
+
+#include "aos/common/queue.h"
+#include "aos/common/controls/control_loop.h"
+#include "aos/linux_code/logging/log_replay.h"
+#include "aos/common/logging/queue_logging.h"
+#include "aos/common/time.h"
+#include "aos/common/macros.h"
+
+namespace aos {
+namespace controls {
+
+// Handles reading logged messages and running them through a control loop
+// again.
+// T should be a queue group suitable for use with ControlLoop.
+template <class T>
+class ControlLoopReplayer {
+ public:
+  typedef typename ControlLoop<T>::GoalType GoalType;
+  typedef typename ControlLoop<T>::PositionType PositionType;
+  typedef typename ControlLoop<T>::StatusType StatusType;
+  typedef typename ControlLoop<T>::OutputType OutputType;
+
+  // loop_group is where to send the messages out.
+  // process_name is the name of the process which wrote the log messages in the
+  // file(s).
+  ControlLoopReplayer(T *loop_group, const ::std::string &process_name)
+      : loop_group_(loop_group) {
+    // Clear out any old messages which will confuse the code.
+    loop_group_->status.FetchLatest();
+    loop_group_->output.FetchLatest();
+
+    replayer_.AddDirectQueueSender("wpilib_interface.DSReader",
+                                   "joystick_state", ::aos::joystick_state);
+    replayer_.AddDirectQueueSender("wpilib_interface.SensorReader",
+                                   "robot_state", ::aos::robot_state);
+
+    replayer_.AddHandler(
+        process_name, "position",
+        ::std::function<void(const PositionType &)>(::std::ref(position_)));
+    replayer_.AddHandler(
+        process_name, "output",
+        ::std::function<void(const OutputType &)>(::std::ref(output_)));
+    replayer_.AddHandler(
+        process_name, "status",
+        ::std::function<void(const StatusType &)>(::std::ref(status_)));
+    // The timing of goal messages doesn't matter, and we don't need to look
+    // back at them after running the loop.
+    replayer_.AddDirectQueueSender(process_name, "goal", loop_group_->goal);
+  }
+
+  // Replays messages from a file.
+  // filename can be straight from the command line; all sanity checking etc is
+  // handled by this function.
+  void ProcessFile(const char *filename);
+
+ private:
+  // A message handler which saves off messages and records whether it currently
+  // has a new one or not.
+  template <class S>
+  class CaptureMessage {
+   public:
+    CaptureMessage() {}
+
+    void operator()(const S &message) {
+      CHECK(!have_new_message_);
+      saved_message_ = message;
+      have_new_message_ = true;
+    }
+
+    const S &saved_message() const { return saved_message_; }
+    bool have_new_message() const { return have_new_message_; }
+    void clear_new_message() { have_new_message_ = false; }
+
+   private:
+    S saved_message_;
+    bool have_new_message_ = false;
+
+    DISALLOW_COPY_AND_ASSIGN(CaptureMessage);
+  };
+
+  // Runs through the file currently loaded in replayer_.
+  // Returns after going through the entire file.
+  void DoProcessFile();
+
+  T *const loop_group_;
+
+  CaptureMessage<PositionType> position_;
+  CaptureMessage<OutputType> output_;
+  CaptureMessage<StatusType> status_;
+
+  // The output that the loop sends for ZeroOutputs(). It might not actually be
+  // all fields zeroed, so we pick the first one and remember it to compare.
+  CaptureMessage<OutputType> zero_output_;
+
+  ::aos::logging::linux_code::LogReplayer replayer_;
+};
+
+template <class T>
+void ControlLoopReplayer<T>::ProcessFile(const char *filename) {
+  int fd;
+  if (strcmp(filename, "-") == 0) {
+    fd = STDIN_FILENO;
+  } else {
+    fd = open(filename, O_RDONLY);
+  }
+  if (fd == -1) {
+    PLOG(FATAL, "couldn't open file '%s' for reading", filename);
+  }
+
+  replayer_.OpenFile(fd);
+  DoProcessFile();
+  replayer_.CloseCurrentFile();
+
+  PCHECK(close(fd));
+}
+
+template <class T>
+void ControlLoopReplayer<T>::DoProcessFile() {
+  while (true) {
+    // Dig through messages until we get a status, which indicates the end of
+    // the control loop cycle.
+    while (!status_.have_new_message()) {
+      if (replayer_.ProcessMessage()) return;
+    }
+
+    // Send out the position message (after adjusting the time offset) so the
+    // loop will run right now.
+    CHECK(position_.have_new_message());
+    ::aos::time::OffsetToNow(position_.saved_message().sent_time);
+    {
+      auto position_message = loop_group_->position.MakeMessage();
+      *position_message = position_.saved_message();
+      CHECK(position_message.Send());
+    }
+    position_.clear_new_message();
+
+    // Wait for the loop to finish running.
+    loop_group_->status.FetchNextBlocking();
+
+    // Point out if the status is different.
+    if (!loop_group_->status->EqualsNoTime(status_.saved_message())) {
+      LOG_STRUCT(WARNING, "expected status", status_.saved_message());
+      LOG_STRUCT(WARNING, "got status", *loop_group_->status);
+    }
+    status_.clear_new_message();
+
+    // Point out if the output is different. This is a lot more complicated than
+    // for the status because there isn't always an output logged.
+    bool loop_new_output = loop_group_->output.FetchLatest();
+    if (output_.have_new_message()) {
+      if (!loop_new_output) {
+        LOG_STRUCT(WARNING, "no output, expected", output_.saved_message());
+      } else if (!loop_group_->output->EqualsNoTime(output_.saved_message())) {
+        LOG_STRUCT(WARNING, "expected output", output_.saved_message());
+        LOG_STRUCT(WARNING, "got output", *loop_group_->output);
+      }
+    } else if (loop_new_output) {
+      if (zero_output_.have_new_message()) {
+        if (!loop_group_->output->EqualsNoTime(zero_output_.saved_message())) {
+          LOG_STRUCT(WARNING, "expected null output",
+                     zero_output_.saved_message());
+          LOG_STRUCT(WARNING, "got output", *loop_group_->output);
+        }
+      } else {
+        zero_output_(*loop_group_->output);
+      }
+    }
+    output_.clear_new_message();
+  }
+}
+
+}  // namespace controls
+}  // namespace aos
+
+#endif  // AOS_COMMON_CONTROLS_REPLAY_CONTROL_LOOP_H_
diff --git a/aos/common/time.cc b/aos/common/time.cc
index 1c86811..7318afb 100644
--- a/aos/common/time.cc
+++ b/aos/common/time.cc
@@ -1,9 +1,13 @@
 #include "aos/common/time.h"
 
 #include <string.h>
+#include <inttypes.h>
+
+// We only use global_core from here, which is weak, so we don't really have a
+// dependency on it.
+#include "aos/linux_code/ipc_lib/shared_mem.h"
 
 #include "aos/common/logging/logging.h"
-#include <inttypes.h>
 #include "aos/common/mutex.h"
 
 namespace aos {
@@ -33,7 +37,11 @@
     PLOG(FATAL, "clock_gettime(%jd, %p) failed",
          static_cast<uintmax_t>(clock), &temp);
   }
-  return Time(temp);
+
+  const timespec offset = (global_core == nullptr)
+                              ? timespec{0, 0}
+                              : global_core->mem_struct->time_offset;
+  return Time(temp) + Time(offset);
 }
 
 }  // namespace
@@ -214,5 +222,12 @@
   }
 }
 
+void OffsetToNow(const Time &now) {
+  CHECK_NOTNULL(global_core);
+  global_core->mem_struct->time_offset.tv_nsec = 0;
+  global_core->mem_struct->time_offset.tv_sec = 0;
+  global_core->mem_struct->time_offset = (now - Time::Now()).ToTimespec();
+}
+
 }  // namespace time
 }  // namespace aos
diff --git a/aos/common/time.h b/aos/common/time.h
index 9c86168..9afe9a0 100644
--- a/aos/common/time.h
+++ b/aos/common/time.h
@@ -242,6 +242,14 @@
 // Sleeps until clock is at the time represented by time.
 void SleepUntil(const Time &time, clockid_t clock = Time::kDefaultClock);
 
+// Sets the global offset for all times so ::aos::time::Time::Now() will return
+// now.
+// There is no synchronization here, so this is only safe when only a single
+// task is running.
+// This is only allowed when the shared memory core infrastructure has been
+// initialized in this process.
+void OffsetToNow(const Time &now);
+
 // RAII class that freezes Time::Now() (to avoid making large numbers of
 // syscalls to find the real time).
 class TimeFreezer {
diff --git a/aos/linux_code/ipc_lib/shared_mem.c b/aos/linux_code/ipc_lib/shared_mem.c
index 79b747a..65305ac 100644
--- a/aos/linux_code/ipc_lib/shared_mem.c
+++ b/aos/linux_code/ipc_lib/shared_mem.c
@@ -20,7 +20,7 @@
 #define SIZEOFSHMSEG (4096 * 0x3000)
 
 void init_shared_mem_core(aos_shm_core *shm_core) {
-  clock_gettime(CLOCK_REALTIME, &shm_core->identifier);
+  memset(&shm_core->time_offset, 0 , sizeof(shm_core->time_offset));
   memset(&shm_core->msg_alloc_lock, 0, sizeof(shm_core->msg_alloc_lock));
   shm_core->queues.pointer = NULL;
   memset(&shm_core->queues.lock, 0, sizeof(shm_core->queues.lock));
diff --git a/aos/linux_code/ipc_lib/shared_mem.h b/aos/linux_code/ipc_lib/shared_mem.h
index a33290c..423fd0c 100644
--- a/aos/linux_code/ipc_lib/shared_mem.h
+++ b/aos/linux_code/ipc_lib/shared_mem.h
@@ -11,7 +11,7 @@
 extern "C" {
 #endif
 
-extern struct aos_core *global_core;
+extern struct aos_core *global_core __attribute__((weak));
 
 // Where the shared memory segment starts in each process's address space.
 // Has to be the same in all of them so that stuff in shared memory
@@ -26,13 +26,17 @@
 } aos_global_pointer;
 
 typedef struct aos_shm_core_t {
-  // clock_gettime(CLOCK_REALTIME, &identifier) gets called to identify
-  // this shared memory area
-  struct timespec identifier;
   // Gets 0-initialized at the start (as part of shared memory) and
   // the owner sets as soon as it finishes setting stuff up.
   aos_condition creation_condition;
 
+  // An offset from CLOCK_REALTIME to times for all the code.
+  // This is currently only set to non-zero by the log replay code.
+  // There is no synchronization on this to avoid the overhead because it is
+  // only updated with proper memory barriers when only a single task is
+  // running.
+  struct timespec time_offset;
+
   struct aos_mutex msg_alloc_lock;
   void *msg_alloc;
 
diff --git a/aos/linux_code/logging/binary_log_writer.cc b/aos/linux_code/logging/binary_log_writer.cc
index 9943e7b..dbe61e0 100644
--- a/aos/linux_code/logging/binary_log_writer.cc
+++ b/aos/linux_code/logging/binary_log_writer.cc
@@ -108,7 +108,7 @@
          fileindex, directory, previous);
 }
 
-#ifdef AOS_COMPILER_gcc_frc
+#ifdef AOS_ARCHITECTURE_arm_frc
 bool FoundThumbDrive(const char *path) {
   FILE *mnt_fp = setmntent("/etc/mtab", "r");
   if (mnt_fp == nullptr) {
@@ -148,7 +148,7 @@
 int BinaryLogReaderMain() {
   InitNRT();
 
-#ifdef AOS_COMPILER_gcc_frc
+#ifdef AOS_ARCHITECTURE_arm_frc
   char folder[128];
 
   {
diff --git a/aos/linux_code/logging/log_replay.cc b/aos/linux_code/logging/log_replay.cc
new file mode 100644
index 0000000..32991de
--- /dev/null
+++ b/aos/linux_code/logging/log_replay.cc
@@ -0,0 +1,44 @@
+#include "aos/linux_code/logging/log_replay.h"
+
+namespace aos {
+namespace logging {
+namespace linux_code {
+
+bool LogReplayer::ProcessMessage() {
+  const LogFileMessageHeader *message = reader_->ReadNextMessage(false);
+  if (message == nullptr) return true;
+  if (message->type != LogFileMessageHeader::MessageType::kStruct) return false;
+
+  const char *position = reinterpret_cast<const char *>(message + 1);
+
+  ::std::string process(position, message->name_size);
+  position += message->name_size;
+
+  uint32_t type_id;
+  memcpy(&type_id, position, sizeof(type_id));
+  position += sizeof(type_id);
+
+  uint32_t message_length;
+  memcpy(&message_length, position, sizeof(message_length));
+  position += sizeof(message_length);
+  ::std::string message_text(position, message_length);
+  position += message_length;
+
+  size_t split_index = message_text.find_first_of(':') + 2;
+  split_index = message_text.find_first_of(':', split_index) + 2;
+  message_text = message_text.substr(split_index);
+
+  auto handler = handlers_.find(Key(process, message_text));
+  if (handler == handlers_.end()) return false;
+
+  handler->second->HandleStruct(
+      ::aos::time::Time(message->time_sec, message->time_nsec), type_id,
+      position,
+      message->message_size -
+          (sizeof(type_id) + sizeof(message_length) + message_length));
+  return false;
+}
+
+}  // namespace linux_code
+}  // namespace logging
+}  // namespace aos
diff --git a/aos/linux_code/logging/log_replay.h b/aos/linux_code/logging/log_replay.h
new file mode 100644
index 0000000..942f26f
--- /dev/null
+++ b/aos/linux_code/logging/log_replay.h
@@ -0,0 +1,164 @@
+#ifndef AOS_LINUX_CODE_LOGGING_LOG_REPLAY_H_
+#define AOS_LINUX_CODE_LOGGING_LOG_REPLAY_H_
+
+#include <unordered_map>
+#include <string>
+#include <functional>
+#include <memory>
+
+#include "aos/linux_code/logging/binary_log_file.h"
+#include "aos/common/queue.h"
+#include "aos/common/logging/logging.h"
+#include "aos/common/macros.h"
+#include "aos/linux_code/ipc_lib/queue.h"
+#include "aos/common/queue_types.h"
+
+namespace aos {
+namespace logging {
+namespace linux_code {
+
+// Manages pulling logged queue messages out of log files.
+//
+// Basic usage:
+//   - Use the Add* methods to register handlers for various message sources.
+//   - Call OpenFile to open a log file.
+//   - Call ProcessMessage repeatedly until it returns true.
+//
+// This code could do something to adapt similar-but-not-identical
+// messages to the current versions, but currently it will LOG(FATAL) if any of
+// the messages don't match up exactly.
+class LogReplayer {
+ public:
+  LogReplayer() {}
+
+  // Gets ready to read messages from fd.
+  // Does not take ownership of fd.
+  void OpenFile(int fd) {
+    reader_.reset(new LogFileReader(fd));
+  }
+  // Closes the currently open file.
+  void CloseCurrentFile() { reader_.reset(); }
+  // Returns true if we have a file which is currently open.
+  bool HasCurrentFile() const { return reader_.get() != nullptr; }
+
+  // Processes a single message from the currently open file.
+  // Returns true if there are no more messages in the file.
+  // This will not call any of the handlers if the next message either has no
+  // registered handlers or is not a queue message.
+  bool ProcessMessage();
+
+  // Adds a handler for messages with a certain string from a certain process.
+  // T must be a Message with the same format as the messages generated by
+  // the .q files.
+  // LOG(FATAL)s for duplicate handlers.
+  template <class T>
+  void AddHandler(const ::std::string &process_name,
+                  const ::std::string &log_message,
+                  ::std::function<void(const T &message)> handler) {
+    CHECK(handlers_.emplace(
+        ::std::piecewise_construct,
+        ::std::forward_as_tuple(process_name, log_message),
+        ::std::forward_as_tuple(::std::unique_ptr<StructHandlerInterface>(
+            new TypedStructHandler<T>(handler)))).second);
+  }
+
+  // Adds a handler which takes messages and places them directly on a queue.
+  // T must be a Message with the same format as the messages generated by
+  // the .q files.
+  template <class T>
+  void AddDirectQueueSender(const ::std::string &process_name,
+                            const ::std::string &log_message,
+                            const ::aos::Queue<T> &queue) {
+    AddHandler(process_name, log_message,
+               ::std::function<void(const T &)>(
+                   QueueDumpStructHandler<T>(queue.name())));
+  }
+
+ private:
+  // A generic handler of struct log messages.
+  class StructHandlerInterface {
+   public:
+    virtual ~StructHandlerInterface() {}
+
+    virtual void HandleStruct(::aos::time::Time log_time, uint32_t type_id,
+                              const void *data, size_t data_size) = 0;
+  };
+
+  // Converts struct log messages to a message type and passes it to an
+  // ::std::function.
+  template <class T>
+  class TypedStructHandler : public StructHandlerInterface {
+   public:
+    TypedStructHandler(::std::function<void(const T &message)> handler)
+        : handler_(handler) {}
+
+    void HandleStruct(::aos::time::Time log_time, uint32_t type_id,
+                      const void *data, size_t data_size) override {
+      CHECK_EQ(type_id, T::GetType()->id);
+      T message;
+      CHECK_EQ(data_size, T::Size());
+      CHECK_EQ(data_size, message.Deserialize(static_cast<const char *>(data)));
+      message.sent_time = log_time;
+      handler_(message);
+    }
+
+   private:
+    const ::std::function<void(T message)> handler_;
+  };
+
+  // A callable class which dumps messages straight to a queue.
+  template <class T>
+  class QueueDumpStructHandler {
+   public:
+    QueueDumpStructHandler(const ::std::string &queue_name)
+        : queue_(RawQueue::Fetch(queue_name.c_str(), sizeof(T), T::kHash,
+                                 T::kQueueLength)) {}
+
+    void operator()(const T &message) {
+      LOG_STRUCT(DEBUG, "re-sending", message);
+      void *raw_message = queue_->GetMessage();
+      CHECK_NOTNULL(raw_message);
+      memcpy(raw_message, &message, sizeof(message));
+      CHECK(queue_->WriteMessage(raw_message, RawQueue::kOverride));
+    }
+
+   private:
+    ::aos::RawQueue *const queue_;
+  };
+
+  // A key for specifying log messages to give to a certain handler.
+  struct Key {
+    Key(const ::std::string &process_name, const ::std::string &log_message)
+        : process_name(process_name), log_message(log_message) {}
+
+    ::std::string process_name;
+    ::std::string log_message;
+  };
+
+  struct KeyHash {
+    size_t operator()(const Key &key) const {
+      return string_hash(key.process_name) ^
+             (string_hash(key.log_message) << 1);
+    }
+
+   private:
+    const ::std::hash<::std::string> string_hash = ::std::hash<::std::string>();
+  };
+  struct KeyEqual {
+    bool operator()(const Key &a, const Key &b) const {
+      return a.process_name == b.process_name && a.log_message == b.log_message;
+    }
+  };
+
+  ::std::unordered_map<const Key, ::std::unique_ptr<StructHandlerInterface>,
+                       KeyHash, KeyEqual> handlers_;
+  ::std::unique_ptr<LogFileReader> reader_;
+
+  DISALLOW_COPY_AND_ASSIGN(LogReplayer);
+};
+
+}  // namespace linux_code
+}  // namespace logging
+}  // namespace aos
+
+#endif  // AOS_LINUX_CODE_LOGGING_LOG_REPLAY_H_
diff --git a/aos/linux_code/logging/logging.gyp b/aos/linux_code/logging/logging.gyp
index 65939df..2ae9e0b 100644
--- a/aos/linux_code/logging/logging.gyp
+++ b/aos/linux_code/logging/logging.gyp
@@ -2,6 +2,19 @@
   'targets': [
     # linux_* is dealt with by aos/build/aos.gyp:logging.
     {
+      'target_name': 'log_replay',
+      'type': 'static_library',
+      'sources': [
+        'log_replay.cc',
+      ],
+      'dependencies': [
+        'binary_log_file',
+        '<(AOS)/common/common.gyp:queues',
+        '<(AOS)/build/aos.gyp:logging',
+        '<(AOS)/linux_code/ipc_lib/ipc_lib.gyp:queue',
+      ],
+    },
+    {
       'target_name': 'binary_log_writer',
       'type': 'executable',
       'sources': [
diff --git a/frc971/analysis/analysis.py b/frc971/analysis/analysis.py
index 66c5056..8152607 100755
--- a/frc971/analysis/analysis.py
+++ b/frc971/analysis/analysis.py
@@ -70,7 +70,8 @@
     """
     for key in self.signal:
       value = self.signal[key]
-      pylab.plot(value.time, value.data, label=key[0] + ' ' + '.'.join(key[2]))
+      pylab.plot(value.time, value.data, label=key[0] + ' ' + key[1] + '.' + \
+          '.'.join(key[2]))
     pylab.legend()
     pylab.show()
 
diff --git a/frc971/control_loops/claw/claw.gyp b/frc971/control_loops/claw/claw.gyp
index b84b5c0..40fe95d 100644
--- a/frc971/control_loops/claw/claw.gyp
+++ b/frc971/control_loops/claw/claw.gyp
@@ -1,6 +1,21 @@
 {
   'targets': [
     {
+      'target_name': 'replay_claw',
+      'type': 'executable',
+      'variables': {
+        'no_rsync': 1,
+      },
+      'sources': [
+        'replay_claw.cc',
+      ],
+      'dependencies': [
+        'claw_queue',
+        '<(AOS)/common/controls/controls.gyp:replay_control_loop',
+        '<(AOS)/linux_code/linux_code.gyp:init',
+      ],
+    },
+    {
       'target_name': 'claw_queue',
       'type': 'static_library',
       'sources': ['claw.q'],
diff --git a/frc971/control_loops/claw/replay_claw.cc b/frc971/control_loops/claw/replay_claw.cc
new file mode 100644
index 0000000..00d3c6d
--- /dev/null
+++ b/frc971/control_loops/claw/replay_claw.cc
@@ -0,0 +1,24 @@
+#include "aos/common/controls/replay_control_loop.h"
+#include "aos/linux_code/init.h"
+
+#include "frc971/control_loops/claw/claw.q.h"
+
+// Reads one or more log files and sends out all the queue messages (in the
+// correct order and at the correct time) to feed a "live" claw process.
+
+int main(int argc, char **argv) {
+  if (argc <= 1) {
+    fprintf(stderr, "Need at least one file to replay!\n");
+    return EXIT_FAILURE;
+  }
+
+  ::aos::InitNRT();
+
+  ::aos::controls::ControlLoopReplayer<::frc971::control_loops::ClawQueue>
+      replayer(&::frc971::control_loops::claw_queue, "claw");
+  for (int i = 1; i < argc; ++i) {
+    replayer.ProcessFile(argv[i]);
+  }
+
+  ::aos::Cleanup();
+}
diff --git a/frc971/control_loops/drivetrain/drivetrain.gyp b/frc971/control_loops/drivetrain/drivetrain.gyp
index f1b28f3..fb32377 100644
--- a/frc971/control_loops/drivetrain/drivetrain.gyp
+++ b/frc971/control_loops/drivetrain/drivetrain.gyp
@@ -1,6 +1,21 @@
 {
   'targets': [
     {
+      'target_name': 'replay_drivetrain',
+      'type': 'executable',
+      'variables': {
+        'no_rsync': 1,
+      },
+      'sources': [
+        'replay_drivetrain.cc',
+      ],
+      'dependencies': [
+        'drivetrain_queue',
+        '<(AOS)/common/controls/controls.gyp:replay_control_loop',
+        '<(AOS)/linux_code/linux_code.gyp:init',
+      ],
+    },
+    {
       'target_name': 'drivetrain_queue',
       'type': 'static_library',
       'sources': ['drivetrain.q'],
diff --git a/frc971/control_loops/drivetrain/replay_drivetrain.cc b/frc971/control_loops/drivetrain/replay_drivetrain.cc
new file mode 100644
index 0000000..432efdc
--- /dev/null
+++ b/frc971/control_loops/drivetrain/replay_drivetrain.cc
@@ -0,0 +1,24 @@
+#include "aos/common/controls/replay_control_loop.h"
+#include "aos/linux_code/init.h"
+
+#include "frc971/control_loops/drivetrain/drivetrain.q.h"
+
+// Reads one or more log files and sends out all the queue messages (in the
+// correct order and at the correct time) to feed a "live" drivetrain process.
+
+int main(int argc, char **argv) {
+  if (argc <= 1) {
+    fprintf(stderr, "Need at least one file to replay!\n");
+    return EXIT_FAILURE;
+  }
+
+  ::aos::InitNRT();
+
+  ::aos::controls::ControlLoopReplayer<::frc971::control_loops::DrivetrainQueue>
+      replayer(&::frc971::control_loops::drivetrain_queue, "drivetrain");
+  for (int i = 1; i < argc; ++i) {
+    replayer.ProcessFile(argv[i]);
+  }
+
+  ::aos::Cleanup();
+}
diff --git a/frc971/control_loops/fridge/fridge.gyp b/frc971/control_loops/fridge/fridge.gyp
index 865d42d..aaf3153 100644
--- a/frc971/control_loops/fridge/fridge.gyp
+++ b/frc971/control_loops/fridge/fridge.gyp
@@ -1,6 +1,21 @@
 {
   'targets': [
     {
+      'target_name': 'replay_fridge',
+      'type': 'executable',
+      'variables': {
+        'no_rsync': 1,
+      },
+      'sources': [
+        'replay_fridge.cc',
+      ],
+      'dependencies': [
+        'fridge_queue',
+        '<(AOS)/common/controls/controls.gyp:replay_control_loop',
+        '<(AOS)/linux_code/linux_code.gyp:init',
+      ],
+    },
+    {
       'target_name': 'fridge_queue',
       'type': 'static_library',
       'sources': ['fridge.q'],
diff --git a/frc971/control_loops/fridge/replay_fridge.cc b/frc971/control_loops/fridge/replay_fridge.cc
new file mode 100644
index 0000000..87833ef
--- /dev/null
+++ b/frc971/control_loops/fridge/replay_fridge.cc
@@ -0,0 +1,24 @@
+#include "aos/common/controls/replay_control_loop.h"
+#include "aos/linux_code/init.h"
+
+#include "frc971/control_loops/fridge/fridge.q.h"
+
+// Reads one or more log files and sends out all the queue messages (in the
+// correct order and at the correct time) to feed a "live" fridge process.
+
+int main(int argc, char **argv) {
+  if (argc <= 1) {
+    fprintf(stderr, "Need at least one file to replay!\n");
+    return EXIT_FAILURE;
+  }
+
+  ::aos::InitNRT();
+
+  ::aos::controls::ControlLoopReplayer<::frc971::control_loops::FridgeQueue>
+      replayer(&::frc971::control_loops::fridge_queue, "fridge");
+  for (int i = 1; i < argc; ++i) {
+    replayer.ProcessFile(argv[i]);
+  }
+
+  ::aos::Cleanup();
+}
diff --git a/frc971/prime/prime.gyp b/frc971/prime/prime.gyp
index 1b19d7a..e30bd22 100644
--- a/frc971/prime/prime.gyp
+++ b/frc971/prime/prime.gyp
@@ -10,10 +10,13 @@
         '../control_loops/control_loops.gyp:position_sensor_sim_test',
         '../control_loops/drivetrain/drivetrain.gyp:drivetrain',
         '../control_loops/drivetrain/drivetrain.gyp:drivetrain_lib_test',
+        '../control_loops/drivetrain/drivetrain.gyp:replay_drivetrain',
         '../control_loops/fridge/fridge.gyp:fridge',
         '../control_loops/fridge/fridge.gyp:fridge_lib_test',
+        '../control_loops/fridge/fridge.gyp:replay_fridge',
         '../control_loops/claw/claw.gyp:claw',
         '../control_loops/claw/claw.gyp:claw_lib_test',
+        '../control_loops/claw/claw.gyp:replay_claw',
         '../autonomous/autonomous.gyp:auto',
         '../frc971.gyp:joystick_reader',
         '../zeroing/zeroing.gyp:zeroing_test',
diff --git a/output/.gitignore b/output/.gitignore
index b617a67..d675b14 100644
--- a/output/.gitignore
+++ b/output/.gitignore
@@ -2,14 +2,14 @@
 /crio/
 /flasher/
 /compiled-*/
-/ip_base.txt
+/ip_address.txt
 /ccache_dir/
 
 /crio/
 /crio-debug/
 
-/arm-[^-]*-[^-]*/
-/arm-[^-]*-debug-[^-]*/
+/arm_frc-[^-]*-[^-]*/
+/arm_frc-[^-]*-debug-[^-]*/
 /amd64-[^-]*-[^-]*/
 /amd64-[^-]*-debug-[^-]*/
 /bot3-[^-]*-[^-]*-[^-]*/