consoleui.cc 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. #include "tools/replay/consoleui.h"
  2. #include <initializer_list>
  3. #include <string>
  4. #include <tuple>
  5. #include <utility>
  6. #include <QApplication>
  7. #include "common/util.h"
  8. #include "common/version.h"
  9. namespace {
  10. const int BORDER_SIZE = 3;
  11. const std::initializer_list<std::pair<std::string, std::string>> keyboard_shortcuts[] = {
  12. {
  13. {"s", "+10s"},
  14. {"shift+s", "-10s"},
  15. {"m", "+60s"},
  16. {"shift+m", "-60s"},
  17. {"space", "Pause/Resume"},
  18. {"e", "Next Engagement"},
  19. {"d", "Next Disengagement"},
  20. {"t", "Next User Tag"},
  21. {"i", "Next Info"},
  22. {"w", "Next Warning"},
  23. {"c", "Next Critical"},
  24. },
  25. {
  26. {"enter", "Enter seek request"},
  27. {"+/-", "Playback speed"},
  28. {"q", "Exit"},
  29. },
  30. };
  31. enum Color {
  32. Default,
  33. Debug,
  34. Yellow,
  35. Green,
  36. Red,
  37. Cyan,
  38. BrightWhite,
  39. Engaged,
  40. Disengaged,
  41. };
  42. void add_str(WINDOW *w, const char *str, Color color = Color::Default, bool bold = false) {
  43. if (color != Color::Default) wattron(w, COLOR_PAIR(color));
  44. if (bold) wattron(w, A_BOLD);
  45. waddstr(w, str);
  46. if (bold) wattroff(w, A_BOLD);
  47. if (color != Color::Default) wattroff(w, COLOR_PAIR(color));
  48. }
  49. } // namespace
  50. ConsoleUI::ConsoleUI(Replay *replay, QObject *parent) : replay(replay), sm({"carState", "liveParameters"}), QObject(parent) {
  51. // Initialize curses
  52. initscr();
  53. clear();
  54. curs_set(false);
  55. cbreak(); // Line buffering disabled. pass on everything
  56. noecho();
  57. keypad(stdscr, true);
  58. nodelay(stdscr, true); // non-blocking getchar()
  59. // Initialize all the colors. https://www.ditig.com/256-colors-cheat-sheet
  60. start_color();
  61. init_pair(Color::Debug, 246, COLOR_BLACK); // #949494
  62. init_pair(Color::Yellow, 184, COLOR_BLACK);
  63. init_pair(Color::Red, COLOR_RED, COLOR_BLACK);
  64. init_pair(Color::Cyan, COLOR_CYAN, COLOR_BLACK);
  65. init_pair(Color::BrightWhite, 15, COLOR_BLACK);
  66. init_pair(Color::Disengaged, COLOR_BLUE, COLOR_BLUE);
  67. init_pair(Color::Engaged, 28, 28);
  68. init_pair(Color::Green, 34, COLOR_BLACK);
  69. initWindows();
  70. qRegisterMetaType<uint64_t>("uint64_t");
  71. qRegisterMetaType<ReplyMsgType>("ReplyMsgType");
  72. installMessageHandler([this](ReplyMsgType type, const std::string msg) {
  73. emit logMessageSignal(type, QString::fromStdString(msg));
  74. });
  75. installDownloadProgressHandler([this](uint64_t cur, uint64_t total, bool success) {
  76. emit updateProgressBarSignal(cur, total, success);
  77. });
  78. QObject::connect(replay, &Replay::streamStarted, this, &ConsoleUI::updateSummary);
  79. QObject::connect(&notifier, SIGNAL(activated(int)), SLOT(readyRead()));
  80. QObject::connect(this, &ConsoleUI::updateProgressBarSignal, this, &ConsoleUI::updateProgressBar);
  81. QObject::connect(this, &ConsoleUI::logMessageSignal, this, &ConsoleUI::logMessage);
  82. sm_timer.callOnTimeout(this, &ConsoleUI::updateStatus);
  83. sm_timer.start(100);
  84. getch_timer.start(1000, this);
  85. readyRead();
  86. }
  87. ConsoleUI::~ConsoleUI() {
  88. endwin();
  89. }
  90. void ConsoleUI::initWindows() {
  91. getmaxyx(stdscr, max_height, max_width);
  92. w.fill(nullptr);
  93. w[Win::Title] = newwin(1, max_width, 0, 0);
  94. w[Win::Stats] = newwin(2, max_width - 2 * BORDER_SIZE, 2, BORDER_SIZE);
  95. w[Win::Timeline] = newwin(4, max_width - 2 * BORDER_SIZE, 5, BORDER_SIZE);
  96. w[Win::TimelineDesc] = newwin(1, 100, 10, BORDER_SIZE);
  97. w[Win::CarState] = newwin(3, 100, 12, BORDER_SIZE);
  98. w[Win::DownloadBar] = newwin(1, 100, 16, BORDER_SIZE);
  99. if (int log_height = max_height - 27; log_height > 4) {
  100. w[Win::LogBorder] = newwin(log_height, max_width - 2 * (BORDER_SIZE - 1), 17, BORDER_SIZE - 1);
  101. box(w[Win::LogBorder], 0, 0);
  102. w[Win::Log] = newwin(log_height - 2, max_width - 2 * BORDER_SIZE, 18, BORDER_SIZE);
  103. scrollok(w[Win::Log], true);
  104. }
  105. w[Win::Help] = newwin(5, max_width - (2 * BORDER_SIZE), max_height - 6, BORDER_SIZE);
  106. // set the title bar
  107. wbkgd(w[Win::Title], A_REVERSE);
  108. mvwprintw(w[Win::Title], 0, 3, "openpilot replay %s", COMMA_VERSION);
  109. // show windows on the real screen
  110. refresh();
  111. displayTimelineDesc();
  112. displayHelp();
  113. updateSummary();
  114. updateTimeline();
  115. for (auto win : w) {
  116. if (win) wrefresh(win);
  117. }
  118. }
  119. void ConsoleUI::timerEvent(QTimerEvent *ev) {
  120. if (ev->timerId() != getch_timer.timerId()) return;
  121. if (is_term_resized(max_height, max_width)) {
  122. for (auto win : w) {
  123. if (win) delwin(win);
  124. }
  125. endwin();
  126. clear();
  127. refresh();
  128. initWindows();
  129. rWarning("resize term %dx%d", max_height, max_width);
  130. }
  131. updateTimeline();
  132. }
  133. void ConsoleUI::updateStatus() {
  134. auto write_item = [this](int y, int x, const char *key, const std::string &value, const std::string &unit,
  135. bool bold = false, Color color = Color::BrightWhite) {
  136. auto win = w[Win::CarState];
  137. wmove(win, y, x);
  138. add_str(win, key);
  139. add_str(win, value.c_str(), color, bold);
  140. add_str(win, unit.c_str());
  141. };
  142. static const std::pair<const char *, Color> status_text[] = {
  143. {"loading...", Color::Red},
  144. {"playing", Color::Green},
  145. {"paused...", Color::Yellow},
  146. };
  147. sm.update(0);
  148. if (status != Status::Paused) {
  149. auto events = replay->events();
  150. uint64_t current_mono_time = replay->routeStartNanos() + replay->currentSeconds() * 1e9;
  151. bool playing = !events->empty() && events->back().mono_time > current_mono_time;
  152. status = playing ? Status::Playing : Status::Waiting;
  153. }
  154. auto [status_str, status_color] = status_text[status];
  155. write_item(0, 0, "STATUS: ", status_str, " ", false, status_color);
  156. std::string current_segment = " - " + std::to_string((int)(replay->currentSeconds() / 60));
  157. write_item(0, 25, "TIME: ", replay->currentDateTime().toString("ddd MMMM dd hh:mm:ss").toStdString(), current_segment, true);
  158. auto p = sm["liveParameters"].getLiveParameters();
  159. write_item(1, 0, "STIFFNESS: ", util::string_format("%.2f %%", p.getStiffnessFactor() * 100), " ");
  160. write_item(1, 25, "SPEED: ", util::string_format("%.2f", sm["carState"].getCarState().getVEgo()), " m/s");
  161. write_item(2, 0, "STEER RATIO: ", util::string_format("%.2f", p.getSteerRatio()), "");
  162. auto angle_offsets = util::string_format("%.2f|%.2f", p.getAngleOffsetAverageDeg(), p.getAngleOffsetDeg());
  163. write_item(2, 25, "ANGLE OFFSET(AVG|INSTANT): ", angle_offsets, " deg");
  164. wrefresh(w[Win::CarState]);
  165. }
  166. void ConsoleUI::displayHelp() {
  167. for (int i = 0; i < std::size(keyboard_shortcuts); ++i) {
  168. wmove(w[Win::Help], i * 2, 0);
  169. for (auto &[key, desc] : keyboard_shortcuts[i]) {
  170. wattron(w[Win::Help], A_REVERSE);
  171. waddstr(w[Win::Help], (' ' + key + ' ').c_str());
  172. wattroff(w[Win::Help], A_REVERSE);
  173. waddstr(w[Win::Help], (' ' + desc + ' ').c_str());
  174. }
  175. }
  176. wrefresh(w[Win::Help]);
  177. }
  178. void ConsoleUI::displayTimelineDesc() {
  179. std::tuple<Color, const char *, bool> indicators[]{
  180. {Color::Engaged, " Engaged ", false},
  181. {Color::Disengaged, " Disengaged ", false},
  182. {Color::Green, " Info ", true},
  183. {Color::Yellow, " Warning ", true},
  184. {Color::Red, " Critical ", true},
  185. {Color::Cyan, " User Tag ", true},
  186. };
  187. for (auto [color, name, bold] : indicators) {
  188. add_str(w[Win::TimelineDesc], "__", color, bold);
  189. add_str(w[Win::TimelineDesc], name);
  190. }
  191. }
  192. void ConsoleUI::logMessage(ReplyMsgType type, const QString &msg) {
  193. if (auto win = w[Win::Log]) {
  194. Color color = Color::Default;
  195. if (type == ReplyMsgType::Debug) {
  196. color = Color::Debug;
  197. } else if (type == ReplyMsgType::Warning) {
  198. color = Color::Yellow;
  199. } else if (type == ReplyMsgType::Critical) {
  200. color = Color::Red;
  201. }
  202. add_str(win, qPrintable(msg + "\n"), color);
  203. wrefresh(win);
  204. }
  205. }
  206. void ConsoleUI::updateProgressBar(uint64_t cur, uint64_t total, bool success) {
  207. werase(w[Win::DownloadBar]);
  208. if (success && cur < total) {
  209. const int width = 35;
  210. const float progress = cur / (double)total;
  211. const int pos = width * progress;
  212. wprintw(w[Win::DownloadBar], "Downloading [%s>%s] %d%% %s", std::string(pos, '=').c_str(),
  213. std::string(width - pos, ' ').c_str(), int(progress * 100.0), formattedDataSize(total).c_str());
  214. }
  215. wrefresh(w[Win::DownloadBar]);
  216. }
  217. void ConsoleUI::updateSummary() {
  218. const auto &route = replay->route();
  219. mvwprintw(w[Win::Stats], 0, 0, "Route: %s, %lu segments", qPrintable(route->name()), route->segments().size());
  220. mvwprintw(w[Win::Stats], 1, 0, "Car Fingerprint: %s", replay->carFingerprint().c_str());
  221. wrefresh(w[Win::Stats]);
  222. }
  223. void ConsoleUI::updateTimeline() {
  224. auto win = w[Win::Timeline];
  225. int width = getmaxx(win);
  226. werase(win);
  227. wattron(win, COLOR_PAIR(Color::Disengaged));
  228. mvwhline(win, 1, 0, ' ', width);
  229. mvwhline(win, 2, 0, ' ', width);
  230. wattroff(win, COLOR_PAIR(Color::Disengaged));
  231. const int total_sec = replay->maxSeconds() - replay->minSeconds();
  232. for (auto [begin, end, type] : replay->getTimeline()) {
  233. int start_pos = ((begin - replay->minSeconds()) / total_sec) * width;
  234. int end_pos = ((end - replay->minSeconds()) / total_sec) * width;
  235. if (type == TimelineType::Engaged) {
  236. mvwchgat(win, 1, start_pos, end_pos - start_pos + 1, A_COLOR, Color::Engaged, NULL);
  237. mvwchgat(win, 2, start_pos, end_pos - start_pos + 1, A_COLOR, Color::Engaged, NULL);
  238. } else if (type == TimelineType::UserFlag) {
  239. mvwchgat(win, 3, start_pos, end_pos - start_pos + 1, ACS_S3, Color::Cyan, NULL);
  240. } else {
  241. auto color_id = Color::Green;
  242. if (type != TimelineType::AlertInfo) {
  243. color_id = type == TimelineType::AlertWarning ? Color::Yellow : Color::Red;
  244. }
  245. mvwchgat(win, 3, start_pos, end_pos - start_pos + 1, ACS_S3, color_id, NULL);
  246. }
  247. }
  248. int cur_pos = ((replay->currentSeconds() - replay->minSeconds()) / total_sec) * width;
  249. wattron(win, COLOR_PAIR(Color::BrightWhite));
  250. mvwaddch(win, 0, cur_pos, ACS_VLINE);
  251. mvwaddch(win, 3, cur_pos, ACS_VLINE);
  252. wattroff(win, COLOR_PAIR(Color::BrightWhite));
  253. wrefresh(win);
  254. }
  255. void ConsoleUI::readyRead() {
  256. int c;
  257. while ((c = getch()) != ERR) {
  258. handleKey(c);
  259. }
  260. }
  261. void ConsoleUI::pauseReplay(bool pause) {
  262. replay->pause(pause);
  263. status = pause ? Status::Paused : Status::Waiting;
  264. }
  265. void ConsoleUI::handleKey(char c) {
  266. if (c == '\n') {
  267. // pause the replay and blocking getchar()
  268. pauseReplay(true);
  269. updateStatus();
  270. getch_timer.stop();
  271. curs_set(true);
  272. nodelay(stdscr, false);
  273. // Wait for user input
  274. rWarning("Waiting for input...");
  275. int y = getmaxy(stdscr) - 9;
  276. move(y, BORDER_SIZE);
  277. add_str(stdscr, "Enter seek request: ", Color::BrightWhite, true);
  278. refresh();
  279. // Seek to choice
  280. echo();
  281. int choice = 0;
  282. scanw((char *)"%d", &choice);
  283. noecho();
  284. pauseReplay(false);
  285. replay->seekTo(choice, false);
  286. // Clean up and turn off the blocking mode
  287. move(y, 0);
  288. clrtoeol();
  289. nodelay(stdscr, true);
  290. curs_set(false);
  291. refresh();
  292. getch_timer.start(1000, this);
  293. } else if (c == '+' || c == '=') {
  294. auto it = std::upper_bound(speed_array.begin(), speed_array.end(), replay->getSpeed());
  295. if (it != speed_array.end()) {
  296. rWarning("playback speed: %.1fx", *it);
  297. replay->setSpeed(*it);
  298. }
  299. } else if (c == '_' || c == '-') {
  300. auto it = std::lower_bound(speed_array.begin(), speed_array.end(), replay->getSpeed());
  301. if (it != speed_array.begin()) {
  302. auto prev = std::prev(it);
  303. rWarning("playback speed: %.1fx", *prev);
  304. replay->setSpeed(*prev);
  305. }
  306. } else if (c == 'e') {
  307. replay->seekToFlag(FindFlag::nextEngagement);
  308. } else if (c == 'd') {
  309. replay->seekToFlag(FindFlag::nextDisEngagement);
  310. } else if (c == 't') {
  311. replay->seekToFlag(FindFlag::nextUserFlag);
  312. } else if (c == 'i') {
  313. replay->seekToFlag(FindFlag::nextInfo);
  314. } else if (c == 'w') {
  315. replay->seekToFlag(FindFlag::nextWarning);
  316. } else if (c == 'c') {
  317. replay->seekToFlag(FindFlag::nextCritical);
  318. } else if (c == 'm') {
  319. replay->seekTo(+60, true);
  320. } else if (c == 'M') {
  321. replay->seekTo(-60, true);
  322. } else if (c == 's') {
  323. replay->seekTo(+10, true);
  324. } else if (c == 'S') {
  325. replay->seekTo(-10, true);
  326. } else if (c == ' ') {
  327. pauseReplay(!replay->isPaused());
  328. } else if (c == 'q' || c == 'Q') {
  329. qApp->exit();
  330. }
  331. }