enode-host 0.1.0__py3-none-any.whl → 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
enode_host/view.py CHANGED
@@ -13,6 +13,7 @@
13
13
  ## PLEASE DO *NOT* EDIT THIS FILE!
14
14
  ###########################################################################
15
15
 
16
+ import os
16
17
  import wx
17
18
  import wx.xrc
18
19
  import wx.grid
@@ -28,9 +29,20 @@ from matplotlib.backends.backend_wxagg import \
28
29
  NavigationToolbar2WxAgg as NavigationToolbar
29
30
  from matplotlib.figure import Figure
30
31
  import matplotlib.dates as mdates
32
+ import numpy as np
31
33
  import wx.grid as gridlib
34
+ def _env_truthy(name: str) -> bool:
35
+ value = os.environ.get(name)
36
+ if not value:
37
+ return False
38
+ return value.strip().lower() in {"1", "true", "yes", "y", "on"}
39
+
40
+
32
41
  try:
33
- import wx.html2 as webview
42
+ if _env_truthy("ENODE_DISABLE_WEBVIEW"):
43
+ webview = None
44
+ else:
45
+ import wx.html2 as webview
34
46
  except ImportError:
35
47
  webview = None
36
48
  try:
@@ -52,6 +64,7 @@ try:
52
64
  PPS_AGE_LIMIT_SEC,
53
65
  PPS_AGE_UPDATE_MS,
54
66
  PPS_COL,
67
+ BAT_COL,
55
68
  )
56
69
  except ImportError:
57
70
  from constants import (
@@ -62,6 +75,7 @@ except ImportError:
62
75
  PPS_AGE_LIMIT_SEC,
63
76
  PPS_AGE_UPDATE_MS,
64
77
  PPS_COL,
78
+ BAT_COL,
65
79
  )
66
80
  import pandas as pd
67
81
 
@@ -147,6 +161,8 @@ class TableUpdater:
147
161
  self.grid.SetColSize(self.col_index[RSSI_COL], 40)
148
162
  self.grid.SetColSize(self.col_index["Children"], 70)
149
163
  self.grid.SetColSize(self.col_index[PPS_COL], 40)
164
+ if BAT_COL in self.col_index:
165
+ self.grid.SetColSize(self.col_index[BAT_COL], 50)
150
166
  self.grid.SetColSize(self.col_index["CMD"], 200)
151
167
  for hidden in [
152
168
  "PPS-time",
@@ -226,6 +242,7 @@ class PlotUpdater:
226
242
  self.timehistory_lines = {}
227
243
  self.psd_lines = {}
228
244
  self._psd_legend_labels = {}
245
+ self.psd_auto_x = True
229
246
 
230
247
  def init_plot(self):
231
248
  self.timehistory_lines = {}
@@ -247,7 +264,8 @@ class PlotUpdater:
247
264
  self.axes1.set_xlabel('Time')
248
265
  self.axes1.set_ylabel('Acceleration (g)')
249
266
  self.axes1.grid(True)
250
- self.axes1.set_ylim(-2, 2)
267
+ if not self.view.time_y_auto:
268
+ self.axes1.set_ylim(*self.view.time_y_limits)
251
269
  self.axes1.tick_params(axis='x', labelbottom=True, pad=2)
252
270
  self.axes1.legend()
253
271
  self.view.m_fig.figure.autofmt_xdate(rotation=0, ha='right')
@@ -265,11 +283,16 @@ class PlotUpdater:
265
283
  self.axes2.set_xlabel('Frequency (Hz)')
266
284
  self.axes2.set_ylabel('ASD (g/sq(Hz))')
267
285
  self.axes2.grid(True)
268
- self.axes2.set_ylim(1e-6, 1e1)
286
+ if not self.view.psd_y_auto:
287
+ self.axes2.set_ylim(*self.view.psd_y_limits)
269
288
  self.axes2.set_xlim(0, 25)
289
+ self.psd_auto_x = True
290
+ self.axes2.callbacks.connect('xlim_changed', self._on_psd_xlim_changed)
270
291
  self.axes2.legend()
271
292
 
272
293
  def figure_update(self):
294
+ min_time_y = None
295
+ max_time_y = None
273
296
  for _, row in self.model.mesh_status_data.iterrows():
274
297
  node_id = row['nodeID']
275
298
  for idx, ch in enumerate(['X', 'Y', 'Z']):
@@ -278,30 +301,74 @@ class PlotUpdater:
278
301
  with self.model.plot_mutex[node_id]:
279
302
  if len(self.model.timehistory_xdata[node_id]) > 1:
280
303
  self.timehistory_lines[node_id_ch].set_xdata(self.model.timehistory_xdata[node_id])
281
- self.timehistory_lines[node_id_ch].set_ydata(self.model.timehistory_ydata[node_id][:, idx])
304
+ series = self.model.timehistory_ydata[node_id][:, idx]
305
+ self.timehistory_lines[node_id_ch].set_ydata(series)
306
+ if self.view.time_y_auto and series is not None and len(series) > 0:
307
+ finite = series[np.isfinite(series)]
308
+ if finite.size:
309
+ smin = float(finite.min())
310
+ smax = float(finite.max())
311
+ min_time_y = smin if min_time_y is None else min(min_time_y, smin)
312
+ max_time_y = smax if max_time_y is None else max(max_time_y, smax)
282
313
  if self.model.timehistory_xlim:
283
314
  self.axes1.set_xlim(self.model.timehistory_xlim)
315
+ if self.view.time_y_auto and min_time_y is not None and max_time_y is not None:
316
+ if min_time_y == max_time_y:
317
+ pad = 1.0 if min_time_y == 0 else abs(min_time_y) * 0.1
318
+ else:
319
+ pad = (max_time_y - min_time_y) * 0.05
320
+ self.axes1.set_ylim(min_time_y - pad, max_time_y + pad)
284
321
  try:
285
322
  self.view.m_fig.figure.canvas.draw()
286
323
  except Exception:
287
324
  raise
288
325
 
326
+ max_f = None
327
+ min_psd_y = None
328
+ max_psd_y = None
289
329
  for _, row in self.model.mesh_status_data.iterrows():
290
330
  node_id = row['nodeID']
331
+ f = self.model.psd_xdata.get(node_id)
291
332
  for idx, ch in enumerate(['X', 'Y', 'Z']):
292
333
  node_id_ch = row['Node ID'] + ch
293
334
  if node_id_ch in self.psd_lines and node_id in self.model.psd_ydata:
294
335
  with self.model.plot_mutex[node_id]:
295
- self.psd_lines[node_id_ch].set_xdata(self.model.psd_xdata)
296
- self.psd_lines[node_id_ch].set_ydata(self.model.psd_ydata[node_id][:, idx])
297
-
298
- if len(self.model.psd_xdata) > 0:
299
- self.axes2.set_xlim(0, round(self.model.psd_xdata[-1]))
336
+ if f is not None and len(f) > 0:
337
+ self.psd_lines[node_id_ch].set_xdata(f)
338
+ last_f = f[-1]
339
+ if max_f is None or last_f > max_f:
340
+ max_f = last_f
341
+ series = self.model.psd_ydata[node_id][:, idx]
342
+ self.psd_lines[node_id_ch].set_ydata(series)
343
+ if self.view.psd_y_auto and series is not None and len(series) > 0:
344
+ finite = series[np.isfinite(series)]
345
+ finite = finite[finite > 0]
346
+ if finite.size:
347
+ smin = float(finite.min())
348
+ smax = float(finite.max())
349
+ min_psd_y = smin if min_psd_y is None else min(min_psd_y, smin)
350
+ max_psd_y = smax if max_psd_y is None else max(max_psd_y, smax)
351
+
352
+ if self.psd_auto_x and max_f is not None:
353
+ self.axes2.set_xlim(0, round(max_f))
354
+ if self.view.psd_y_auto and min_psd_y is not None and max_psd_y is not None:
355
+ low = min_psd_y * 0.8
356
+ high = max_psd_y * 1.2
357
+ if low <= 0:
358
+ low = min_psd_y * 0.5
359
+ if low > 0 and high > low:
360
+ self.axes2.set_ylim(low, high)
300
361
  try:
301
362
  self.view.m_fig.figure.canvas.draw()
302
363
  except Exception:
303
364
  raise
304
365
 
366
+ def _on_psd_xlim_changed(self, _axes):
367
+ # Disable auto-scaling after user zoom/pan.
368
+ if not self.psd_auto_x:
369
+ return
370
+ self.psd_auto_x = False
371
+
305
372
  def update_psd_legend(self):
306
373
  if not self.psd_lines:
307
374
  return
@@ -348,6 +415,7 @@ class MergedPlotUpdater:
348
415
  self.axes2 = view.axes2_merged
349
416
  self.timehistory_lines = {}
350
417
  self.psd_lines = {}
418
+ self.psd_auto_x = True
351
419
 
352
420
  def _node_label(self, node_id):
353
421
  return str(node_id).upper()
@@ -370,7 +438,8 @@ class MergedPlotUpdater:
370
438
  self.axes1.set_xlabel('Time')
371
439
  self.axes1.set_ylabel('Acceleration (g)')
372
440
  self.axes1.grid(True)
373
- self.axes1.set_ylim(-2, 2)
441
+ if not self.view.time_y_auto:
442
+ self.axes1.set_ylim(*self.view.time_y_limits)
374
443
  self.axes1.tick_params(axis='x', labelbottom=True, pad=2)
375
444
  self.axes1.legend()
376
445
  self.view.m_fig_merged.figure.autofmt_xdate(rotation=0, ha='right')
@@ -387,11 +456,16 @@ class MergedPlotUpdater:
387
456
  self.axes2.set_xlabel('Frequency (Hz)')
388
457
  self.axes2.set_ylabel('ASD (g/sq(Hz))')
389
458
  self.axes2.grid(True)
390
- self.axes2.set_ylim(1e-6, 1e1)
459
+ if not self.view.psd_y_auto:
460
+ self.axes2.set_ylim(*self.view.psd_y_limits)
391
461
  self.axes2.set_xlim(0, 25)
462
+ self.psd_auto_x = True
463
+ self.axes2.callbacks.connect('xlim_changed', self._on_psd_xlim_changed)
392
464
  self.axes2.legend()
393
465
 
394
466
  def figure_update(self):
467
+ min_time_y = None
468
+ max_time_y = None
395
469
  for node_id in self.model.merged_node_ids:
396
470
  node_label = self._node_label(node_id)
397
471
  xdata = self.model.merged_timehistory_xdata.get(node_id)
@@ -403,14 +477,30 @@ class MergedPlotUpdater:
403
477
  line = self.timehistory_lines.get(label)
404
478
  if line is not None:
405
479
  line.set_xdata(xdata)
406
- line.set_ydata(ydata[:, idx])
480
+ series = ydata[:, idx]
481
+ line.set_ydata(series)
482
+ if self.view.time_y_auto and series is not None and len(series) > 0:
483
+ finite = series[np.isfinite(series)]
484
+ if finite.size:
485
+ smin = float(finite.min())
486
+ smax = float(finite.max())
487
+ min_time_y = smin if min_time_y is None else min(min_time_y, smin)
488
+ max_time_y = smax if max_time_y is None else max(max_time_y, smax)
407
489
  if self.model.merged_timehistory_xlim:
408
490
  self.axes1.set_xlim(self.model.merged_timehistory_xlim)
491
+ if self.view.time_y_auto and min_time_y is not None and max_time_y is not None:
492
+ if min_time_y == max_time_y:
493
+ pad = 1.0 if min_time_y == 0 else abs(min_time_y) * 0.1
494
+ else:
495
+ pad = (max_time_y - min_time_y) * 0.05
496
+ self.axes1.set_ylim(min_time_y - pad, max_time_y + pad)
409
497
  try:
410
498
  self.view.m_fig_merged.figure.canvas.draw()
411
499
  except Exception:
412
500
  raise
413
501
 
502
+ min_psd_y = None
503
+ max_psd_y = None
414
504
  f = self.model.merged_psd_xdata
415
505
  if f is None:
416
506
  f = []
@@ -424,15 +514,37 @@ class MergedPlotUpdater:
424
514
  line = self.psd_lines.get(label)
425
515
  if line is not None:
426
516
  line.set_xdata(f)
427
- line.set_ydata(ydata[:, idx])
428
-
429
- if len(f) > 0:
517
+ series = ydata[:, idx]
518
+ line.set_ydata(series)
519
+ if self.view.psd_y_auto and series is not None and len(series) > 0:
520
+ finite = series[np.isfinite(series)]
521
+ finite = finite[finite > 0]
522
+ if finite.size:
523
+ smin = float(finite.min())
524
+ smax = float(finite.max())
525
+ min_psd_y = smin if min_psd_y is None else min(min_psd_y, smin)
526
+ max_psd_y = smax if max_psd_y is None else max(max_psd_y, smax)
527
+
528
+ if self.psd_auto_x and len(f) > 0:
430
529
  self.axes2.set_xlim(0, round(f[-1]))
530
+ if self.view.psd_y_auto and min_psd_y is not None and max_psd_y is not None:
531
+ low = min_psd_y * 0.8
532
+ high = max_psd_y * 1.2
533
+ if low <= 0:
534
+ low = min_psd_y * 0.5
535
+ if low > 0 and high > low:
536
+ self.axes2.set_ylim(low, high)
431
537
  try:
432
538
  self.view.m_fig_merged.figure.canvas.draw()
433
539
  except Exception:
434
540
  raise
435
541
 
542
+ def _on_psd_xlim_changed(self, _axes):
543
+ # Disable auto-scaling after user zoom/pan.
544
+ if not self.psd_auto_x:
545
+ return
546
+ self.psd_auto_x = False
547
+
436
548
  ###########################################################################
437
549
  ## Class View
438
550
  ###########################################################################
@@ -447,12 +559,19 @@ class View(wx.Frame):
447
559
  self.model = self.controller.model
448
560
  if self.model is not None:
449
561
  self.timespan_length = self.model.timespan_length
562
+ else:
563
+ self.timespan_length = 30
450
564
  self.m_table = None
451
565
  self.table_updater = None
452
566
  self.plot_updater = None
453
567
  self.merged_plot_updater = None
454
568
  self.PPS_outdate_check = True
455
569
  self.channel_selection = [False, True, False]
570
+ self.time_y_auto = False
571
+ self.time_y_limits = (-2.0, 2.0)
572
+ self.psd_y_auto = False
573
+ self.psd_y_limits = (1e-6, 1e1)
574
+ self._load_plot_options()
456
575
 
457
576
  self.SetSizeHints( wx.DefaultSize, wx.DefaultSize )
458
577
 
@@ -479,17 +598,14 @@ class View(wx.Frame):
479
598
  self.m_panel_status = wx.Panel(self.m_left_notebook, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL)
480
599
  self.m_panel_map = wx.Panel(self.m_left_notebook, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL)
481
600
  self.m_panel_plots = wx.Panel(self.m_left_notebook, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL)
482
- self.m_panel_merged_plots = wx.Panel(self.m_left_notebook, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL)
483
601
  self.m_left_notebook.AddPage(self.m_panel_nodes, _(u"Nodes"), False)
484
602
  self.m_left_notebook.AddPage(self.m_panel_status, _(u"Mesh"), True)
485
603
  self.m_left_notebook.AddPage(self.m_panel_map, _(u"Map"), False)
486
- self.m_left_notebook.AddPage(self.m_panel_plots, _(u"RT-Plot"), False)
487
- self.m_left_notebook.AddPage(self.m_panel_merged_plots, _(u"Merged Plot"), False)
604
+ self.m_left_notebook.AddPage(self.m_panel_plots, _(u"Plots"), False)
488
605
  self.tab_index_nodes = 0
489
606
  self.tab_index_status = 1
490
607
  self.tab_index_map = 2
491
608
  self.tab_index_rt_plots = 3
492
- self.tab_index_merged_plots = 4
493
609
 
494
610
  # Sensor Nodes definition tab
495
611
  nodes_sizer = wx.BoxSizer(wx.VERTICAL)
@@ -505,7 +621,7 @@ class View(wx.Frame):
505
621
  self.controller.model.options.get('acc_nums_txt', '[1:3]'),
506
622
  wx.DefaultPosition,
507
623
  wx.DefaultSize,
508
- 0,
624
+ wx.TE_PROCESS_ENTER,
509
625
  )
510
626
  nodes_grid.Add(self.m_textCtrl_acc, 1, wx.EXPAND)
511
627
 
@@ -517,7 +633,7 @@ class View(wx.Frame):
517
633
  self.controller.model.options.get('tmp_nums_txt', '[]'),
518
634
  wx.DefaultPosition,
519
635
  wx.DefaultSize,
520
- 0,
636
+ wx.TE_PROCESS_ENTER,
521
637
  )
522
638
  nodes_grid.Add(self.m_textCtrl_tmp, 1, wx.EXPAND)
523
639
 
@@ -529,7 +645,7 @@ class View(wx.Frame):
529
645
  self.controller.model.options.get('str_nums_txt', '[]'),
530
646
  wx.DefaultPosition,
531
647
  wx.DefaultSize,
532
- 0,
648
+ wx.TE_PROCESS_ENTER,
533
649
  )
534
650
  nodes_grid.Add(self.m_textCtrl_str, 1, wx.EXPAND)
535
651
 
@@ -541,7 +657,7 @@ class View(wx.Frame):
541
657
  self.controller.model.options.get('veh_nums_txt', '[]'),
542
658
  wx.DefaultPosition,
543
659
  wx.DefaultSize,
544
- 0,
660
+ wx.TE_PROCESS_ENTER,
545
661
  )
546
662
  nodes_grid.Add(self.m_textCtrl_veh, 1, wx.EXPAND)
547
663
 
@@ -573,6 +689,7 @@ class View(wx.Frame):
573
689
  self.m_map = webview.WebView.New(self.m_panel_map)
574
690
  map_sizer.Add(self.m_map, 1, wx.ALL | wx.EXPAND, 5)
575
691
  self.m_map_loaded = False
692
+ self._map_nodes = set()
576
693
  self.m_map.Bind(webview.EVT_WEBVIEW_LOADED, self.on_map_loaded)
577
694
  self._load_map()
578
695
  else:
@@ -586,14 +703,88 @@ class View(wx.Frame):
586
703
  self.m_panel_map.SetSizer(map_sizer)
587
704
 
588
705
  plots_sizer = wx.BoxSizer(wx.VERTICAL)
706
+
707
+ plot_controls_sizer = wx.BoxSizer(wx.HORIZONTAL)
708
+
709
+ self.m_button_clf = wx.Button(self.m_panel_plots, wx.ID_ANY, _(u"Clear Figure"), wx.DefaultPosition, wx.DefaultSize, 0)
710
+ plot_controls_sizer.Add(self.m_button_clf, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 4)
711
+
712
+ plot_controls_sizer.Add(wx.StaticText(self.m_panel_plots, wx.ID_ANY, _(u"Channels:")), 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 4)
713
+ self.m_check_chx = wx.CheckBox(self.m_panel_plots, wx.ID_ANY, _(u"X"))
714
+ self.m_check_chy = wx.CheckBox(self.m_panel_plots, wx.ID_ANY, _(u"Y"))
715
+ self.m_check_chz = wx.CheckBox(self.m_panel_plots, wx.ID_ANY, _(u"Z"))
716
+ self.m_check_chx.SetValue(self.channel_selection[0])
717
+ self.m_check_chy.SetValue(self.channel_selection[1])
718
+ self.m_check_chz.SetValue(self.channel_selection[2])
719
+ plot_controls_sizer.Add(self.m_check_chx, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 4)
720
+ plot_controls_sizer.Add(self.m_check_chy, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 4)
721
+ plot_controls_sizer.Add(self.m_check_chz, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 4)
722
+
723
+ plot_controls_sizer.Add(wx.StaticText(self.m_panel_plots, wx.ID_ANY, _(u"Timespan:")), 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 4)
724
+ self.timespan_choices = [30, 20, 10, 5, 2]
725
+ self.m_choice_timespan = wx.Choice(
726
+ self.m_panel_plots,
727
+ choices=[f"{v} sec" for v in self.timespan_choices],
728
+ )
729
+ if self.timespan_length in self.timespan_choices:
730
+ self.m_choice_timespan.SetSelection(self.timespan_choices.index(self.timespan_length))
731
+ else:
732
+ self.m_choice_timespan.SetSelection(0)
733
+ plot_controls_sizer.Add(self.m_choice_timespan, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 4)
734
+
735
+ plots_sizer.Add(plot_controls_sizer, 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, 5)
736
+
737
+ ylim_sizer = wx.FlexGridSizer(1, 6, 2, 6)
738
+ ylim_sizer.AddGrowableCol(2, 1)
739
+ ylim_sizer.AddGrowableCol(5, 1)
740
+
741
+ self.m_check_time_y_auto = wx.CheckBox(self.m_panel_plots, wx.ID_ANY, _(u"Time Y auto"))
742
+ self.m_check_time_y_auto.SetValue(self.time_y_auto)
743
+ ylim_sizer.Add(self.m_check_time_y_auto, 0, wx.ALIGN_CENTER_VERTICAL)
744
+ ylim_sizer.Add(wx.StaticText(self.m_panel_plots, wx.ID_ANY, _(u"ylim:")), 0, wx.ALIGN_CENTER_VERTICAL)
745
+ self.m_text_time_ylim = wx.TextCtrl(
746
+ self.m_panel_plots,
747
+ wx.ID_ANY,
748
+ f"[{self.time_y_limits[0]}, {self.time_y_limits[1]}]",
749
+ wx.DefaultPosition,
750
+ wx.Size(120, -1),
751
+ wx.TE_PROCESS_ENTER,
752
+ )
753
+ self.m_text_time_ylim.Enable(not self.time_y_auto)
754
+ ylim_sizer.Add(self.m_text_time_ylim, 0, wx.ALIGN_CENTER_VERTICAL)
755
+
756
+ self.m_check_psd_y_auto = wx.CheckBox(self.m_panel_plots, wx.ID_ANY, _(u"ASD Y auto"))
757
+ self.m_check_psd_y_auto.SetValue(self.psd_y_auto)
758
+ ylim_sizer.Add(self.m_check_psd_y_auto, 0, wx.ALIGN_CENTER_VERTICAL)
759
+ ylim_sizer.Add(wx.StaticText(self.m_panel_plots, wx.ID_ANY, _(u"ylim:")), 0, wx.ALIGN_CENTER_VERTICAL)
760
+ self.m_text_psd_ylim = wx.TextCtrl(
761
+ self.m_panel_plots,
762
+ wx.ID_ANY,
763
+ f"[{self.psd_y_limits[0]}, {self.psd_y_limits[1]}]",
764
+ wx.DefaultPosition,
765
+ wx.Size(120, -1),
766
+ wx.TE_PROCESS_ENTER,
767
+ )
768
+ self.m_text_psd_ylim.Enable(not self.psd_y_auto)
769
+ ylim_sizer.Add(self.m_text_psd_ylim, 0, wx.ALIGN_CENTER_VERTICAL)
770
+
771
+ ylim_row = wx.BoxSizer(wx.HORIZONTAL)
772
+ indent = self.m_button_clf.GetBestSize().width + 8
773
+ ylim_row.AddSpacer(indent)
774
+ ylim_row.Add(ylim_sizer, 0, wx.ALIGN_CENTER_VERTICAL)
775
+ plots_sizer.Add(ylim_row, 0, wx.LEFT | wx.RIGHT | wx.BOTTOM, 5)
589
776
  self.m_plot_panel = wx.Panel(self.m_panel_plots, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL)
590
777
  plots_sizer.Add(self.m_plot_panel, 1, wx.ALL | wx.EXPAND, 5)
591
778
  self.m_panel_plots.SetSizer(plots_sizer)
592
779
 
593
- merged_plots_sizer = wx.BoxSizer(wx.VERTICAL)
594
- self.m_plot_panel_merged = wx.Panel(self.m_panel_merged_plots, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL)
595
- merged_plots_sizer.Add(self.m_plot_panel_merged, 1, wx.ALL | wx.EXPAND, 5)
596
- self.m_panel_merged_plots.SetSizer(merged_plots_sizer)
780
+ self.m_check_chx.Bind(wx.EVT_CHECKBOX, self.onChannelToggled)
781
+ self.m_check_chy.Bind(wx.EVT_CHECKBOX, self.onChannelToggled)
782
+ self.m_check_chz.Bind(wx.EVT_CHECKBOX, self.onChannelToggled)
783
+ self.m_choice_timespan.Bind(wx.EVT_CHOICE, self.onTimeSpanChange)
784
+ self.m_check_time_y_auto.Bind(wx.EVT_CHECKBOX, self.on_time_y_auto_toggle)
785
+ self.m_text_time_ylim.Bind(wx.EVT_TEXT_ENTER, self.on_time_ylim_enter)
786
+ self.m_check_psd_y_auto.Bind(wx.EVT_CHECKBOX, self.on_psd_y_auto_toggle)
787
+ self.m_text_psd_ylim.Bind(wx.EVT_TEXT_ENTER, self.on_psd_ylim_enter)
597
788
 
598
789
  bSizer3.Add(self.m_left_notebook, 1, wx.ALL | wx.EXPAND, 5)
599
790
  bSizer1.Add( bSizer3, 1, wx.EXPAND, 5 )
@@ -613,6 +804,7 @@ class View(wx.Frame):
613
804
  "SD Stream Start",
614
805
  "SD Stream Stop",
615
806
  "SD Clear All",
807
+ "Shutdown",
616
808
  ],
617
809
  )
618
810
  cmd_choice_sizer.Add(self.m_choice_OperationMode, 0, wx.ALL | wx.CENTER, 5)
@@ -681,46 +873,6 @@ class View(wx.Frame):
681
873
  self.m_menu_data.AppendSubMenu(self.m_menu_data_clear, _(u"Clear"))
682
874
  self.m_menubar.Append(self.m_menu_data, _(u"&Data"))
683
875
 
684
- self.m_menu_cmd = wx.Menu()
685
- self.m_menu_cmd_sdclear = wx.MenuItem(self.m_menu_cmd, wx.ID_ANY, _(u"SD &Clear"), wx.EmptyString, wx.ITEM_NORMAL)
686
- self.m_menu_cmd.Append(self.m_menu_cmd_sdclear)
687
- self.m_menu_cmd_shutdown = wx.MenuItem(self.m_menu_cmd, wx.ID_ANY, _(u"&Shutdown"), wx.EmptyString, wx.ITEM_NORMAL)
688
- self.m_menu_cmd.Append(self.m_menu_cmd_shutdown)
689
- self.m_menubar.Append(self.m_menu_cmd, _(u"&CMD"))
690
-
691
-
692
- self.m_menu_view = wx.Menu()
693
- self.m_menu_view_clf = wx.MenuItem(self.m_menu_view, wx.ID_ANY, _(u"&Clear Figure"), wx.EmptyString, wx.ITEM_NORMAL)
694
- self.m_menu_view.Append(self.m_menu_view_clf)
695
-
696
- self.m_menu_view_chsel = wx.Menu()
697
- self.m_menu_view_chsel_x = self.m_menu_view_chsel.AppendCheckItem(wx.ID_ANY, "X")
698
- self.m_menu_view_chsel_y = self.m_menu_view_chsel.AppendCheckItem(wx.ID_ANY, "Y")
699
- self.m_menu_view_chsel_z = self.m_menu_view_chsel.AppendCheckItem(wx.ID_ANY, "Z")
700
- self.m_menu_view_chsel_y.Check(True)
701
- self.m_menu_view.AppendSubMenu(self.m_menu_view_chsel, "Channel Selection")
702
- self.Bind(wx.EVT_MENU, self.onChannelToggled, self.m_menu_view_chsel_x)
703
- self.Bind(wx.EVT_MENU, self.onChannelToggled, self.m_menu_view_chsel_y)
704
- self.Bind(wx.EVT_MENU, self.onChannelToggled, self.m_menu_view_chsel_z)
705
-
706
-
707
- self.m_menu_view_timespan = wx.Menu()
708
- self.m_menu_view_timespan_radio_30s = self.m_menu_view_timespan.AppendRadioItem(wx.ID_ANY, "30 sec")
709
- self.m_menu_view_timespan_radio_20s = self.m_menu_view_timespan.AppendRadioItem(wx.ID_ANY, "20 sec")
710
- self.m_menu_view_timespan_radio_10s = self.m_menu_view_timespan.AppendRadioItem(wx.ID_ANY, "10 sec")
711
- self.m_menu_view_timespan_radio_05s = self.m_menu_view_timespan.AppendRadioItem(wx.ID_ANY, "5 sec")
712
- self.m_menu_view_timespan_radio_02s = self.m_menu_view_timespan.AppendRadioItem(wx.ID_ANY, "2 sec")
713
- self.m_menu_view_timespan_radio_30s.Check(True)
714
- self.m_menu_view.AppendSubMenu(self.m_menu_view_timespan, "Timespan Select")
715
- self.Bind(wx.EVT_MENU, self.onTimeSpanChange, self.m_menu_view_timespan_radio_30s)
716
- self.Bind(wx.EVT_MENU, self.onTimeSpanChange, self.m_menu_view_timespan_radio_20s)
717
- self.Bind(wx.EVT_MENU, self.onTimeSpanChange, self.m_menu_view_timespan_radio_10s)
718
- self.Bind(wx.EVT_MENU, self.onTimeSpanChange, self.m_menu_view_timespan_radio_05s)
719
- self.Bind(wx.EVT_MENU, self.onTimeSpanChange, self.m_menu_view_timespan_radio_02s)
720
-
721
-
722
- self.m_menubar.Append(self.m_menu_view, _(u"&View"))
723
-
724
876
  self.m_menu_help = wx.Menu()
725
877
  self.m_menu_help_about = wx.MenuItem(self.m_menu_help, wx.ID_ANY, _(u"&Help"), wx.EmptyString, wx.ITEM_NORMAL)
726
878
  self.m_menu_help.Append(self.m_menu_help_about)
@@ -739,23 +891,25 @@ class View(wx.Frame):
739
891
  self.plot_updater = PlotUpdater(self)
740
892
  self.plot_updater.init_plot()
741
893
  self.m_plot_canvas = self.m_fig.canvas
742
- sizer = wx.BoxSizer(wx.VERTICAL)
743
- sizer.Add(self.m_fig, 1, wx.EXPAND)
744
- self.m_plot_panel.SetSizer(sizer)
745
894
  self.m_plot_canvas.Bind(wx.EVT_LEFT_DOWN, self._focus_plot_canvas)
746
895
 
747
- self.m_fig_merged = Plot(self.m_plot_panel_merged)
896
+ self.m_fig_merged = Plot(self.m_plot_panel)
748
897
  self.axes1_merged = self.m_fig_merged.figure.add_subplot(2, 1, 1)
749
898
  self.axes2_merged = self.m_fig_merged.figure.add_subplot(2, 1, 2)
750
899
  self.m_fig_merged.figure.subplots_adjust(hspace=0.28, bottom=0.08, top=0.95)
751
900
  self.merged_plot_updater = MergedPlotUpdater(self)
752
901
  self.merged_plot_updater.init_plot()
753
902
  self.m_plot_canvas_merged = self.m_fig_merged.canvas
754
- merged_sizer = wx.BoxSizer(wx.VERTICAL)
755
- merged_sizer.Add(self.m_fig_merged, 1, wx.EXPAND)
756
- self.m_plot_panel_merged.SetSizer(merged_sizer)
757
903
  self.m_plot_canvas_merged.Bind(wx.EVT_LEFT_DOWN, self._focus_plot_canvas)
758
904
 
905
+ plot_stack = wx.BoxSizer(wx.VERTICAL)
906
+ plot_stack.Add(self.m_fig, 1, wx.EXPAND)
907
+ plot_stack.Add(self.m_fig_merged, 1, wx.EXPAND)
908
+ self.m_fig_merged.Hide()
909
+ self.m_plot_panel.SetSizer(plot_stack)
910
+ self._plot_stack = plot_stack
911
+ self._active_plot = "rt"
912
+
759
913
  # UPDATE TIMERS
760
914
 
761
915
  self._last_plot_version = 0
@@ -796,14 +950,6 @@ class View(wx.Frame):
796
950
  self.m_left_notebook.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.on_tab_changed)
797
951
 
798
952
 
799
- self.timespan_map = {
800
- self.m_menu_view_timespan_radio_30s.GetId(): 30,
801
- self.m_menu_view_timespan_radio_20s.GetId(): 20,
802
- self.m_menu_view_timespan_radio_10s.GetId(): 10,
803
- self.m_menu_view_timespan_radio_05s.GetId(): 5,
804
- self.m_menu_view_timespan_radio_02s.GetId(): 2,
805
- }
806
-
807
953
  def mesh_status_data_view(self):
808
954
  if self.table_updater is None:
809
955
  self.table_updater = TableUpdater(self)
@@ -821,6 +967,15 @@ class View(wx.Frame):
821
967
  self.m_textCtrl_tmp,
822
968
  self.m_textCtrl_str,
823
969
  self.m_textCtrl_veh,
970
+ getattr(self, "m_button_clf", None),
971
+ getattr(self, "m_check_chx", None),
972
+ getattr(self, "m_check_chy", None),
973
+ getattr(self, "m_check_chz", None),
974
+ getattr(self, "m_choice_timespan", None),
975
+ getattr(self, "m_check_time_y_auto", None),
976
+ getattr(self, "m_text_time_ylim", None),
977
+ getattr(self, "m_check_psd_y_auto", None),
978
+ getattr(self, "m_text_psd_ylim", None),
824
979
  getattr(self, "m_plot_canvas", None),
825
980
  getattr(self, "m_plot_canvas_merged", None),
826
981
  ):
@@ -900,9 +1055,32 @@ class View(wx.Frame):
900
1055
  return
901
1056
  self.merged_plot_updater.figure_update()
902
1057
 
1058
+ def _set_active_plot(self, mode: str):
1059
+ if mode not in ("rt", "merged"):
1060
+ return
1061
+ if getattr(self, "_active_plot", None) == mode:
1062
+ return
1063
+ self._active_plot = mode
1064
+ if mode == "merged":
1065
+ self.m_fig.Hide()
1066
+ self.m_fig_merged.Show()
1067
+ else:
1068
+ self.m_fig_merged.Hide()
1069
+ self.m_fig.Show()
1070
+ try:
1071
+ self.m_plot_panel.Layout()
1072
+ except Exception:
1073
+ pass
1074
+
1075
+ def show_rt_plots(self):
1076
+ self._set_active_plot("rt")
1077
+ if hasattr(self, "tab_index_rt_plots"):
1078
+ self.m_left_notebook.SetSelection(self.tab_index_rt_plots)
1079
+
903
1080
  def show_merged_plots(self):
904
- if hasattr(self, "tab_index_merged_plots"):
905
- self.m_left_notebook.SetSelection(self.tab_index_merged_plots)
1081
+ self._set_active_plot("merged")
1082
+ if hasattr(self, "tab_index_rt_plots"):
1083
+ self.m_left_notebook.SetSelection(self.tab_index_rt_plots)
906
1084
 
907
1085
  def _focus_plot_canvas(self, event):
908
1086
  try:
@@ -986,14 +1164,12 @@ class View(wx.Frame):
986
1164
 
987
1165
  def on_tab_changed(self, event):
988
1166
  idx = event.GetSelection()
989
- if idx == self.tab_index_rt_plots and hasattr(self, "m_plot_canvas"):
990
- try:
991
- self.m_plot_canvas.SetFocus()
992
- except Exception:
993
- pass
994
- if idx == self.tab_index_merged_plots and hasattr(self, "m_plot_canvas_merged"):
1167
+ if idx == self.tab_index_rt_plots:
1168
+ canvas = self.m_plot_canvas
1169
+ if getattr(self, "_active_plot", "rt") == "merged":
1170
+ canvas = self.m_plot_canvas_merged
995
1171
  try:
996
- self.m_plot_canvas_merged.SetFocus()
1172
+ canvas.SetFocus()
997
1173
  except Exception:
998
1174
  pass
999
1175
  event.Skip()
@@ -1004,10 +1180,24 @@ class View(wx.Frame):
1004
1180
  def map_update(self, event):
1005
1181
  if self.m_map is None or not self.m_map_loaded:
1006
1182
  return
1183
+ connected = set()
1184
+ if self.model is not None:
1185
+ for _, row in self.model.mesh_status_data.iterrows():
1186
+ if row.get("Connection") == "connected":
1187
+ connected.add(row.get("nodeID"))
1188
+
1189
+ for node_id in list(self._map_nodes):
1190
+ if node_id not in connected:
1191
+ safe_id = str(node_id).replace("'", "\\'")
1192
+ self.m_map.RunScript(f"removeMarker('{safe_id}');")
1193
+ self._map_nodes.discard(node_id)
1194
+
1007
1195
  positions = getattr(self.model, "gnss_positions", {})
1008
1196
  if not positions:
1009
1197
  return
1010
1198
  for node_id, pos in positions.items():
1199
+ if node_id not in connected:
1200
+ continue
1011
1201
  if not pos.get("valid"):
1012
1202
  continue
1013
1203
  lat = pos.get("lat")
@@ -1016,6 +1206,7 @@ class View(wx.Frame):
1016
1206
  continue
1017
1207
  safe_id = str(node_id).replace("'", "\\'")
1018
1208
  self.m_map.RunScript(f"updateMarker('{safe_id}', {lat}, {lon});")
1209
+ self._map_nodes.add(node_id)
1019
1210
 
1020
1211
  def _load_map(self):
1021
1212
  if self.m_map is None:
@@ -1043,6 +1234,26 @@ class View(wx.Frame):
1043
1234
  attribution: '&copy; OpenStreetMap contributors'
1044
1235
  }).addTo(map);
1045
1236
  const markers = {};
1237
+ let bounds = L.latLngBounds();
1238
+ let autoFit = true;
1239
+
1240
+ function rebuildBounds() {
1241
+ bounds = L.latLngBounds();
1242
+ let hasMarker = false;
1243
+ for (const key in markers) {
1244
+ if (!Object.prototype.hasOwnProperty.call(markers, key)) continue;
1245
+ const latlng = markers[key].getLatLng();
1246
+ bounds.extend(latlng);
1247
+ hasMarker = true;
1248
+ }
1249
+ if (autoFit && hasMarker && bounds.isValid()) {
1250
+ map.fitBounds(bounds.pad(0.15), {animate: false});
1251
+ }
1252
+ }
1253
+
1254
+ map.on('dragstart zoomstart', () => {
1255
+ autoFit = false;
1256
+ });
1046
1257
  window.updateMarker = function(nodeId, lat, lon) {
1047
1258
  let m = markers[nodeId];
1048
1259
  if (!m) {
@@ -1051,6 +1262,17 @@ class View(wx.Frame):
1051
1262
  markers[nodeId] = m;
1052
1263
  }
1053
1264
  m.setLatLng([lat, lon]);
1265
+ bounds.extend([lat, lon]);
1266
+ if (autoFit && bounds.isValid()) {
1267
+ map.fitBounds(bounds.pad(0.15), {animate: false});
1268
+ }
1269
+ };
1270
+ window.removeMarker = function(nodeId) {
1271
+ const m = markers[nodeId];
1272
+ if (!m) return;
1273
+ map.removeLayer(m);
1274
+ delete markers[nodeId];
1275
+ rebuildBounds();
1054
1276
  };
1055
1277
  </script>
1056
1278
  </body>
@@ -1136,14 +1358,156 @@ class View(wx.Frame):
1136
1358
  return
1137
1359
  self.plot_updater.update_psd_legend()
1138
1360
 
1361
+ def _parse_ylim_text(self, text: str):
1362
+ if text is None:
1363
+ return None
1364
+ raw = text.strip()
1365
+ if not raw:
1366
+ return None
1367
+ if raw.startswith("[") and raw.endswith("]"):
1368
+ raw = raw[1:-1].strip()
1369
+ parts = [p.strip() for p in raw.split(",") if p.strip()]
1370
+ if len(parts) != 2:
1371
+ parts = [p.strip() for p in raw.split() if p.strip()]
1372
+ if len(parts) != 2:
1373
+ return None
1374
+ try:
1375
+ lo = float(parts[0])
1376
+ hi = float(parts[1])
1377
+ except (TypeError, ValueError):
1378
+ return None
1379
+ if lo == hi:
1380
+ return None
1381
+ if lo > hi:
1382
+ lo, hi = hi, lo
1383
+ return lo, hi
1384
+
1385
+ def _parse_bool_option(self, value, default: bool) -> bool:
1386
+ if value is None:
1387
+ return default
1388
+ if isinstance(value, bool):
1389
+ return value
1390
+ text = str(value).strip().lower()
1391
+ if text in {"1", "true", "yes", "y", "on"}:
1392
+ return True
1393
+ if text in {"0", "false", "no", "n", "off"}:
1394
+ return False
1395
+ return default
1396
+
1397
+ def _parse_int_option(self, value, default: int) -> int:
1398
+ if value is None:
1399
+ return default
1400
+ try:
1401
+ return int(str(value).strip())
1402
+ except (TypeError, ValueError):
1403
+ return default
1139
1404
 
1140
- def onChannelToggled(self, event):
1141
-
1405
+ def _load_plot_options(self) -> None:
1406
+ if self.model is None:
1407
+ return
1408
+ options = getattr(self.model, "options", {}) or {}
1142
1409
  self.channel_selection = [
1143
- self.m_menu_view_chsel_x.IsChecked(),
1144
- self.m_menu_view_chsel_y.IsChecked(),
1145
- self.m_menu_view_chsel_z.IsChecked()
1410
+ self._parse_bool_option(options.get("plot_channel_x"), self.channel_selection[0]),
1411
+ self._parse_bool_option(options.get("plot_channel_y"), self.channel_selection[1]),
1412
+ self._parse_bool_option(options.get("plot_channel_z"), self.channel_selection[2]),
1146
1413
  ]
1414
+ self.timespan_length = self._parse_int_option(
1415
+ options.get("plot_timespan"), self.timespan_length
1416
+ )
1417
+ self.model.timespan_length = self.timespan_length
1418
+ self.time_y_auto = self._parse_bool_option(
1419
+ options.get("plot_time_y_auto"), self.time_y_auto
1420
+ )
1421
+ time_ylim = self._parse_ylim_text(options.get("plot_time_ylim", ""))
1422
+ if time_ylim is not None:
1423
+ self.time_y_limits = time_ylim
1424
+ self.psd_y_auto = self._parse_bool_option(
1425
+ options.get("plot_psd_y_auto"), self.psd_y_auto
1426
+ )
1427
+ psd_ylim = self._parse_ylim_text(options.get("plot_psd_ylim", ""))
1428
+ if psd_ylim is not None:
1429
+ self.psd_y_limits = psd_ylim
1430
+
1431
+ def _persist_plot_options(self) -> None:
1432
+ if self.model is None:
1433
+ return
1434
+ options = getattr(self.model, "options", None)
1435
+ if options is None:
1436
+ return
1437
+ options["plot_channel_x"] = "true" if self.channel_selection[0] else "false"
1438
+ options["plot_channel_y"] = "true" if self.channel_selection[1] else "false"
1439
+ options["plot_channel_z"] = "true" if self.channel_selection[2] else "false"
1440
+ options["plot_timespan"] = str(self.timespan_length)
1441
+ options["plot_time_y_auto"] = "true" if self.time_y_auto else "false"
1442
+ options["plot_time_ylim"] = f"[{self.time_y_limits[0]}, {self.time_y_limits[1]}]"
1443
+ options["plot_psd_y_auto"] = "true" if self.psd_y_auto else "false"
1444
+ options["plot_psd_ylim"] = f"[{self.psd_y_limits[0]}, {self.psd_y_limits[1]}]"
1445
+ self.model.save_config()
1446
+
1447
+ def on_time_y_auto_toggle(self, event):
1448
+ self.time_y_auto = self.m_check_time_y_auto.GetValue()
1449
+ self.m_text_time_ylim.Enable(not self.time_y_auto)
1450
+ self._persist_plot_options()
1451
+ self.init_plot()
1452
+ self.figure_update()
1453
+ self.init_merged_plot()
1454
+ self.figure_update_merged()
1455
+
1456
+ def on_psd_y_auto_toggle(self, event):
1457
+ self.psd_y_auto = self.m_check_psd_y_auto.GetValue()
1458
+ self.m_text_psd_ylim.Enable(not self.psd_y_auto)
1459
+ self._persist_plot_options()
1460
+ self.init_plot()
1461
+ self.figure_update()
1462
+ self.init_merged_plot()
1463
+ self.figure_update_merged()
1464
+
1465
+ def on_time_ylim_enter(self, event):
1466
+ lims = self._parse_ylim_text(self.m_text_time_ylim.GetValue())
1467
+ if lims is None:
1468
+ self.append_status_message("[plot] invalid time ylim (use [low, high])")
1469
+ return
1470
+ self.time_y_limits = lims
1471
+ self.m_text_time_ylim.SetValue(f"[{lims[0]}, {lims[1]}]")
1472
+ self._persist_plot_options()
1473
+ if not self.time_y_auto:
1474
+ self.init_plot()
1475
+ self.figure_update()
1476
+ self.init_merged_plot()
1477
+ self.figure_update_merged()
1478
+
1479
+ def on_psd_ylim_enter(self, event):
1480
+ lims = self._parse_ylim_text(self.m_text_psd_ylim.GetValue())
1481
+ if lims is None:
1482
+ self.append_status_message("[plot] invalid ASD ylim (use [low, high])")
1483
+ return
1484
+ self.psd_y_limits = lims
1485
+ self.m_text_psd_ylim.SetValue(f"[{lims[0]}, {lims[1]}]")
1486
+ self._persist_plot_options()
1487
+ if not self.psd_y_auto:
1488
+ self.init_plot()
1489
+ self.figure_update()
1490
+ self.init_merged_plot()
1491
+ self.figure_update_merged()
1492
+
1493
+ def _read_channel_selection(self):
1494
+ if hasattr(self, "m_check_chx"):
1495
+ return [
1496
+ self.m_check_chx.GetValue(),
1497
+ self.m_check_chy.GetValue(),
1498
+ self.m_check_chz.GetValue(),
1499
+ ]
1500
+ if hasattr(self, "m_menu_view_chsel_x"):
1501
+ return [
1502
+ self.m_menu_view_chsel_x.IsChecked(),
1503
+ self.m_menu_view_chsel_y.IsChecked(),
1504
+ self.m_menu_view_chsel_z.IsChecked(),
1505
+ ]
1506
+ return list(self.channel_selection)
1507
+
1508
+ def onChannelToggled(self, event):
1509
+ self.channel_selection = self._read_channel_selection()
1510
+ self._persist_plot_options()
1147
1511
 
1148
1512
  self.init_plot()
1149
1513
  self.figure_update()
@@ -1210,13 +1574,17 @@ class View(wx.Frame):
1210
1574
  event.Skip()
1211
1575
 
1212
1576
  def onTimeSpanChange(self, event):
1213
-
1214
- ts = self.timespan_map.get(event.GetId(), None)
1577
+ ts = None
1578
+ if hasattr(self, "m_choice_timespan"):
1579
+ idx = self.m_choice_timespan.GetSelection()
1580
+ if idx != wx.NOT_FOUND and hasattr(self, "timespan_choices"):
1581
+ ts = self.timespan_choices[idx]
1215
1582
  if ts:
1216
1583
  logger.info(f"Timespan selected: {ts} sec")
1217
1584
  self.timespan_length = ts
1218
1585
  if self.model is not None:
1219
1586
  self.model.timespan_length = ts
1587
+ self._persist_plot_options()
1220
1588
  self.init_plot()
1221
1589
  self.figure_update()
1222
1590
  self.init_merged_plot()
@@ -1228,6 +1596,6 @@ class View(wx.Frame):
1228
1596
  def _append_status_message(self, msg: str) -> None:
1229
1597
  if hasattr(self, "m_statusBar1") and self.m_statusBar1 is not None:
1230
1598
  self.m_statusBar1.SetStatusText(msg)
1231
- print(msg, flush=True)
1599
+ logger.info("%s", msg)
1232
1600
 
1233
1601