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