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