add support for replaying log messages
For example, this allows reproducing logged but hard to physically
reproduce problems to control loops with more logging to help debug the
problem, and then verifying that it does something different under the
same conditions.
Change-Id: I1e55e7a7c0b3154bcaf86373a9e6c57c594310a0
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/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/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 dc25b63..d64bf9a 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',