blob: e8843a90e529a973b84b31d5c483775b53d27ac6 [file] [log] [blame]
James Kuszmaul7daef362019-12-31 18:28:17 -08001// This file provides a Python module for reading logfiles. See
2// log_reader_test.py for usage.
3//
4// This reader works by having the user specify exactly what channels they want
5// data for. We then process the logfile and store all the data on that channel
6// into a list of timestamps + JSON message data. The user can then use an
7// accessor method (get_data_for_channel) to retrieve the cached data.
8
9// Defining PY_SSIZE_T_CLEAN seems to be suggested by most of the Python
10// documentation.
11#define PY_SSIZE_T_CLEAN
12// Note that Python.h needs to be included before anything else.
13#include <Python.h>
14
15#include <memory>
16
17#include "aos/configuration.h"
18#include "aos/events/logging/logger.h"
19#include "aos/events/simulated_event_loop.h"
20#include "aos/flatbuffer_merge.h"
21#include "aos/json_to_flatbuffer.h"
22
23namespace frc971 {
24namespace analysis {
25namespace {
26
27// All the data corresponding to a single message.
28struct MessageData {
29 aos::monotonic_clock::time_point monotonic_sent_time;
30 aos::realtime_clock::time_point realtime_sent_time;
31 // JSON representation of the message.
32 std::string json_data;
33};
34
35// Data corresponding to an entire channel.
36struct ChannelData {
37 std::string name;
38 std::string type;
39 // Each message published on the channel, in order by monotonic time.
40 std::vector<MessageData> messages;
41};
42
43// All the objects that we need for managing reading a logfile.
44struct LogReaderTools {
45 std::unique_ptr<aos::logger::LogReader> reader;
James Kuszmaul7daef362019-12-31 18:28:17 -080046 // Event loop to use for subscribing to buses.
47 std::unique_ptr<aos::EventLoop> event_loop;
48 std::vector<ChannelData> channel_data;
49 // Whether we have called process() on the reader yet.
50 bool processed = false;
51};
52
53struct LogReaderType {
54 PyObject_HEAD;
55 LogReaderTools *tools = nullptr;
56};
57
58void LogReader_dealloc(LogReaderType *self) {
59 LogReaderTools *tools = self->tools;
James Kuszmaul7daef362019-12-31 18:28:17 -080060 delete tools;
61 Py_TYPE(self)->tp_free((PyObject *)self);
62}
63
64PyObject *LogReader_new(PyTypeObject *type, PyObject * /*args*/,
65 PyObject * /*kwds*/) {
66 LogReaderType *self;
67 self = (LogReaderType *)type->tp_alloc(type, 0);
68 if (self != nullptr) {
69 self->tools = new LogReaderTools();
70 if (self->tools == nullptr) {
71 return nullptr;
72 }
73 }
74 return (PyObject *)self;
75}
76
77int LogReader_init(LogReaderType *self, PyObject *args, PyObject *kwds) {
78 const char *kwlist[] = {"log_file_name", nullptr};
79
80 const char *log_file_name;
81 if (!PyArg_ParseTupleAndKeywords(args, kwds, "s", const_cast<char **>(kwlist),
82 &log_file_name)) {
83 return -1;
84 }
85
86 LogReaderTools *tools = CHECK_NOTNULL(self->tools);
87 tools->reader = std::make_unique<aos::logger::LogReader>(log_file_name);
James Kuszmaul84ff3e52020-01-03 19:48:53 -080088 tools->reader->Register();
James Kuszmaul7daef362019-12-31 18:28:17 -080089
Austin Schuh9fe5d202020-02-29 15:09:53 -080090 if (aos::configuration::MultiNode(tools->reader->configuration())) {
91 tools->event_loop = tools->reader->event_loop_factory()->MakeEventLoop(
92 "data_fetcher",
93 aos::configuration::GetNode(tools->reader->configuration(), "roborio"));
94 } else {
95 tools->event_loop =
96 tools->reader->event_loop_factory()->MakeEventLoop("data_fetcher");
97 }
James Kuszmaul7daef362019-12-31 18:28:17 -080098 tools->event_loop->SkipTimingReport();
Tyler Chatow67ddb032020-01-12 14:30:04 -080099 tools->event_loop->SkipAosLog();
James Kuszmaul7daef362019-12-31 18:28:17 -0800100
101 return 0;
102}
103
104PyObject *LogReader_get_data_for_channel(LogReaderType *self,
105 PyObject *args,
106 PyObject *kwds) {
107 const char *kwlist[] = {"name", "type", nullptr};
108
109 const char *name;
110 const char *type;
111 if (!PyArg_ParseTupleAndKeywords(args, kwds, "ss",
112 const_cast<char **>(kwlist), &name, &type)) {
113 return nullptr;
114 }
115
116 LogReaderTools *tools = CHECK_NOTNULL(self->tools);
117
118 if (!tools->processed) {
119 PyErr_SetString(PyExc_RuntimeError,
120 "Called get_data_for_bus before calling process().");
121 return nullptr;
122 }
123
124 for (const auto &channel : tools->channel_data) {
125 if (channel.name == name && channel.type == type) {
126 PyObject *list = PyList_New(channel.messages.size());
127 for (size_t ii = 0; ii < channel.messages.size(); ++ii)
128 {
129 const auto &message = channel.messages[ii];
130 PyObject *monotonic_time = PyLong_FromLongLong(
131 std::chrono::duration_cast<std::chrono::nanoseconds>(
132 message.monotonic_sent_time.time_since_epoch())
133 .count());
134 PyObject *realtime_time = PyLong_FromLongLong(
135 std::chrono::duration_cast<std::chrono::nanoseconds>(
136 message.realtime_sent_time.time_since_epoch())
137 .count());
138 PyObject *json_data = PyUnicode_FromStringAndSize(
139 message.json_data.data(), message.json_data.size());
140 PyObject *entry =
141 PyTuple_Pack(3, monotonic_time, realtime_time, json_data);
142 if (PyList_SetItem(list, ii, entry) != 0) {
143 return nullptr;
144 }
145 }
146 return list;
147 }
148 }
149 PyErr_SetString(PyExc_ValueError,
150 "The provided channel was never subscribed to.");
151 return nullptr;
152}
153
154PyObject *LogReader_subscribe(LogReaderType *self, PyObject *args,
155 PyObject *kwds) {
156 const char *kwlist[] = {"name", "type", nullptr};
157
158 const char *name;
159 const char *type;
160 if (!PyArg_ParseTupleAndKeywords(args, kwds, "ss",
161 const_cast<char **>(kwlist), &name, &type)) {
162 return nullptr;
163 }
164
165 LogReaderTools *tools = CHECK_NOTNULL(self->tools);
166
167 if (tools->processed) {
168 PyErr_SetString(PyExc_RuntimeError,
169 "Called subscribe after calling process().");
170 return nullptr;
171 }
172
173 const aos::Channel *const channel = aos::configuration::GetChannel(
174 tools->reader->configuration(), name, type, "", nullptr);
175 if (channel == nullptr) {
176 return Py_False;
177 }
178 const int index = tools->channel_data.size();
179 tools->channel_data.push_back(
180 {.name = name, .type = type, .messages = {}});
181 tools->event_loop->MakeRawWatcher(
182 channel, [channel, index, tools](const aos::Context &context,
183 const void *message) {
184 tools->channel_data[index].messages.push_back(
185 {.monotonic_sent_time = context.monotonic_event_time,
186 .realtime_sent_time = context.realtime_event_time,
187 .json_data = aos::FlatbufferToJson(
188 channel->schema(), static_cast<const uint8_t *>(message))});
189 });
190 return Py_True;
191}
192
193static PyObject *LogReader_process(LogReaderType *self,
194 PyObject *Py_UNUSED(ignored)) {
195 LogReaderTools *tools = CHECK_NOTNULL(self->tools);
196
197 if (tools->processed) {
198 PyErr_SetString(PyExc_RuntimeError, "process() may only be called once.");
199 return nullptr;
200 }
201
202 tools->processed = true;
203
James Kuszmaul84ff3e52020-01-03 19:48:53 -0800204 tools->reader->event_loop_factory()->Run();
James Kuszmaul7daef362019-12-31 18:28:17 -0800205
206 Py_RETURN_NONE;
207}
208
209static PyObject *LogReader_configuration(LogReaderType *self,
210 PyObject *Py_UNUSED(ignored)) {
211 LogReaderTools *tools = CHECK_NOTNULL(self->tools);
212
213 // I have no clue if the Configuration that we get from the log reader is in a
214 // contiguous chunk of memory, and I'm too lazy to either figure it out or
215 // figure out how to extract the actual data buffer + offset.
216 // Instead, copy the flatbuffer and return a copy of the new buffer.
217 aos::FlatbufferDetachedBuffer<aos::Configuration> buffer =
218 aos::CopyFlatBuffer(tools->reader->configuration());
219
220 return PyBytes_FromStringAndSize(
Austin Schuhadd6eb32020-11-09 21:24:26 -0800221 reinterpret_cast<const char *>(buffer.span().data()),
222 buffer.span().size());
James Kuszmaul7daef362019-12-31 18:28:17 -0800223}
224
225static PyMethodDef LogReader_methods[] = {
226 {"configuration", (PyCFunction)LogReader_configuration, METH_NOARGS,
227 "Return a bytes buffer for the Configuration of the logfile."},
228 {"process", (PyCFunction)LogReader_process, METH_NOARGS,
229 "Processes the logfile and all the subscribed to channels."},
230 {"subscribe", (PyCFunction)LogReader_subscribe,
231 METH_VARARGS | METH_KEYWORDS,
232 "Attempts to subscribe to the provided channel name + type. Returns True "
233 "if successful."},
234 {"get_data_for_channel", (PyCFunction)LogReader_get_data_for_channel,
235 METH_VARARGS | METH_KEYWORDS,
236 "Returns the logged data for a given channel. Raises an exception if you "
237 "did not subscribe to the provided channel. Returned data is a list of "
238 "tuples where each tuple is of the form (monotonic_nsec, realtime_nsec, "
239 "json_message_data)."},
240 {nullptr, 0, 0, nullptr} /* Sentinel */
241};
242
243static PyTypeObject LogReaderType = {
244 PyVarObject_HEAD_INIT(NULL, 0).tp_name = "py_log_reader.LogReader",
245 .tp_doc = "LogReader objects",
246 .tp_basicsize = sizeof(LogReaderType),
247 .tp_itemsize = 0,
248 .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
249 .tp_new = LogReader_new,
250 .tp_init = (initproc)LogReader_init,
251 .tp_dealloc = (destructor)LogReader_dealloc,
252 .tp_methods = LogReader_methods,
253};
254
255static PyModuleDef log_reader_module = {
256 PyModuleDef_HEAD_INIT,
257 .m_name = "py_log_reader",
258 .m_doc = "Example module that creates an extension type.",
259 .m_size = -1,
260};
261
262PyObject *InitModule() {
263 PyObject *m;
264 if (PyType_Ready(&LogReaderType) < 0) return nullptr;
265
266 m = PyModule_Create(&log_reader_module);
267 if (m == nullptr) return nullptr;
268
269 Py_INCREF(&LogReaderType);
270 if (PyModule_AddObject(m, "LogReader", (PyObject *)&LogReaderType) < 0) {
271 Py_DECREF(&LogReaderType);
272 Py_DECREF(m);
273 return nullptr;
274 }
275
276 return m;
277}
278
279} // namespace
280} // namespace analysis
281} // namespace frc971
282
283PyMODINIT_FUNC PyInit_py_log_reader(void) {
284 return frc971::analysis::InitModule();
285}