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