12#include <wx/filedlg.h>
15#include <wx/platinfo.h>
17#include <wx/sstream.h>
18#include <wx/statline.h>
19#include <wx/stattext.h>
20#include <wx/translation.h>
21#include <wx/wrapsizer.h>
23#ifndef ocpnUSE_wxBitmapBundle
28#include "androidUTIL.h"
33#include "model/nmea_log.h"
41#include "std_filesystem.h"
43#pragma clang diagnostic push
44#pragma ide diagnostic ignored "UnreachableCode"
48#if wxCHECK_VERSION(3, 2, 0)
49#define _(s) wxGetTranslation(wxASCII_STR(s)).ToStdString()
51#define _(s) wxGetTranslation((s)).ToStdString()
54static const char*
const kFilterChoiceName =
"FilterChoiceWindow";
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"}};
64static bool IsUserFilter(
const std::string& filter_name) {
66 auto found = std::find(filters.begin(), filters.end(), filter_name);
67 if (found != filters.end())
return true;
68 for (
auto& f : filters) {
70 if (nf.m_description == filter_name)
return true;
80static std::string VdrQuote(
const std::string& arg) {
81 auto static const npos = std::string::npos;
82 if (arg.find(
',') == npos && arg.find(
'"') == npos)
return arg;
90 return "\"" + s +
"\"";
97static void AddVdrLogline(
const Logline& ll, std::ostream& stream) {
98 if (kSourceByBus.find(ll.navmsg->bus) == kSourceByBus.end())
return;
100 using namespace std::chrono;
101 auto now = steady_clock::now();
102 auto ms = duration_cast<milliseconds>(now.time_since_epoch()).count();
105 stream << kSourceByBus.at(ll.navmsg->bus) <<
",";
106 stream << ll.navmsg->source->iface <<
",";
107 switch (ll.navmsg->bus) {
108 case NavAddr::Bus::N0183: {
109 auto msg0183 = std::dynamic_pointer_cast<const Nmea0183Msg>(ll.navmsg);
110 stream << msg0183->talker << msg0183->type <<
",";
112 case NavAddr::Bus::N2000: {
113 auto msg2000 = std::dynamic_pointer_cast<const Nmea2000Msg>(ll.navmsg);
114 stream << msg2000->PGN.to_string() <<
",";
116 case NavAddr::Bus::Signalk: {
117 auto msgSignalK = std::dynamic_pointer_cast<const SignalkMsg>(ll.navmsg);
118 stream <<
"\"" << msgSignalK->context_self <<
"\",";
121 assert(
false &&
"Illegal message type");
123 stream << VdrQuote(ll.navmsg->to_vdr()) <<
"\n";
127static void AddStdLogline(
const Logline& ll, std::ostream& stream,
char fs) {
130 ws << wxDateTime::Now().FormatISOTime() << fs;
134 if (ll.state.direction == NavmsgStatus::Direction::kOutput)
135 ws << kUtfRightArrow << fs;
136 else if (ll.state.direction == NavmsgStatus::Direction::kInput)
137 ws << kUtfLeftwardsArrowToBar << fs;
138 else if (ll.state.direction == NavmsgStatus::Direction::kInternal)
139 ws << kUtfLeftRightArrow << fs;
141 ws << kUtfLeftArrow << fs;
142 if (ll.state.status != NavmsgStatus::State::kOk)
143 ws << kUtfMultiplicationX << fs;
144 else if (ll.state.accepted == NavmsgStatus::Accepted::kFilteredNoOutput)
145 ws << kUtfFallingDiagonal << fs;
146 else if (ll.state.accepted == NavmsgStatus::Accepted::kFilteredDropped)
147 ws << kUtfCircledDivisionSlash << fs;
149 ws << kUtfCheckMark << fs;
151 ws << (ll.navmsg ? ll.navmsg->source->iface :
".") << fs;
152 ws << (ll.navmsg ? NavAddr::BusToString(ll.navmsg->bus) :
"-") << fs;
153 if (ll.state.status != NavmsgStatus::State::kOk)
154 ws << (ll.error_msg.size() > 0 ? ll.error_msg :
"Unknown errror");
157 ws << fs << ll.message <<
"\n";
164 TtyPanel(wxWindow* parent,
size_t lines)
165 : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize,
166 wxTAB_TRAVERSAL,
"TtyPanel"),
167 m_tty_scroll(
nullptr),
168 m_filter(
this, wxID_ANY),
170 m_on_right_click([] {}) {
171 auto vbox =
new wxBoxSizer(wxVERTICAL);
172 m_tty_scroll =
new TtyScroll(
this, m_lines);
173 m_tty_scroll->Bind(wxEVT_RIGHT_UP,
174 [&](wxMouseEvent&) { m_on_right_click(); });
175 vbox->
Add(m_tty_scroll, wxSizerFlags(1).Expand().Border());
183 bool IsActive()
const override {
return IsShownOnScreen(); }
185 void OnStop(
bool stop) {
186 m_tty_scroll->
Pause(stop);
188 m_tty_scroll->ShowScrollbars(wxSHOW_SB_DEFAULT, wxSHOW_SB_DEFAULT);
190 m_tty_scroll->ShowScrollbars(wxSHOW_SB_NEVER, wxSHOW_SB_NEVER);
195 void SetQuickFilter(
const std::string& filter) {
199 void SetOnRightClick(std::function<
void()> f) {
200 m_on_right_click = std::move(f);
205 auto window = wxWindow::FindWindowByName(
"TtyPanel");
207 auto tty_panel =
dynamic_cast<TtyPanel*
>(window);
208 if (tty_panel) tty_panel->
Add(ll);
212 wxSize DoGetBestClientSize()
const override {
213 return wxSize(-1, m_lines * GetCharHeight());
220 std::function<void()> m_on_right_click;
227 m_text_ctrl(
new wxTextCtrl(
this, wxID_ANY)),
228 m_on_text_evt(std::move(on_text_evt)) {
229 auto hbox =
new wxBoxSizer(wxHORIZONTAL);
230 auto flags = wxSizerFlags(0).Border();
231 auto label_box =
new wxBoxSizer(wxVERTICAL);
232 label_box->Add(
new wxStaticText(
this, wxID_ANY, _(
"Quick filter:")));
233 hbox->Add(label_box, flags.Align(wxALIGN_CENTER_VERTICAL));
234 hbox->Add(m_text_ctrl, flags);
238 m_text_ctrl->Bind(wxEVT_TEXT, [&](wxCommandEvent&) { m_on_text_evt(); });
241 bool Show(
bool show =
true)
override {
242 if (!show) m_text_ctrl->SetValue(
"");
243 return wxWindow::Show(show);
246 std::string GetValue() {
return m_text_ctrl->GetValue().ToStdString(); }
249 wxTextCtrl* m_text_ctrl;
250 std::function<void()> m_on_text_evt;
257 : wxButton(parent, wxID_ANY), is_logging(
true), m_logger(logger) {
258 Bind(wxEVT_BUTTON, [&](wxCommandEvent&) { OnClick(); });
265 void UpdateTooltip() {
266 if (!IsThisEnabled())
267 SetToolTip(_(
"Set log file using menu to enable"));
269 SetToolTip(_(
"Click to stop logging"));
271 SetToolTip(_(
"Click to start logging"));
274 bool Enable(
bool enable)
override {
275 bool result = wxWindow::Enable(enable);
285 is_logging = !is_logging;
286 SetLabel(is_logging ? _(
"Stop") : _(
"Start"));
288 m_logger.SetLogging(is_logging);
296 : wxChoice(parent, wxID_ANY), m_tty_panel(tty_panel) {
297 SetName(kFilterChoiceName);
298 Bind(wxEVT_CHOICE, [&](wxCommandEvent&) { OnChoice(); });
299 OnFilterListChange();
300 int ix = FindString(
"Default settings");
301 if (ix != wxNOT_FOUND) SetSelection(ix);
302 NavmsgFilter filter = filters_on_disk::Read(
"default.filter");
303 m_tty_panel->SetFilter(filter);
306 void OnFilterListChange() {
308 int select_ix = GetSelection();
309 std::string selected;
310 if (select_ix != wxNOT_FOUND) selected = GetString(select_ix).ToStdString();
312 for (
auto& filter : m_filters) {
314 Append(kLabels.at(filter.m_name));
315 }
catch (std::out_of_range&) {
316 if (filter.m_description.empty())
317 Append(filter.m_name);
319 Append(filter.m_description);
322 if (!selected.empty()) {
323 int ix = FindString(selected);
324 SetSelection(ix == wxNOT_FOUND ? 0 : ix);
328 void OnFilterUpdate(
const std::string& name) {
330 int select_ix = GetSelection();
331 if (select_ix == wxNOT_FOUND)
return;
333 std::string selected = GetString(select_ix).ToStdString();
334 if (selected != name)
return;
337 m_tty_panel->SetFilter(filter);
340 void OnApply(
const std::string& name) {
341 int found = FindString(name);
342 if (found == wxNOT_FOUND) {
343 for (
auto& filter : m_filters) {
344 if (filter.m_name == name) {
345 found = FindString(filter.m_description);
350 if (found == wxNOT_FOUND)
return;
353 OnFilterUpdate(name);
359 const std::unordered_map<std::string, std::string> kLabels = {
360 {
"all-data", _(
"All data")},
361 {
"all-nmea", _(
"All NMEA data")},
362 {
"malformed", _(
"Malformed messages")},
363 {
"nmea-input", _(
"NMEA input data")},
364 {
"nmea-output", _(
"NMEA output data")},
365 {
"plugins", _(
"Messages to plugins")},
368 std::vector<NavmsgFilter> m_filters;
372 wxString label = GetString(GetSelection());
373 NavmsgFilter filter = FilterByLabel(label.ToStdString());
374 m_tty_panel->SetFilter(filter);
379 for (
const auto& kv : kLabels) {
380 if (kv.second == label) {
386 for (
auto& f : m_filters)
387 if (f.m_name == name)
return f;
389 for (
auto& f : m_filters)
390 if (f.m_description == label)
return f;
400 : wxButton(parent, wxID_ANY),
402 m_on_stop(std::move(on_stop)) {
403 Bind(wxEVT_BUTTON, [&](wxCommandEvent&) { OnClick(); });
409 std::function<void(
bool)> m_on_stop;
412 is_paused = !is_paused;
413 m_on_stop(is_paused);
414 SetLabel(is_paused ? _(
"Resume") : _(
"Pause"));
421 enum class Id :
char {
438 wxWindow* log_button)
440 m_log_button(log_button),
442 m_log_label(log_label) {
443 auto filters =
new wxMenu(
"");
444 AppendId(filters, Id::kNewFilter, _(
"Create new..."));
445 AppendId(filters, Id::kEditFilter, _(
"Edit..."));
446 AppendId(filters, Id::kDeleteFilter, _(
"Delete..."));
447 AppendSubMenu(filters, _(
"Filters..."));
448 if (IsUserFilter(m_filter))
449 Append(
static_cast<int>(Id::kEditActiveFilter), _(
"Edit active filter"));
450 auto logging =
new wxMenu(
"");
451 AppendId(logging, Id::kLogFile, _(
"Log file..."));
452 AppendRadioId(logging, Id::kLogFormatDefault, _(
"Log format: standard"));
453 AppendRadioId(logging, Id::kLogFormatCsv, _(
"Log format: CSV"));
454 AppendRadioId(logging, Id::kLogFormatVdr, _(
"Log format: VDR"));
455 AppendSubMenu(logging, _(
"Logging..."));
457 auto view =
new wxMenu(
"");
458 AppendCheckId(view, Id::kViewStdColors, _(
"Use colors"));
459 AppendId(view, Id::kViewCopy, _(
"Copy messages to clipboard"));
460 AppendSubMenu(view, _(
"View..."));
462 Bind(wxEVT_MENU, [&](wxCommandEvent& ev) {
463 switch (
static_cast<Id
>(ev.GetId())) {
464 case Id::kLogFormatDefault:
465 SetLogFormat(DataLogger::Format::kDefault, _(
"Log format: default"));
468 case Id::kLogFormatVdr:
469 SetLogFormat(DataLogger::Format::kVdr, _(
"Log format: VDR"));
472 case Id::kLogFormatCsv:
473 SetLogFormat(DataLogger::Format::kCsv, _(
"Log format: csv"));
480 case Id::kViewStdColors:
481 SetColor(static_cast<int>(Id::kViewStdColors));
489 CreateFilterDlg(parent);
492 case Id::kEditFilter:
493 EditFilterDlg(wxTheApp->GetTopWindow());
496 case Id::kEditActiveFilter:
497 EditOneFilterDlg(wxTheApp->GetTopWindow(), m_filter);
500 case Id::kDeleteFilter:
501 RemoveFilterDlg(parent);
505 std::cout <<
"Menu id: " << ev.GetId() <<
"\n";
511 void SetFilterName(
const std::string& filter) {
512 int id =
static_cast<int>(Id::kEditActiveFilter);
513 if (FindItem(
id)) Delete(
id);
514 if (IsUserFilter(filter)) Append(
id, _(
"Edit active filter"));
520 wxWindow* m_log_button;
522 wxStaticText* m_log_label;
523 std::string m_filter;
525 wxMenuItem* AppendId(wxMenu* root, Id
id,
const wxString& label) {
526 return root->Append(
static_cast<int>(
id), label);
529 void AppendRadioId(wxMenu* root, Id
id,
const wxString& label) {
530 root->AppendRadioItem(
static_cast<int>(
id), label);
533 void AppendCheckId(wxMenu* root, Id
id,
const wxString& label) {
534 root->AppendCheckItem(
static_cast<int>(
id), label);
537 void SetLogFormat(DataLogger::Format format,
const std::string& label) {
538 m_log_label->SetLabel(label);
539 m_logger.SetFormat(format);
540 m_log_button->Disable();
545 wxFileDialog dlg(m_parent, _(
"Select logfile"),
"",
546 m_logger.GetLogfile().string(), _(
"Log Files (*.log)"),
547 wxFD_SAVE | wxFD_OVERWRITE_PROMPT);
548 if (dlg.ShowModal() == wxID_CANCEL)
return;
549 m_logger.SetLogfile(fs::path(dlg.GetPath().ToStdString()));
550 m_log_button->Enable();
553 void SetColor(
int id) {
554 auto* w = wxWindow::FindWindowByName(
"TtyScroll");
555 auto tty_scroll =
dynamic_cast<TtyScroll*
>(w);
556 if (!tty_scroll)
return;
558 wxMenuItem* item = FindItem(
id);
561 if (item->IsCheck() && item->IsChecked())
562 tty_scroll->SetColors(std::make_unique<StdColorsByState>());
564 tty_scroll->SetColors(
565 std::make_unique<NoColorsByState>(tty_scroll->GetForegroundColour()));
568 void CopyToClipboard() {
570 dynamic_cast<TtyScroll*
>(wxWindow::FindWindowByName(
"TtyScroll"));
571 if (!tty_scroll)
return;
580 : wxButton(parent, wxID_ANY, wxEmptyString, wxDefaultPosition,
581 wxDefaultSize, wxBU_EXACTFIT | wxBU_BOTTOM),
582 m_quick_filter(quick_filter),
583 m_show_filter(
true) {
584 Bind(wxEVT_BUTTON, [&](wxCommandEvent&) { OnClick(); });
589 wxWindow* m_quick_filter;
593 m_show_filter = !m_show_filter;
595 strcpy(buffer, m_show_filter ? kNoFunnelSvg : kFunnelSvg);
596#ifdef ocpnUSE_wxBitmapBundle
597 auto icon_size = wxSize(2 * GetCharWidth(), GetCharHeight());
598 auto bundle = wxBitmapBundle::FromSVG(buffer, icon_size);
601 wxStringInputStream wis(buffer);
602 wxSVGDocument svg_doc(wis);
603 wxImage image = svg_doc.Render(GetCharHeight(), GetCharHeight());
604 SetBitmap(wxBitmap(image));
606 m_quick_filter->Show(m_show_filter);
607 SetToolTip(m_show_filter ? _(
"Close quick filter")
608 : _(
"Open quick filter"));
609 GetGrandParent()->Layout();
617 std::function<std::string()> get_current_filter)
618 : wxButton(parent, wxID_ANY, wxEmptyString, wxDefaultPosition,
619 wxDefaultSize, wxBU_EXACTFIT),
621 m_get_current_filter(std::move(get_current_filter)) {
622 Bind(wxEVT_BUTTON, [&](wxCommandEvent&) { OnClick(); });
623 SetLabel(kUtfIdenticalTo);
624 SetToolTip(_(
"Open menu"));
629 std::function<std::string()> m_get_current_filter;
632 m_menu.SetFilterName(m_get_current_filter());
641 std::function<
void(
bool)> on_stop,
DataLogger& logger)
644 m_log_button(
new LogButton(
this, logger)),
645 m_log_label(
new wxStaticText(
this, wxID_ANY, _(
"Logging: Default"))),
647 m_menu(
this, m_log_label, logger, m_log_button) {
649 auto log_label_box =
new wxBoxSizer(wxVERTICAL);
650 log_label_box->Add(m_log_label);
651 auto filter_label_box =
new wxBoxSizer(wxVERTICAL);
652 filter_label_box->Add(
new wxStaticText(
this, wxID_ANY, _(
"View")));
654 auto flags = wxSizerFlags(0).Border();
655 auto wbox =
new wxWrapSizer(wxHORIZONTAL);
656 wbox->Add(log_label_box, flags.Align(wxALIGN_CENTER_VERTICAL));
657 wbox->Add(m_log_button, flags);
658 wbox->Add(GetCharWidth() * 2, 0, 1);
659 wbox->Add(filter_label_box, flags.Align(wxALIGN_CENTER_VERTICAL));
660 wbox->Add(m_filter_choice, flags);
663 auto get_current_filter = [&] {
664 return m_filter_choice->GetStringSelection().ToStdString();
666 wbox->Add(
new MenuButton(
this, m_menu, get_current_filter), flags);
671 Bind(wxEVT_SIZE, [&](wxSizeEvent& ev) {
675 Bind(wxEVT_RIGHT_UP, [&](wxMouseEvent& ev) {
676 m_menu.SetFilterName(m_filter_choice->GetStringSelection().ToStdString());
681 void OnContextClick() {
682 m_menu.SetFilterName(m_filter_choice->GetStringSelection().ToStdString());
689 wxSize DoGetBestClientSize()
const override {
691 return wxSize(-1, -1);
693 return wxSize(85 * GetCharWidth(), 2.5 * GetCharHeight());
698 wxButton* m_log_button;
699 wxStaticText* m_log_label;
700 wxChoice* m_filter_choice;
704DataLogger::DataLogger(wxWindow* parent,
const fs::path& path)
707 m_stream(path, std::ios_base::app),
709 m_format(Format::kDefault) {}
711DataLogger::DataLogger(wxWindow* parent)
714void DataLogger::SetLogging(
bool logging) { m_is_logging = logging; }
716void DataLogger::SetLogfile(
const fs::path& path) {
717 m_stream = std::ofstream(path);
718 m_stream <<
"# timestamp_format: EPOCH_MILLIS\n";
719 m_stream <<
"received_at,protocol,msg_type,source,raw_data\n";
720 m_stream << std::flush;
723void DataLogger::SetFormat(DataLogger::Format format) { m_format = format; }
725fs::path DataLogger::DefaultLogfile() {
726 if (wxPlatformInfo::Get().GetOperatingSystemId() & wxOS_WINDOWS)
732void DataLogger::Add(
const Logline& ll) {
733 if (!m_is_logging || !ll.navmsg)
return;
734 if (m_format == Format::kVdr && ll.navmsg->to_vdr().empty())
return;
735 if (m_format == DataLogger::Format::kVdr)
736 AddVdrLogline(ll, m_stream);
738 AddStdLogline(ll, m_stream,
739 m_format == DataLogger::Format::kCsv ?
'|' :
' ');
742DataMonitor::DataMonitor(wxWindow* parent)
743 : wxFrame(parent, wxID_ANY,
"Data Monitor", wxDefaultPosition,
744 wxDefaultSize, wxDEFAULT_FRAME_STYLE, kDataMonitorWindowName),
745 m_monitor_src([&](const std::shared_ptr<const
NavMsg>& navmsg) {
746 auto msg = std::dynamic_pointer_cast<const Nmea0183Msg>(navmsg);
749 m_quick_filter(
nullptr),
751 auto vbox =
new wxBoxSizer(wxVERTICAL);
752 auto tty_panel =
new TtyPanel(
this, 12);
753 vbox->Add(tty_panel, wxSizerFlags(1).Expand().Border());
754 vbox->Add(
new wxStaticLine(
this), wxSizerFlags().Expand().Border());
756 auto on_quick_filter_evt = [&, tty_panel] {
758 assert(quick_filter);
759 std::string value = quick_filter->GetValue();
760 tty_panel->SetQuickFilter(value);
763 vbox->Add(m_quick_filter, wxSizerFlags());
765 auto on_stop = [&, tty_panel](
bool stop) { tty_panel->OnStop(stop); };
767 new StatusLine(
this, m_quick_filter, tty_panel, on_stop, m_logger);
768 vbox->Add(status_line, wxSizerFlags().Expand());
773 m_quick_filter->Bind(wxEVT_TEXT, [&, tty_panel](wxCommandEvent&) {
774 tty_panel->SetQuickFilter(GetLabel().ToStdString());
776 m_quick_filter->Hide();
777 tty_panel->SetOnRightClick(
778 [&, status_line] { status_line->OnContextClick(); });
780 Bind(wxEVT_CLOSE_WINDOW, [
this](wxCloseEvent& ev) { Hide(); });
781 Bind(wxEVT_RIGHT_UP, [status_line](wxMouseEvent& ev) {
782 status_line->OnContextClick();
785 m_filter_list_lstnr.Init(FilterEvents::GetInstance().filter_list_change,
787 m_filter_update_lstnr.Init(
788 FilterEvents::GetInstance().filter_update,
789 [&](
ObservedEvt& ev) { OnFilterUpdate(ev.GetString().ToStdString()); });
791 m_filter_apply_lstnr.Init(
792 FilterEvents::GetInstance().filter_apply,
793 [&](
ObservedEvt& ev) { OnFilterApply(ev.GetString().ToStdString()); });
802 wxWindow* w = wxWindow::FindWindowByName(
"TtyPanel");
803 assert(w &&
"No TtyPanel found");
804 return w->IsShownOnScreen();
807void DataMonitor::OnFilterListChange() {
808 wxWindow* w = wxWindow::FindWindowByName(kFilterChoiceName);
811 assert(filter_choice &&
"Wrong FilterChoice type (!)");
812 filter_choice->OnFilterListChange();
815void DataMonitor::OnFilterUpdate(
const std::string& name) {
816 wxWindow* w = wxWindow::FindWindowByName(
"TtyScroll");
818 auto tty_scroll =
dynamic_cast<TtyScroll*
>(w);
819 assert(tty_scroll &&
"Wrong TtyScroll type (!)");
820 tty_scroll->SetFilter(filters_on_disk::Read(name));
823void DataMonitor::OnFilterApply(
const std::string& name) {
824 wxWindow* w = wxWindow::FindWindowByName(kFilterChoiceName);
827 assert(filter_choice &&
"Wrong FilterChoice type (!)");
828 filter_choice->OnApply(name);
831#pragma clang diagnostic pop
bool IsActive() const override
Return true if log is visible i.
void Add(std::string msg)
Add an input line to log output.
Offer user to select current filter.
Actual data sent between application and transport layer.
static std::vector< NavmsgFilter > GetAllFilters()
Return list of all filters, system + user defined.
Custom event class for OpenCPN's notification system.
Overall bottom status line.
Main window, a rolling log of messages.
void Add(const Logline &ll) override
Add an formatted string to log output.
static void AddIfExists(const Logline &ll)
Invoke Add(s) for possibly existing instance.
bool IsActive() const override
Return true if log is visible i.
New NMEA Debugger successor main window.
Provide a data stream of input messages for the Data Monitor.
Dialogs handing user defined filters.
std::vector< std::string > List(bool include_system)
Return list of filters, possibly including also the system ones.
NavmsgFilter Read(const std::string &name)
Read filter with given name from disk.
Hooks into gui available in model.
Data monitor filter definitions.