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