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 m_set_logtype(DataLogger::Format::kDefault, _("Default"));
477 FilenameLstnr.Init(logger.OnNewLogfile, [&](ObservedEvt& ev) {
478 GetWindowById<wxStaticText>(kFilenameLabelId)->SetLabel(ev.GetString());
479 });
480 }
481
482 void OnFileDialog() {
483 long options = wxFD_SAVE;
484 if (!m_overwrite) options |= wxFD_OVERWRITE_PROMPT;
485 wxFileDialog dlg(m_parent, _("Select logfile"),
486 m_logger.GetDefaultLogfile().parent_path().string(),
487 m_logger.GetDefaultLogfile().stem().string(),
488 m_logger.GetFileDlgTypes(), options);
489 if (dlg.ShowModal() == wxID_CANCEL) return;
490 m_logger.SetLogfile(fs::path(dlg.GetPath().ToStdString()));
491 auto file_label = GetWindowById<wxStaticText>(kFilenameLabelId);
492 file_label->SetLabel(dlg.GetPath());
493 }
494
495 bool m_overwrite;
496 SetFormatFunc m_set_logtype;
497 DataLogger& m_logger;
498 const int kFilenameLabelId;
499 ObsListener FilenameLstnr;
500 }; // ThePanel
501
502 LoggingSetup(wxWindow* parent, SetFormatFunc set_logtype, DataLogger& logger)
503 : wxDialog(parent, wxID_ANY, _("Logging setup"), wxDefaultPosition,
504 wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) {
505 auto flags = wxSizerFlags(0).Border();
506
507 /* Buttons at bottom */
508 auto buttons = new wxStdDialogButtonSizer();
509 auto close_btn = new wxButton(this, wxID_CLOSE);
510 close_btn->Bind(wxEVT_COMMAND_BUTTON_CLICKED,
511 [&](wxCommandEvent& ev) { EndModal(0); });
512 buttons->AddButton(close_btn);
513 buttons->Realize();
514 buttons->Fit(parent);
515
516 /* Overall vbox setup */
517 auto panel = new ThePanel(this, set_logtype, logger);
518 auto vbox = new wxBoxSizer(wxVERTICAL);
519 vbox->Add(panel, flags.Expand());
520 vbox->Add(new wxStaticLine(this, wxID_ANY), flags.Expand());
521 vbox->Add(buttons, flags.Expand());
522 SetSizer(vbox);
523 Fit();
524 Show();
525 }
526 ObsListener FilenameLstnr;
527};
528
530class TheMenu : public wxMenu {
531public:
532 enum class Id : char {
533 kNewFilter = 1, // MacOS does not want ids to be 0.
534 kEditFilter,
535 kDeleteFilter,
536 kEditActiveFilter,
537 kLogSetup,
538 kViewStdColors,
539 };
540
541 TheMenu(wxWindow* parent, DataLogger& logger)
542 : m_parent(parent), m_logger(logger) {
543 AppendCheckItem(static_cast<int>(Id::kViewStdColors), _("Use colors"));
544 Append(static_cast<int>(Id::kLogSetup), _("Logging..."));
545 auto filters = new wxMenu("");
546 AppendId(filters, Id::kNewFilter, _("Create new..."));
547 AppendId(filters, Id::kEditFilter, _("Edit..."));
548 AppendId(filters, Id::kDeleteFilter, _("Delete..."));
549 AppendSubMenu(filters, _("Filters..."));
550 if (IsUserFilter(m_filter))
551 Append(static_cast<int>(Id::kEditActiveFilter), _("Edit active filter"));
552
553 Bind(wxEVT_MENU, [&](wxCommandEvent& ev) {
554 switch (static_cast<Id>(ev.GetId())) {
555 case Id::kLogSetup:
556 ConfigureLogging();
557 break;
558
559 case Id::kViewStdColors:
560 SetColor(static_cast<int>(Id::kViewStdColors));
561 break;
562
563 case Id::kNewFilter:
564 CreateFilterDlg(parent);
565 break;
566
567 case Id::kEditFilter:
568 EditFilterDlg(wxTheApp->GetTopWindow());
569 break;
570
571 case Id::kEditActiveFilter:
572 EditOneFilterDlg(wxTheApp->GetTopWindow(), m_filter);
573 break;
574
575 case Id::kDeleteFilter:
576 RemoveFilterDlg(parent);
577 break;
578 }
579 });
580 Check(static_cast<int>(Id::kViewStdColors), true);
581 }
582
583 void SetFilterName(const std::string& filter) {
584 int id = static_cast<int>(Id::kEditActiveFilter);
585 if (FindItem(id)) Delete(id);
586 if (IsUserFilter(filter)) Append(id, _("Edit active filter"));
587 m_filter = filter;
588 }
589
590 void ConfigureLogging() {
591 auto dlg = new LoggingSetup(
592 m_parent,
593 [&](DataLogger::Format f, std::string s) { SetLogFormat(f, s); },
594 m_logger);
595 dlg->ShowModal();
596 auto monitor = wxWindow::FindWindowByName(kDataMonitorWindowName);
597 assert(monitor);
598 monitor->Layout();
599 }
600
601private:
602 wxMenuItem* AppendId(wxMenu* root, Id id, const wxString& label) {
603 return root->Append(static_cast<int>(id), label);
604 }
605
606 void SetLogFormat(DataLogger::Format format, const std::string& label) {
607 m_logger.SetFormat(format);
608 std::string extension =
609 format == DataLogger::Format::kDefault ? ".log" : ".csv";
610 fs::path path = m_logger.GetLogfile();
611 path = path.parent_path() / (path.stem().string() + extension);
612 m_logger.SetLogfile(path);
613 }
614
615 void SetColor(int id) {
616 auto* w = wxWindow::FindWindowByName("TtyScroll");
617 auto tty_scroll = dynamic_cast<TtyScroll*>(w);
618 if (!tty_scroll) return;
619
620 wxMenuItem* item = FindItem(id);
621 if (!item) return;
622 if (item->IsCheck() && item->IsChecked())
623 tty_scroll->SetColors(std::make_unique<StdColorsByState>());
624 else
625 tty_scroll->SetColors(
626 std::make_unique<NoColorsByState>(tty_scroll->GetForegroundColour()));
627 }
628
629 wxWindow* m_parent;
630 DataLogger& m_logger;
631 std::string m_filter;
632};
633
635class LogButton : public wxButton {
636public:
637 LogButton(wxWindow* parent, DataLogger& logger, TheMenu& menu)
638 : wxButton(parent, wxID_ANY),
639 is_logging(true),
640 m_is_inited(false),
641 m_logger(logger),
642 m_menu(menu) {
643 Bind(wxEVT_BUTTON, [&](wxCommandEvent&) { OnClick(); });
644 OnClick(true);
645 UpdateTooltip();
646 }
647
648 void UpdateTooltip() {
649 if (is_logging)
650 SetToolTip(_("Click to stop logging"));
651 else
652 SetToolTip(_("Click to start logging"));
653 }
654
655private:
656 bool is_logging;
657 bool m_is_inited;
658 DataLogger& m_logger;
659 TheMenu& m_menu;
660
661 void OnClick(bool ctor = false) {
662 if (!m_is_inited && !ctor) {
663 m_menu.ConfigureLogging();
664 m_is_inited = true;
665 }
666 is_logging = !is_logging;
667 SetLabel(is_logging ? _("Stop logging") : _("Start logging"));
668 UpdateTooltip();
669 m_logger.SetLogging(is_logging);
670 }
671};
672
675public:
676 CopyClipboardButton(wxWindow* parent) : SvgButton(parent) {
677 LoadIcon(kCopyIconSvg);
678 SetToolTip(_("Copy to clipboard"));
679 Bind(wxEVT_BUTTON, [&](wxCommandEvent&) {
680 auto* tty_scroll =
681 dynamic_cast<TtyScroll*>(wxWindow::FindWindowByName("TtyScroll"));
682 if (tty_scroll) tty_scroll->CopyToClipboard();
683 });
684 }
685};
686
688class FilterButton : public SvgButton {
689public:
690 FilterButton(wxWindow* parent, wxWindow* quick_filter)
691 : SvgButton(parent), m_quick_filter(quick_filter), m_show_filter(true) {
692 Bind(wxEVT_BUTTON, [&](wxCommandEvent&) { OnClick(); });
693 OnClick();
694 }
695
696private:
697 wxWindow* m_quick_filter;
698 bool m_show_filter;
699
700 void OnClick() {
701 LoadIcon(m_show_filter ? kFunnelSvg : kNoFunnelSvg);
702 m_show_filter = !m_show_filter;
703 m_quick_filter->Show(m_show_filter);
704 SetToolTip(m_show_filter ? _("Close quick filter")
705 : _("Open quick filter"));
706 GetGrandParent()->Layout();
707 }
708};
709
711class MenuButton : public SvgButton {
712public:
713 MenuButton(wxWindow* parent, TheMenu& menu,
714 std::function<std::string()> get_current_filter)
715 : SvgButton(parent),
716 m_menu(menu),
717 m_get_current_filter(std::move(get_current_filter)) {
718 LoadIcon(kMenuSvg);
719 Bind(wxEVT_BUTTON, [&](wxCommandEvent&) { OnClick(); });
720 SetToolTip(_("Open menu"));
721 }
722
723private:
724 TheMenu& m_menu;
725 std::function<std::string()> m_get_current_filter;
726
727 void OnClick() {
728 m_menu.SetFilterName(m_get_current_filter());
729 PopupMenu(&m_menu);
730 }
731};
732
734class StatusLine : public wxPanel {
735public:
736 StatusLine(wxWindow* parent, wxWindow* quick_filter, TtyPanel* tty_panel,
737 std::function<void(bool)> on_stop, std::function<void()> on_hide,
738 DataLogger& logger)
739 : wxPanel(parent),
740 m_is_resized(false),
741 m_filter_choice(new FilterChoice(this, tty_panel)),
742 m_menu(this, logger),
743 m_log_button(new LogButton(this, logger, m_menu)) {
744 // Add a containing sizer for labels, so they can be aligned vertically
745 auto filter_label_box = new wxBoxSizer(wxVERTICAL);
746 filter_label_box->Add(new wxStaticText(this, wxID_ANY, _("Filter")));
747
748 auto flags = wxSizerFlags(0).Border();
749 auto wbox = new wxWrapSizer(wxHORIZONTAL);
750 wbox->Add(m_log_button, flags);
751 // Stretching horizontal space. Does not work with a WrapSizer, known
752 // wx bug. Left in place if it becomes fixed.
753 wbox->Add(GetCharWidth() * 5, 0, 1);
754 wbox->Add(filter_label_box, flags.Align(wxALIGN_CENTER_VERTICAL));
755 wbox->Add(m_filter_choice, flags);
756 wbox->Add(new PauseResumeButton(this, std::move(on_stop)), flags);
757 wbox->Add(new FilterButton(this, quick_filter), flags);
758 auto get_current_filter = [&] {
759 return m_filter_choice->GetStringSelection().ToStdString();
760 };
761 wbox->Add(new CopyClipboardButton(this), flags);
762 wbox->Add(new MenuButton(this, m_menu, get_current_filter), flags);
763#ifdef ANDROID
764 wbox->Add(new CloseButton(this, std::move(on_hide)), flags);
765#endif
766 SetSizer(wbox);
767 Layout();
768 Show();
769
770 Bind(wxEVT_SIZE, [&](wxSizeEvent& ev) {
771 m_is_resized = true;
772 ev.Skip();
773 });
774 Bind(wxEVT_RIGHT_UP, [&](wxMouseEvent& ev) {
775 m_menu.SetFilterName(m_filter_choice->GetStringSelection().ToStdString());
776 PopupMenu(&m_menu);
777 });
778 }
779
780 void OnContextClick() {
781 m_menu.SetFilterName(m_filter_choice->GetStringSelection().ToStdString());
782 PopupMenu(&m_menu);
783 }
784
785protected:
786 // Make sure the initial size is sane, don't meddle when user resizes
787 // dialog
788 wxSize DoGetBestClientSize() const override {
789 if (m_is_resized)
790 return wxSize(-1, -1);
791 else
792 return wxSize(85 * GetCharWidth(), 2.5 * GetCharHeight());
793 }
794
795private:
796 bool m_is_resized;
797 wxChoice* m_filter_choice;
798 TheMenu m_menu;
799 wxButton* m_log_button;
800};
801
802DataLogger::DataLogger(wxWindow* parent, const fs::path& path)
803 : m_parent(parent),
804 m_path(path),
805 m_stream(path, std::ios_base::app),
806 m_is_logging(false),
807 m_format(Format::kDefault),
808 m_log_start(NavmsgClock::now()) {}
809
810DataLogger::DataLogger(wxWindow* parent) : DataLogger(parent, NullLogfile()) {}
811
812void DataLogger::SetLogging(bool logging) { m_is_logging = logging; }
813
814void DataLogger::SetLogfile(const fs::path& path) {
815 m_stream = std::ofstream(path);
816 m_stream << "# timestamp_format: EPOCH_MILLIS\n";
817 const auto now = std::chrono::system_clock::now();
818 const std::time_t t_c = std::chrono::system_clock::to_time_t(now);
819 m_stream << "# Created at: " << std::ctime(&t_c) << " \n";
820 m_stream << "received_at,protocol,msg_type,source,raw_data\n";
821 m_stream << std::flush;
822 m_path = path;
823 OnNewLogfile.Notify(path.string());
824}
825
826void DataLogger::SetFormat(DataLogger::Format format) { m_format = format; }
827
828fs::path DataLogger::NullLogfile() {
829 if (wxPlatformInfo::Get().GetOperatingSystemId() & wxOS_WINDOWS)
830 return "NUL:";
831 else
832 return "/dev/null";
833}
834
835fs::path DataLogger::GetDefaultLogfile() {
836 if (m_path.stem() != NullLogfile().stem()) return m_path;
837 fs::path path(g_BasePlatform->GetHomeDir().ToStdString());
838 path /= "monitor";
839 path += (m_format == Format::kDefault ? ".log" : ".csv");
840 return path;
841}
842
843std::string DataLogger::GetFileDlgTypes() {
844 if (m_format == Format::kDefault)
845 return _("Log file (*.log)|*.log");
846 else
847 return _("Spreadsheet csv file(*.csv)|*.csv");
848}
849
850void DataLogger::Add(const Logline& ll) {
851 if (!m_is_logging || !ll.navmsg) return;
852 if (m_format == Format::kVdr && ll.navmsg->to_vdr().empty()) return;
853 if (m_format == DataLogger::Format::kVdr)
854 AddVdrLogline(ll, m_stream);
855 else
856 AddStdLogline(ll, m_stream,
857 m_format == DataLogger::Format::kCsv ? '|' : ' ',
858 m_log_start);
859}
860
861DataMonitor::DataMonitor(wxWindow* parent)
862 : wxFrame(parent, wxID_ANY, _("Data Monitor"), wxPoint(0, 0), wxDefaultSize,
863 wxDEFAULT_FRAME_STYLE | wxFRAME_FLOAT_ON_PARENT,
864 kDataMonitorWindowName),
865 m_monitor_src([&](const std::shared_ptr<const NavMsg>& navmsg) {
866 auto msg = std::dynamic_pointer_cast<const Nmea0183Msg>(navmsg);
867 TtyPanel::AddIfExists(navmsg);
868 }),
869 m_quick_filter(nullptr),
870 m_logger(parent) {
871 auto vbox = new wxBoxSizer(wxVERTICAL);
872 auto tty_panel = new TtyPanel(this, 12);
873 vbox->Add(tty_panel, wxSizerFlags(1).Expand().Border());
874 vbox->Add(new wxStaticLine(this), wxSizerFlags().Expand().Border());
875
876 auto on_quick_filter_evt = [&, tty_panel] {
877 auto* quick_filter = dynamic_cast<QuickFilterPanel*>(m_quick_filter);
878 assert(quick_filter);
879 std::string value = quick_filter->GetValue();
880 tty_panel->SetQuickFilter(value);
881 };
882 m_quick_filter = new QuickFilterPanel(this, on_quick_filter_evt);
883 vbox->Add(m_quick_filter, wxSizerFlags());
884
885 auto on_stop = [&, tty_panel](bool stop) { tty_panel->OnStop(stop); };
886 auto on_close = [&, this]() { this->OnHide(); };
887 auto status_line = new StatusLine(this, m_quick_filter, tty_panel, on_stop,
888 on_close, m_logger);
889 vbox->Add(status_line, wxSizerFlags().Expand());
890 SetSizer(vbox);
891 Fit();
892 Hide();
893
894 m_quick_filter->Bind(wxEVT_TEXT, [&, tty_panel](wxCommandEvent&) {
895 tty_panel->SetQuickFilter(GetLabel().ToStdString());
896 });
897 m_quick_filter->Hide();
898 tty_panel->SetOnRightClick(
899 [&, status_line] { status_line->OnContextClick(); });
900
901 Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent& ev) { Hide(); });
902 Bind(wxEVT_RIGHT_UP, [status_line](wxMouseEvent& ev) {
903 status_line->OnContextClick();
904 ev.Skip();
905 });
906 m_filter_list_lstnr.Init(FilterEvents::GetInstance().filter_list_change,
907 [&](ObservedEvt&) { OnFilterListChange(); });
908 m_filter_update_lstnr.Init(
909 FilterEvents::GetInstance().filter_update,
910 [&](ObservedEvt& ev) { OnFilterUpdate(ev.GetString().ToStdString()); });
911
912 m_filter_apply_lstnr.Init(
913 FilterEvents::GetInstance().filter_apply,
914 [&](ObservedEvt& ev) { OnFilterApply(ev.GetString().ToStdString()); });
915}
916
917void DataMonitor::Add(const Logline& ll) {
919 m_logger.Add(ll);
920}
921
923 wxWindow* w = wxWindow::FindWindowByName("TtyPanel");
924 assert(w && "No TtyPanel found");
925 return w->IsShownOnScreen();
926}
927
928void DataMonitor::OnFilterListChange() {
929 wxWindow* w = wxWindow::FindWindowByName(kFilterChoiceName);
930 if (!w) return;
931 auto filter_choice = dynamic_cast<FilterChoice*>(w);
932 assert(filter_choice && "Wrong FilterChoice type (!)");
933 filter_choice->OnFilterListChange();
934}
935
936void DataMonitor::OnFilterUpdate(const std::string& name) {
937 if (name != m_current_filter) return;
938 wxWindow* w = wxWindow::FindWindowByName("TtyScroll");
939 if (!w) return;
940 auto tty_scroll = dynamic_cast<TtyScroll*>(w);
941 assert(tty_scroll && "Wrong TtyScroll type (!)");
942 tty_scroll->SetFilter(filters_on_disk::Read(name));
943}
944
945void DataMonitor::OnFilterApply(const std::string& name) {
946 wxWindow* w = wxWindow::FindWindowByName(kFilterChoiceName);
947 if (!w) return;
948 auto filter_choice = dynamic_cast<FilterChoice*>(w);
949 assert(filter_choice && "Wrong FilterChoice type (!)");
950 m_current_filter = name;
951 filter_choice->OnApply(name);
952}
953
954void DataMonitor::OnHide() { Hide(); }
955
956#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.