OpenCPN Partial API docs
Loading...
Searching...
No Matches
data_monitor.cpp
1#include <chrono>
2#include <fstream>
3#include <sstream>
4
5#include <wx/app.h>
6#include <wx/button.h>
7#include <wx/choice.h>
8#include <wx/filedlg.h>
9#include <wx/menu.h>
10#include <wx/panel.h>
11#include <wx/platinfo.h>
12#include <wx/sizer.h>
13#include <wx/statline.h>
14#include <wx/stattext.h>
15#include <wx/translation.h>
16#include <wx/wrapsizer.h>
17
18#ifdef __ANDROID__
19#include "androidUTIL.h"
20#endif
21
22#include "model/base_platform.h"
25#include "model/navmsg_filter.h"
26#include "model/nmea_log.h"
27#include "model/gui.h"
28
29#include "data_monitor.h"
30#include "std_filesystem.h"
31#include "svg_button.h"
32#include "svg_icons.h"
33#include "tty_scroll.h"
34#include "filter_dlg.h"
35
36#pragma clang diagnostic push
37#pragma ide diagnostic ignored "UnreachableCode"
38
39// Make _() return std::string instead of wxString;
40#undef _
41#if wxCHECK_VERSION(3, 2, 0)
42#define _(s) wxGetTranslation(wxASCII_STR(s)).ToStdString()
43#else
44#define _(s) wxGetTranslation((s)).ToStdString()
45#endif
46
47using SetFormatFunc = std::function<void(DataLogger::Format, std::string)>;
48
49extern BasePlatform* g_BasePlatform;
50
52template <typename T>
53T* GetWindowById(int id) {
54 return dynamic_cast<T*>(wxWindow::FindWindowById(id));
55};
56
57static const char* const kFilterChoiceName = "FilterChoiceWindow";
58
59// clang-format: off
60static const std::unordered_map<NavAddr::Bus, std::string> kSourceByBus = {
61 {NavAddr::Bus::N0183, "NMEA0183"},
62 {NavAddr::Bus::N2000, "NMEA2000"},
63 {NavAddr::Bus::Signalk, "SignalK"}}; // clang-format: on
64
66static bool IsUserFilter(const std::string& filter_name) {
67 std::vector<std::string> filters = filters_on_disk::List();
68 auto found = std::find(filters.begin(), filters.end(), filter_name);
69 if (found != filters.end()) return true;
70 for (auto& f : filters) {
72 if (nf.m_description == filter_name) return true;
73 }
74 return false;
75};
76
78static std::string TimeStamp(const NavmsgTimePoint& when,
79 const NavmsgTimePoint& since) {
80 using namespace std::chrono;
81 using namespace std;
82
83 auto duration = when - since;
84 std::stringstream ss;
85 auto hrs = duration_cast<hours>(duration) % 24;
86 duration -= duration_cast<hours>(duration) / 24;
87 auto mins = duration_cast<minutes>(duration) % 60;
88 duration -= duration_cast<minutes>(duration) / 60;
89 auto secs = duration_cast<seconds>(duration) % 60;
90 duration -= duration_cast<seconds>(duration) / 60;
91 auto msecs = duration_cast<milliseconds>(duration);
92 ss << setw(2) << setfill('0') << hrs.count() << ":" << setw(2) << mins.count()
93 << ":" << setw(2) << secs.count() << "." << setw(3)
94 << msecs.count() % 1000;
95 return ss.str();
96}
97
104static std::string VdrQuote(const std::string& arg) {
105 auto static const npos = std::string::npos;
106 if (arg.find(',') == npos && arg.find('"') == npos) return arg;
107 std::string s;
108 for (auto c : arg) {
109 if (c == '"')
110 s += "\"\"";
111 else
112 s += c;
113 }
114 return "\"" + s + "\"";
115}
116
121static void AddVdrLogline(const Logline& ll, std::ostream& stream) {
122 if (kSourceByBus.find(ll.navmsg->bus) == kSourceByBus.end()) return;
123
124 using namespace std::chrono;
125 auto now = system_clock::now();
126 auto ms = duration_cast<milliseconds>(now.time_since_epoch()).count();
127 stream << ms << ",";
128
129 stream << kSourceByBus.at(ll.navmsg->bus) << ",";
130 stream << ll.navmsg->source->iface << ",";
131 switch (ll.navmsg->bus) {
132 case NavAddr::Bus::N0183: {
133 auto msg0183 = std::dynamic_pointer_cast<const Nmea0183Msg>(ll.navmsg);
134 stream << msg0183->talker << msg0183->type << ",";
135 } break;
136 case NavAddr::Bus::N2000: {
137 auto msg2000 = std::dynamic_pointer_cast<const Nmea2000Msg>(ll.navmsg);
138 stream << msg2000->PGN.to_string() << ",";
139 } break;
140 case NavAddr::Bus::Signalk: {
141 auto msgSignalK = std::dynamic_pointer_cast<const SignalkMsg>(ll.navmsg);
142 stream << "\"" << msgSignalK->context_self << "\",";
143 } break;
144 default:
145 assert(false && "Illegal message type");
146 };
147 stream << VdrQuote(ll.navmsg->to_vdr()) << "\n";
148}
149
151static void AddStdLogline(const Logline& ll, std::ostream& stream, char fs,
152 const NavmsgTimePoint log_start) {
153 if (!ll.navmsg) return;
154 wxString ws;
155 ws << TimeStamp(ll.navmsg->created_at, log_start) << fs;
156 if (ll.state.direction == NavmsgStatus::Direction::kOutput)
157 ws << kUtfRightArrow << fs;
158 else if (ll.state.direction == NavmsgStatus::Direction::kInput)
159 ws << kUtfLeftwardsArrowToBar << fs;
160 else if (ll.state.direction == NavmsgStatus::Direction::kInternal)
161 ws << kUtfLeftRightArrow << fs;
162 else
163 ws << kUtfLeftArrow << fs;
164 if (ll.state.status != NavmsgStatus::State::kOk)
165 ws << kUtfMultiplicationX << fs;
166 else if (ll.state.accepted == NavmsgStatus::Accepted::kFilteredNoOutput)
167 ws << kUtfFallingDiagonal << fs;
168 else if (ll.state.accepted == NavmsgStatus::Accepted::kFilteredDropped)
169 ws << kUtfCircledDivisionSlash << fs;
170 else
171 ws << kUtfCheckMark << fs;
172
173 ws << ll.navmsg->source->iface << fs;
174 ws << NavAddr::BusToString(ll.navmsg->bus) << fs;
175 if (ll.state.status != NavmsgStatus::State::kOk)
176 ws << (!ll.error_msg.empty() ? ll.error_msg : "Unknown errror");
177 else
178 ws << "ok";
179 ws << fs << ll.message << "\n";
180 stream << ws;
181}
182
184class TtyPanel : public wxPanel, public NmeaLog {
185public:
186 TtyPanel(wxWindow* parent, size_t lines)
187 : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize,
188 wxTAB_TRAVERSAL, "TtyPanel"),
189 m_tty_scroll(nullptr),
190 m_filter(this, wxID_ANY),
191 m_lines(lines),
192 m_on_right_click([] {}) {
193 auto vbox = new wxBoxSizer(wxVERTICAL);
194 m_tty_scroll = new TtyScroll(this, m_lines);
195 m_tty_scroll->Bind(wxEVT_RIGHT_UP,
196 [&](wxMouseEvent&) { m_on_right_click(); });
197 vbox->Add(m_tty_scroll, wxSizerFlags(1).Expand().Border());
198 m_filter.Hide();
199 SetSizer(vbox);
200 Fit();
201 }
202
203 void Add(const Logline& ll) override { m_tty_scroll->Add(ll); }
204
205 bool IsVisible() const override { return IsShownOnScreen(); }
206
207 void OnStop(bool stop) {
208 m_tty_scroll->Pause(stop);
209 if (stop)
210 m_tty_scroll->ShowScrollbars(wxSHOW_SB_DEFAULT, wxSHOW_SB_DEFAULT);
211 else
212 m_tty_scroll->ShowScrollbars(wxSHOW_SB_NEVER, wxSHOW_SB_NEVER);
213 }
214
215 void SetFilter(const NavmsgFilter& f) { m_tty_scroll->SetFilter(f); };
216
217 void SetQuickFilter(const std::string& filter) {
218 m_tty_scroll->SetQuickFilter(filter);
219 }
220
221 void SetOnRightClick(std::function<void()> f) {
222 m_on_right_click = std::move(f);
223 }
224
226 static void AddIfExists(const Logline& ll) {
227 auto window = wxWindow::FindWindowByName("TtyPanel");
228 if (!window) return;
229 auto tty_panel = dynamic_cast<TtyPanel*>(window);
230 if (tty_panel) tty_panel->Add(ll);
231 }
232
233protected:
234 wxSize DoGetBestClientSize() const override {
235 return wxSize(-1, m_lines * GetCharHeight());
236 }
237
238private:
239 TtyScroll* m_tty_scroll;
240 wxTextCtrl m_filter;
241 size_t m_lines;
242 std::function<void()> m_on_right_click;
243};
244
246class QuickFilterPanel : public wxPanel {
247public:
248 QuickFilterPanel(wxWindow* parent, std::function<void()> on_text_evt)
249 : wxPanel(parent),
250 m_text_ctrl(new wxTextCtrl(this, wxID_ANY)),
251 m_on_text_evt(std::move(on_text_evt)) {
252 auto hbox = new wxBoxSizer(wxHORIZONTAL);
253 auto flags = wxSizerFlags(0).Border();
254 auto label_box = new wxBoxSizer(wxVERTICAL);
255 label_box->Add(new wxStaticText(this, wxID_ANY, _("Quick filter:")));
256 hbox->Add(label_box, flags.Align(wxALIGN_CENTER_VERTICAL));
257 hbox->Add(m_text_ctrl, flags);
258 SetSizer(hbox);
259 Fit();
260 Show();
261 m_text_ctrl->Bind(wxEVT_TEXT, [&](wxCommandEvent&) { m_on_text_evt(); });
262 }
263
264 bool Show(bool show = true) override {
265 if (!show) m_text_ctrl->SetValue("");
266 return wxWindow::Show(show);
267 }
268
269 std::string GetValue() { return m_text_ctrl->GetValue().ToStdString(); }
270
271private:
272 wxTextCtrl* m_text_ctrl;
273 std::function<void()> m_on_text_evt;
274};
275
277class FilterChoice : public wxChoice {
278public:
279 FilterChoice(wxWindow* parent, TtyPanel* tty_panel)
280 : wxChoice(parent, wxID_ANY), m_tty_panel(tty_panel) {
281 SetName(kFilterChoiceName);
282 Bind(wxEVT_CHOICE, [&](wxCommandEvent&) { OnChoice(); });
283 OnFilterListChange();
284 int ix = FindString(kLabels.at("default"));
285 if (ix != wxNOT_FOUND) SetSelection(ix);
286 NavmsgFilter filter = filters_on_disk::Read("default.filter");
287 m_tty_panel->SetFilter(filter);
288 }
289
290 void OnFilterListChange() {
291 m_filters = NavmsgFilter::GetAllFilters();
292 int select_ix = GetSelection();
293 std::string selected;
294 if (select_ix != wxNOT_FOUND) selected = GetString(select_ix).ToStdString();
295 Clear();
296 for (auto& filter : m_filters) {
297 try {
298 Append(kLabels.at(filter.m_name));
299 } catch (std::out_of_range&) {
300 if (filter.m_description.empty())
301 Append(filter.m_name);
302 else
303 Append(filter.m_description);
304 }
305 }
306 if (!selected.empty()) {
307 int ix = FindString(selected);
308 SetSelection(ix == wxNOT_FOUND ? 0 : ix);
309 }
310 }
311
312 void OnFilterUpdate(const std::string& name) {
313 m_filters = NavmsgFilter::GetAllFilters();
314 int select_ix = GetSelection();
315 if (select_ix == wxNOT_FOUND) return;
316
317 std::string selected = GetString(select_ix).ToStdString();
318 if (selected != name) return;
319
320 NavmsgFilter filter = filters_on_disk::Read(name);
321 m_tty_panel->SetFilter(filter);
322 }
323
324 void OnApply(const std::string& name) {
325 int found = FindString(name);
326 if (found == wxNOT_FOUND) {
327 for (auto& filter : m_filters) {
328 if (filter.m_name == name) {
329 found = FindString(filter.m_description);
330 break;
331 }
332 }
333 }
334 if (found == wxNOT_FOUND) return;
335
336 SetSelection(found);
337 OnFilterUpdate(name);
338 }
339
340private:
341 // Translated labels for system filters by filter name. If not
342 // found the untranslated json description is used.
343 const std::unordered_map<std::string, std::string> kLabels = {
344 {"all-data", _("All data")},
345 {"all-nmea", _("All NMEA data")},
346 {"default", _("Default settings")},
347 {"malformed", _("Malformed messages")},
348 {"nmea-input", _("NMEA input data")},
349 {"nmea-output", _("NMEA output data")},
350 {"plugins", _("Messages to plugins")},
351 };
352
353 std::vector<NavmsgFilter> m_filters;
354 TtyPanel* m_tty_panel;
355
356 void OnChoice() {
357 wxString label = GetString(GetSelection());
358 NavmsgFilter filter = FilterByLabel(label.ToStdString());
359 m_tty_panel->SetFilter(filter);
360 }
361
362 NavmsgFilter FilterByLabel(const std::string& label) {
363 std::string name;
364 for (const auto& kv : kLabels) {
365 if (kv.second == label) {
366 name = kv.first;
367 break;
368 }
369 }
370 if (!name.empty()) {
371 for (auto& f : m_filters)
372 if (f.m_name == name) return f;
373 } else {
374 for (auto& f : m_filters)
375 if (f.m_description == label) return f;
376 }
377 return NavmsgFilter();
378 }
379};
380
382class PauseResumeButton : public wxButton {
383public:
384 PauseResumeButton(wxWindow* parent, std::function<void(bool)> on_stop)
385 : wxButton(parent, wxID_ANY),
386 is_paused(true),
387 m_on_stop(std::move(on_stop)) {
388 Bind(wxEVT_BUTTON, [&](wxCommandEvent&) { OnClick(); });
389 OnClick();
390 }
391
392private:
393 bool is_paused;
394 std::function<void(bool)> m_on_stop;
395
396 void OnClick() {
397 is_paused = !is_paused;
398 m_on_stop(is_paused);
399 SetLabel(is_paused ? _("Resume") : _("Pause"));
400 }
401};
402
404class CloseButton : public wxButton {
405public:
406 CloseButton(wxWindow* parent, std::function<void()> on_close)
407 : wxButton(parent, wxID_ANY), m_on_close(std::move(on_close)) {
408 SetLabel(_("Close"));
409 Bind(wxEVT_BUTTON, [&](wxCommandEvent&) { OnClick(); });
410 OnClick();
411 }
412
413private:
414 std::function<void()> m_on_close;
415
416 void OnClick() { m_on_close(); }
417};
418
420class LoggingSetup : public wxDialog {
421public:
423 class ThePanel : public wxPanel {
424 public:
425 ThePanel(wxWindow* parent, SetFormatFunc set_logtype, DataLogger& logger)
426 : wxPanel(parent),
427 m_overwrite(false),
428 m_set_logtype(set_logtype),
429 m_logger(logger),
430 kFilenameLabelId(wxWindow::NewControlId()) {
431 auto flags = wxSizerFlags(0).Border();
432
433 /* left column: Select log format. */
434 auto vdr_btn = new wxRadioButton(this, wxID_ANY, "VDR");
435 vdr_btn->Bind(wxEVT_RADIOBUTTON, [&](wxCommandEvent e) {
436 m_set_logtype(DataLogger::Format::kVdr, "VDR");
437 });
438 auto default_btn = new wxRadioButton(this, wxID_ANY, "Default");
439 default_btn->Bind(wxEVT_RADIOBUTTON, [&](wxCommandEvent e) {
440 m_set_logtype(DataLogger::Format::kDefault, _("Default"));
441 });
442 default_btn->SetValue(true);
443 auto csv_btn = new wxRadioButton(this, wxID_ANY, "CSV");
444 csv_btn->Bind(wxEVT_RADIOBUTTON, [&](wxCommandEvent e) {
445 m_set_logtype(DataLogger::Format::kCsv, "CSV");
446 });
447 auto left_vbox = new wxStaticBoxSizer(wxVERTICAL, this, _("Log format"));
448 left_vbox->Add(default_btn, flags.DoubleBorder());
449 left_vbox->Add(vdr_btn, flags);
450 left_vbox->Add(csv_btn, flags);
451
452 /* Right column: log file */
453 m_logger.SetLogfile(m_logger.GetDefaultLogfile());
454 auto label = new wxStaticText(this, kFilenameLabelId,
455 m_logger.GetDefaultLogfile().string());
456 auto path_btn = new wxButton(this, wxID_ANY, _("Change..."));
457 path_btn->Bind(wxEVT_BUTTON, [&](wxCommandEvent&) { OnFileDialog(); });
458 auto force_box =
459 new wxCheckBox(this, wxID_ANY, _("Overwrite existing file"));
460 force_box->Bind(wxEVT_CHECKBOX,
461 [&](wxCommandEvent& e) { m_overwrite = e.IsChecked(); });
462 auto right_vbox = new wxStaticBoxSizer(wxVERTICAL, this, _("Log file"));
463 right_vbox->Add(label, flags);
464 right_vbox->Add(path_btn, flags);
465 right_vbox->Add(force_box, flags);
466
467 /* Top part above buttons */
468 auto hbox = new wxBoxSizer(wxHORIZONTAL);
469 hbox->Add(left_vbox, flags);
470 hbox->Add(GetCharWidth() * 10, 0, 1);
471 hbox->Add(right_vbox, flags);
472 SetSizer(hbox);
473 Layout();
474 Show();
475
476 FilenameLstnr.Init(logger.OnNewLogfile, [&](ObservedEvt& ev) {
477 GetWindowById<wxStaticText>(kFilenameLabelId)->SetLabel(ev.GetString());
478 });
479 }
480
481 void OnFileDialog() {
482 long options = wxFD_SAVE;
483 if (!m_overwrite) options |= wxFD_OVERWRITE_PROMPT;
484 wxFileDialog dlg(m_parent, _("Select logfile"),
485 m_logger.GetDefaultLogfile().parent_path().string(),
486 m_logger.GetDefaultLogfile().stem().string(),
487 m_logger.GetFileDlgTypes(), options);
488 if (dlg.ShowModal() == wxID_CANCEL) return;
489 m_logger.SetLogfile(fs::path(dlg.GetPath().ToStdString()));
490 auto file_label = GetWindowById<wxStaticText>(kFilenameLabelId);
491 file_label->SetLabel(dlg.GetPath());
492 }
493
494 bool m_overwrite;
495 SetFormatFunc m_set_logtype;
496 DataLogger& m_logger;
497 const int kFilenameLabelId;
498 ObsListener FilenameLstnr;
499 }; // ThePanel
500
501 LoggingSetup(wxWindow* parent, SetFormatFunc set_logtype, DataLogger& logger)
502 : wxDialog(parent, wxID_ANY, _("Logging setup"), wxDefaultPosition,
503 wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) {
504 auto flags = wxSizerFlags(0).Border();
505
506 /* Buttons at bottom */
507 auto buttons = new wxStdDialogButtonSizer();
508 auto close_btn = new wxButton(this, wxID_CLOSE);
509 close_btn->Bind(wxEVT_COMMAND_BUTTON_CLICKED,
510 [&](wxCommandEvent& ev) { EndModal(0); });
511 buttons->AddButton(close_btn);
512 buttons->Realize();
513 buttons->Fit(parent);
514
515 /* Overall vbox setup */
516 auto panel = new ThePanel(this, set_logtype, logger);
517 auto vbox = new wxBoxSizer(wxVERTICAL);
518 vbox->Add(panel, flags.Expand());
519 vbox->Add(new wxStaticLine(this, wxID_ANY), flags.Expand());
520 vbox->Add(buttons, flags.Expand());
521 SetSizer(vbox);
522 Fit();
523 Show();
524 }
525 ObsListener FilenameLstnr;
526};
527
529class TheMenu : public wxMenu {
530public:
531 enum class Id : char {
532 kNewFilter = 1, // MacOS does not want ids to be 0.
533 kEditFilter,
534 kDeleteFilter,
535 kEditActiveFilter,
536 kLogSetup,
537 kViewStdColors,
538 };
539
540 TheMenu(wxWindow* parent, DataLogger& logger)
541 : m_parent(parent), m_logger(logger) {
542 AppendCheckItem(static_cast<int>(Id::kViewStdColors), _("Use colors"));
543 Append(static_cast<int>(Id::kLogSetup), _("Logging..."));
544 auto filters = new wxMenu("");
545 AppendId(filters, Id::kNewFilter, _("Create new..."));
546 AppendId(filters, Id::kEditFilter, _("Edit..."));
547 AppendId(filters, Id::kDeleteFilter, _("Delete..."));
548 AppendSubMenu(filters, _("Filters..."));
549 if (IsUserFilter(m_filter))
550 Append(static_cast<int>(Id::kEditActiveFilter), _("Edit active filter"));
551
552 Bind(wxEVT_MENU, [&](wxCommandEvent& ev) {
553 switch (static_cast<Id>(ev.GetId())) {
554 case Id::kLogSetup:
555 ConfigureLogging();
556 break;
557
558 case Id::kViewStdColors:
559 SetColor(static_cast<int>(Id::kViewStdColors));
560 break;
561
562 case Id::kNewFilter:
563 CreateFilterDlg(parent);
564 break;
565
566 case Id::kEditFilter:
567 EditFilterDlg(wxTheApp->GetTopWindow());
568 break;
569
570 case Id::kEditActiveFilter:
571 EditOneFilterDlg(wxTheApp->GetTopWindow(), m_filter);
572 break;
573
574 case Id::kDeleteFilter:
575 RemoveFilterDlg(parent);
576 break;
577 }
578 });
579 Check(static_cast<int>(Id::kViewStdColors), true);
580 }
581
582 void SetFilterName(const std::string& filter) {
583 int id = static_cast<int>(Id::kEditActiveFilter);
584 if (FindItem(id)) Delete(id);
585 if (IsUserFilter(filter)) Append(id, _("Edit active filter"));
586 m_filter = filter;
587 }
588
589 void ConfigureLogging() {
590 auto dlg = new LoggingSetup(
591 m_parent,
592 [&](DataLogger::Format f, std::string s) { SetLogFormat(f, s); },
593 m_logger);
594 dlg->ShowModal();
595 auto monitor = wxWindow::FindWindowByName(kDataMonitorWindowName);
596 assert(monitor);
597 monitor->Layout();
598 }
599
600private:
601 wxMenuItem* AppendId(wxMenu* root, Id id, const wxString& label) {
602 return root->Append(static_cast<int>(id), label);
603 }
604
605 void SetLogFormat(DataLogger::Format format, const std::string& label) {
606 m_logger.SetFormat(format);
607 std::string extension =
608 format == DataLogger::Format::kDefault ? ".log" : ".csv";
609 fs::path path = m_logger.GetLogfile();
610 path = path.parent_path() / (path.stem().string() + extension);
611 m_logger.SetLogfile(path);
612 }
613
614 void SetColor(int id) {
615 auto* w = wxWindow::FindWindowByName("TtyScroll");
616 auto tty_scroll = dynamic_cast<TtyScroll*>(w);
617 if (!tty_scroll) return;
618
619 wxMenuItem* item = FindItem(id);
620 if (!item) return;
621 if (item->IsCheck() && item->IsChecked())
622 tty_scroll->SetColors(std::make_unique<StdColorsByState>());
623 else
624 tty_scroll->SetColors(
625 std::make_unique<NoColorsByState>(tty_scroll->GetForegroundColour()));
626 }
627
628 wxWindow* m_parent;
629 DataLogger& m_logger;
630 std::string m_filter;
631};
632
634class LogButton : public wxButton {
635public:
636 LogButton(wxWindow* parent, DataLogger& logger, TheMenu& menu)
637 : wxButton(parent, wxID_ANY),
638 is_logging(true),
639 m_is_inited(false),
640 m_logger(logger),
641 m_menu(menu) {
642 Bind(wxEVT_BUTTON, [&](wxCommandEvent&) { OnClick(); });
643 OnClick(true);
644 UpdateTooltip();
645 }
646
647 void UpdateTooltip() {
648 if (is_logging)
649 SetToolTip(_("Click to stop logging"));
650 else
651 SetToolTip(_("Click to start logging"));
652 }
653
654private:
655 bool is_logging;
656 bool m_is_inited;
657 DataLogger& m_logger;
658 TheMenu& m_menu;
659
660 void OnClick(bool ctor = false) {
661 if (!m_is_inited && !ctor) {
662 m_menu.ConfigureLogging();
663 m_is_inited = true;
664 }
665 is_logging = !is_logging;
666 SetLabel(is_logging ? _("Stop logging") : _("Start logging"));
667 UpdateTooltip();
668 m_logger.SetLogging(is_logging);
669 }
670};
671
674public:
675 CopyClipboardButton(wxWindow* parent) : SvgButton(parent) {
676 LoadIcon(kCopyIconSvg);
677 SetToolTip(_("Copy to clipboard"));
678 Bind(wxEVT_BUTTON, [&](wxCommandEvent&) {
679 auto* tty_scroll =
680 dynamic_cast<TtyScroll*>(wxWindow::FindWindowByName("TtyScroll"));
681 if (tty_scroll) tty_scroll->CopyToClipboard();
682 });
683 }
684};
685
687class FilterButton : public SvgButton {
688public:
689 FilterButton(wxWindow* parent, wxWindow* quick_filter)
690 : SvgButton(parent), m_quick_filter(quick_filter), m_show_filter(true) {
691 Bind(wxEVT_BUTTON, [&](wxCommandEvent&) { OnClick(); });
692 OnClick();
693 }
694
695private:
696 wxWindow* m_quick_filter;
697 bool m_show_filter;
698
699 void OnClick() {
700 LoadIcon(m_show_filter ? kFunnelSvg : kNoFunnelSvg);
701 m_show_filter = !m_show_filter;
702 m_quick_filter->Show(m_show_filter);
703 SetToolTip(m_show_filter ? _("Close quick filter")
704 : _("Open quick filter"));
705 GetGrandParent()->Layout();
706 }
707};
708
710class MenuButton : public SvgButton {
711public:
712 MenuButton(wxWindow* parent, TheMenu& menu,
713 std::function<std::string()> get_current_filter)
714 : SvgButton(parent),
715 m_menu(menu),
716 m_get_current_filter(std::move(get_current_filter)) {
717 LoadIcon(kMenuSvg);
718 Bind(wxEVT_BUTTON, [&](wxCommandEvent&) { OnClick(); });
719 SetToolTip(_("Open menu"));
720 }
721
722private:
723 TheMenu& m_menu;
724 std::function<std::string()> m_get_current_filter;
725
726 void OnClick() {
727 m_menu.SetFilterName(m_get_current_filter());
728 PopupMenu(&m_menu);
729 }
730};
731
733class StatusLine : public wxPanel {
734public:
735 StatusLine(wxWindow* parent, wxWindow* quick_filter, TtyPanel* tty_panel,
736 std::function<void(bool)> on_stop, std::function<void()> on_hide,
737 DataLogger& logger)
738 : wxPanel(parent),
739 m_is_resized(false),
740 m_filter_choice(new FilterChoice(this, tty_panel)),
741 m_menu(this, logger),
742 m_log_button(new LogButton(this, logger, m_menu)) {
743 // Add a containing sizer for labels, so they can be aligned vertically
744 auto filter_label_box = new wxBoxSizer(wxVERTICAL);
745 filter_label_box->Add(new wxStaticText(this, wxID_ANY, _("Filter")));
746
747 auto flags = wxSizerFlags(0).Border();
748 auto wbox = new wxWrapSizer(wxHORIZONTAL);
749 wbox->Add(m_log_button, flags);
750 // Stretching horizontal space. Does not work with a WrapSizer, known
751 // wx bug. Left in place if it becomes fixed.
752 wbox->Add(GetCharWidth() * 5, 0, 1);
753 wbox->Add(filter_label_box, flags.Align(wxALIGN_CENTER_VERTICAL));
754 wbox->Add(m_filter_choice, flags);
755 wbox->Add(new PauseResumeButton(this, std::move(on_stop)), flags);
756 wbox->Add(new FilterButton(this, quick_filter), flags);
757 auto get_current_filter = [&] {
758 return m_filter_choice->GetStringSelection().ToStdString();
759 };
760 wbox->Add(new CopyClipboardButton(this), flags);
761 wbox->Add(new MenuButton(this, m_menu, get_current_filter), flags);
762#ifdef ANDROID
763 wbox->Add(new CloseButton(this, std::move(on_hide)), flags);
764#endif
765 SetSizer(wbox);
766 Layout();
767 Show();
768
769 Bind(wxEVT_SIZE, [&](wxSizeEvent& ev) {
770 m_is_resized = true;
771 ev.Skip();
772 });
773 Bind(wxEVT_RIGHT_UP, [&](wxMouseEvent& ev) {
774 m_menu.SetFilterName(m_filter_choice->GetStringSelection().ToStdString());
775 PopupMenu(&m_menu);
776 });
777 }
778
779 void OnContextClick() {
780 m_menu.SetFilterName(m_filter_choice->GetStringSelection().ToStdString());
781 PopupMenu(&m_menu);
782 }
783
784protected:
785 // Make sure the initial size is sane, don't meddle when user resizes
786 // dialog
787 wxSize DoGetBestClientSize() const override {
788 if (m_is_resized)
789 return wxSize(-1, -1);
790 else
791 return wxSize(85 * GetCharWidth(), 2.5 * GetCharHeight());
792 }
793
794private:
795 bool m_is_resized;
796 wxChoice* m_filter_choice;
797 TheMenu m_menu;
798 wxButton* m_log_button;
799};
800
801DataLogger::DataLogger(wxWindow* parent, const fs::path& path)
802 : m_parent(parent),
803 m_path(path),
804 m_stream(path, std::ios_base::app),
805 m_is_logging(false),
806 m_format(Format::kDefault),
807 m_log_start(NavmsgClock::now()) {}
808
809DataLogger::DataLogger(wxWindow* parent) : DataLogger(parent, NullLogfile()) {}
810
811void DataLogger::SetLogging(bool logging) { m_is_logging = logging; }
812
813void DataLogger::SetLogfile(const fs::path& path) {
814 m_stream = std::ofstream(path);
815 m_stream << "# timestamp_format: EPOCH_MILLIS\n";
816 const auto now = std::chrono::system_clock::now();
817 const std::time_t t_c = std::chrono::system_clock::to_time_t(now);
818 m_stream << "# Created at: " << std::ctime(&t_c) << " \n";
819 m_stream << "received_at,protocol,msg_type,source,raw_data\n";
820 m_stream << std::flush;
821 m_path = path;
822 OnNewLogfile.Notify(path.string());
823}
824
825void DataLogger::SetFormat(DataLogger::Format format) { m_format = format; }
826
827fs::path DataLogger::NullLogfile() {
828 if (wxPlatformInfo::Get().GetOperatingSystemId() & wxOS_WINDOWS)
829 return "NUL:";
830 else
831 return "/dev/null";
832}
833
834fs::path DataLogger::GetDefaultLogfile() {
835 if (m_path.stem() != NullLogfile().stem()) return m_path;
836 fs::path path(g_BasePlatform->GetHomeDir().ToStdString());
837 path /= "monitor";
838 path += (m_format == Format::kDefault ? ".log" : ".csv");
839 return path;
840}
841
842std::string DataLogger::GetFileDlgTypes() {
843 if (m_format == Format::kDefault)
844 return _("Log file (*.log)|*.log");
845 else
846 return _("Spreadsheet csv file(*.csv)|*.csv");
847}
848
849void DataLogger::Add(const Logline& ll) {
850 if (!m_is_logging || !ll.navmsg) return;
851 if (m_format == Format::kVdr && ll.navmsg->to_vdr().empty()) return;
852 if (m_format == DataLogger::Format::kVdr)
853 AddVdrLogline(ll, m_stream);
854 else
855 AddStdLogline(ll, m_stream,
856 m_format == DataLogger::Format::kCsv ? '|' : ' ',
857 m_log_start);
858}
859
860DataMonitor::DataMonitor(wxWindow* parent)
861 : wxFrame(parent, wxID_ANY, _("Data Monitor"), wxPoint(0, 0), wxDefaultSize,
862 wxDEFAULT_FRAME_STYLE | wxFRAME_FLOAT_ON_PARENT,
863 kDataMonitorWindowName),
864 m_monitor_src([&](const std::shared_ptr<const NavMsg>& navmsg) {
865 auto msg = std::dynamic_pointer_cast<const Nmea0183Msg>(navmsg);
866 TtyPanel::AddIfExists(navmsg);
867 }),
868 m_quick_filter(nullptr),
869 m_logger(parent) {
870 auto vbox = new wxBoxSizer(wxVERTICAL);
871 auto tty_panel = new TtyPanel(this, 12);
872 vbox->Add(tty_panel, wxSizerFlags(1).Expand().Border());
873 vbox->Add(new wxStaticLine(this), wxSizerFlags().Expand().Border());
874
875 auto on_quick_filter_evt = [&, tty_panel] {
876 auto* quick_filter = dynamic_cast<QuickFilterPanel*>(m_quick_filter);
877 assert(quick_filter);
878 std::string value = quick_filter->GetValue();
879 tty_panel->SetQuickFilter(value);
880 };
881 m_quick_filter = new QuickFilterPanel(this, on_quick_filter_evt);
882 vbox->Add(m_quick_filter, wxSizerFlags());
883
884 auto on_stop = [&, tty_panel](bool stop) { tty_panel->OnStop(stop); };
885 auto on_close = [&, this]() { this->OnHide(); };
886 auto status_line = new StatusLine(this, m_quick_filter, tty_panel, on_stop,
887 on_close, m_logger);
888 vbox->Add(status_line, wxSizerFlags().Expand());
889 SetSizer(vbox);
890 Fit();
891 Hide();
892
893 m_quick_filter->Bind(wxEVT_TEXT, [&, tty_panel](wxCommandEvent&) {
894 tty_panel->SetQuickFilter(GetLabel().ToStdString());
895 });
896 m_quick_filter->Hide();
897 tty_panel->SetOnRightClick(
898 [&, status_line] { status_line->OnContextClick(); });
899
900 Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent& ev) { Hide(); });
901 Bind(wxEVT_RIGHT_UP, [status_line](wxMouseEvent& ev) {
902 status_line->OnContextClick();
903 ev.Skip();
904 });
905 m_filter_list_lstnr.Init(FilterEvents::GetInstance().filter_list_change,
906 [&](ObservedEvt&) { OnFilterListChange(); });
907 m_filter_update_lstnr.Init(
908 FilterEvents::GetInstance().filter_update,
909 [&](ObservedEvt& ev) { OnFilterUpdate(ev.GetString().ToStdString()); });
910
911 m_filter_apply_lstnr.Init(
912 FilterEvents::GetInstance().filter_apply,
913 [&](ObservedEvt& ev) { OnFilterApply(ev.GetString().ToStdString()); });
914}
915
916void DataMonitor::Add(const Logline& ll) {
918 m_logger.Add(ll);
919}
920
922 wxWindow* w = wxWindow::FindWindowByName("TtyPanel");
923 assert(w && "No TtyPanel found");
924 return w->IsShownOnScreen();
925}
926
927void DataMonitor::OnFilterListChange() {
928 wxWindow* w = wxWindow::FindWindowByName(kFilterChoiceName);
929 if (!w) return;
930 auto filter_choice = dynamic_cast<FilterChoice*>(w);
931 assert(filter_choice && "Wrong FilterChoice type (!)");
932 filter_choice->OnFilterListChange();
933}
934
935void DataMonitor::OnFilterUpdate(const std::string& name) {
936 if (name != m_current_filter) return;
937 wxWindow* w = wxWindow::FindWindowByName("TtyScroll");
938 if (!w) return;
939 auto tty_scroll = dynamic_cast<TtyScroll*>(w);
940 assert(tty_scroll && "Wrong TtyScroll type (!)");
941 tty_scroll->SetFilter(filters_on_disk::Read(name));
942}
943
944void DataMonitor::OnFilterApply(const std::string& name) {
945 wxWindow* w = wxWindow::FindWindowByName(kFilterChoiceName);
946 if (!w) return;
947 auto filter_choice = dynamic_cast<FilterChoice*>(w);
948 assert(filter_choice && "Wrong FilterChoice type (!)");
949 m_current_filter = name;
950 filter_choice->OnApply(name);
951}
952
953void DataMonitor::OnHide() { Hide(); }
954
955#pragma clang diagnostic pop
Button to hide data monitor, used only on Android.
Copy to clipboard button.
Internal helper class.
EventVar OnNewLogfile
Notified with new path on filename change.
void Add(const Logline &ll) override
Add an input line to log output.
virtual bool IsVisible() const override
Return true if log is visible i.
const void Notify()
Notify all listeners, no data supplied.
Button opening the filter dialog.
Offer user to select current filter.
Button to start/stop logging.
Top part above buttons.
Log setup window invoked from menu "Logging" item.
Button invoking the popup menu.
Actual data sent between application and transport layer.
static std::vector< NavmsgFilter > GetAllFilters()
Return list of all filters, system + user defined.
Define an action to be performed when a KeyProvider is notified.
Definition observable.h:228
void Init(const KeyProvider &kp, std::function< void(ObservedEvt &ev)> action)
Initiate an object yet not listening.
Definition observable.h:255
Custom event class for OpenCPN's notification system.
Button to stop/resume messages in main log window.
The quick filter above the status line, invoked by funnel button.
Overall bottom status line.
A button capable of loading an svg image.
Definition svg_button.h:44
void LoadIcon(const char *svg)
Load an svg icon available in memory.
The monitor popup menu.
Main window, a rolling log of messages.
void Add(const Logline &ll) override
Add an formatted string to log output.
static void AddIfExists(const Logline &ll)
Invoke Add(s) for possibly existing instance.
bool IsVisible() const override
Return true if log is visible i.
Scrolled TTY-like window for logging, etc.
Definition tty_scroll.h:75
void SetQuickFilter(const std::string s)
Apply a quick filter directly matched against lines.
Definition tty_scroll.h:106
void Pause(bool pause)
Set the window to ignore Add() or not depending on pause.
Definition tty_scroll.h:94
void SetFilter(const NavmsgFilter &filter)
Apply a display filter.
Definition tty_scroll.h:100
virtual void Add(const Logline &line)
Add a line to bottom of window, typically discarding top-most line.
void CopyToClipboard() const
Copy message contents to clipboard.
New NMEA Debugger successor main window.
Provide a data stream of input messages for the Data Monitor.
Dialogs handing user defined filters.
std::vector< std::string > List(bool include_system)
Return list of filters, possibly including also the system ones.
NavmsgFilter Read(const std::string &name)
Read filter with given name from disk.
Filter storage routines.
Hooks into gui available in model.
Data monitor filter definitions.
Item in the log window.
Definition nmea_log.h:10
Scrolled TTY-like window for logging, related utilities.