enode-host 0.1.0__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 ADDED
@@ -0,0 +1,1233 @@
1
+ #!/usr/local/bin/python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ #
5
+ # This is the file to construct GUI elements
6
+ #
7
+
8
+
9
+ ###########################################################################
10
+ ## Python code generated with wxFormBuilder (version 4.2.1-0-g80c4cb6)
11
+ ## http://www.wxformbuilder.org/
12
+ ##
13
+ ## PLEASE DO *NOT* EDIT THIS FILE!
14
+ ###########################################################################
15
+
16
+ import wx
17
+ import wx.xrc
18
+ import wx.grid
19
+
20
+ import gettext
21
+ _ = gettext.gettext
22
+
23
+ import wx
24
+ import wx.lib.agw.aui as aui
25
+ import wx.lib.mixins.inspection as wit
26
+ from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
27
+ from matplotlib.backends.backend_wxagg import \
28
+ NavigationToolbar2WxAgg as NavigationToolbar
29
+ from matplotlib.figure import Figure
30
+ import matplotlib.dates as mdates
31
+ import wx.grid as gridlib
32
+ try:
33
+ import wx.html2 as webview
34
+ except ImportError:
35
+ webview = None
36
+ try:
37
+ from . import queues
38
+ except ImportError:
39
+ import queues
40
+ import datetime
41
+ from numpy import empty, where, array, delete, append, ones, nan, floor, shape
42
+ import random
43
+ import logging
44
+ import time
45
+ import math
46
+ try:
47
+ from .constants import (
48
+ GUI_TIMER_MS,
49
+ LEVEL_COL,
50
+ SPEED_COL,
51
+ RSSI_COL,
52
+ PPS_AGE_LIMIT_SEC,
53
+ PPS_AGE_UPDATE_MS,
54
+ PPS_COL,
55
+ )
56
+ except ImportError:
57
+ from constants import (
58
+ GUI_TIMER_MS,
59
+ LEVEL_COL,
60
+ SPEED_COL,
61
+ RSSI_COL,
62
+ PPS_AGE_LIMIT_SEC,
63
+ PPS_AGE_UPDATE_MS,
64
+ PPS_COL,
65
+ )
66
+ import pandas as pd
67
+
68
+ if "SPEED_COL" not in globals():
69
+ SPEED_COL = "Speed\n(kB/s)"
70
+
71
+ logger = logging.getLogger(__name__)
72
+
73
+ class Plot(wx.Panel):
74
+ def __init__(self, parent, id=-1, dpi=None, **kwargs):
75
+ super().__init__(parent, id=id, **kwargs)
76
+ self.figure = Figure(dpi=dpi, figsize=(2, 2))
77
+ self.canvas = FigureCanvas(self, -1, self.figure)
78
+ self.toolbar = NavigationToolbar(self.canvas)
79
+ self.toolbar.Realize()
80
+
81
+ sizer = wx.BoxSizer(wx.VERTICAL)
82
+ sizer.Add(self.canvas, 1, wx.EXPAND)
83
+ sizer.Add(self.toolbar, 0, wx.LEFT | wx.EXPAND)
84
+ self.SetSizer(sizer)
85
+
86
+ class PandasTable(wx.grid.GridTableBase):
87
+ def __init__(self, dataframe):
88
+ wx.grid.GridTableBase.__init__(self)
89
+ self.dataframe = dataframe
90
+
91
+ def GetNumberRows(self):
92
+ return self.dataframe.shape[0]
93
+
94
+ def GetNumberCols(self):
95
+ return self.dataframe.shape[1]
96
+
97
+ def GetValue(self, row, col):
98
+ # Fetch value from pandas DataFrame
99
+ return str(self.dataframe.iat[row, col])
100
+
101
+ def SetValue(self, row, col, value):
102
+ # Set value in pandas DataFrame
103
+ self.dataframe.iat[row, col] = value
104
+ self.GetView().ForceRefresh()
105
+
106
+ def GetColLabelValue(self, col):
107
+ # Get column names from pandas DataFrame
108
+ return str(self.dataframe.columns[col])
109
+
110
+ def GetRowLabelValue(self, row):
111
+ # Optional: Get row indices from pandas DataFrame
112
+ return str(self.dataframe.index[row])
113
+
114
+ def update_table(self):
115
+ # msg = wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_NOTIFY_TABLE_RESET)
116
+ msg = wx.grid.GridTableMessage(self, wx.grid.GRIDTABLE_NOTIFY_ROWS_APPENDED)
117
+ self.GetView().ProcessTableMessage(msg)
118
+
119
+
120
+ class TableUpdater:
121
+ def __init__(self, view):
122
+ self.view = view
123
+ self.model = view.model
124
+ self.grid = view.m_grid2
125
+ self.table = None
126
+
127
+ def setup_table(self):
128
+ self.table = PandasTable(self.model.mesh_status_data)
129
+ self.grid.SetTable(self.table, takeOwnership=True)
130
+ self.grid.SetColLabelSize(40)
131
+ self.col_index = {
132
+ name: self.model.mesh_status_data.columns.get_loc(name)
133
+ for name in self.model.mesh_status_data.columns
134
+ }
135
+ self.view.col_index = self.col_index
136
+ self.grid.SetColSize(self.col_index["Node ID"], 70)
137
+ self.grid.SetColSize(self.col_index["Connection"], 90)
138
+ self.grid.SetColSize(self.col_index["Sensor"], 80)
139
+ self.grid.SetColSize(self.col_index["DAQ Mode"], 120)
140
+ if "DAQ" in self.col_index:
141
+ self.grid.SetColSize(self.col_index["DAQ"], 40)
142
+ if "Stream" in self.col_index:
143
+ self.grid.SetColSize(self.col_index["Stream"], 60)
144
+ self.grid.SetColSize(self.col_index[SPEED_COL], 60)
145
+ self.grid.SetColSize(self.col_index[LEVEL_COL], 20)
146
+ self.grid.SetColSize(self.col_index["Parent"], 70)
147
+ self.grid.SetColSize(self.col_index[RSSI_COL], 40)
148
+ self.grid.SetColSize(self.col_index["Children"], 70)
149
+ self.grid.SetColSize(self.col_index[PPS_COL], 40)
150
+ self.grid.SetColSize(self.col_index["CMD"], 200)
151
+ for hidden in [
152
+ "PPS-time",
153
+ "PPS-flash-time",
154
+ "DAQ-time",
155
+ "Parent MAC",
156
+ "Self MAC",
157
+ "nodeID",
158
+ "ConnRptTime",
159
+ "node_number",
160
+ "node_type",
161
+ ]:
162
+ if hidden in self.col_index:
163
+ self.grid.SetColSize(self.col_index[hidden], 0)
164
+
165
+ def refresh(self):
166
+ self.grid.ForceRefresh()
167
+ self.update_status_table()
168
+
169
+ def update_status_table(self):
170
+ for index, row in self.model.mesh_status_data.iterrows():
171
+ if row['Connection'] == 'disconnected':
172
+ attr = gridlib.GridCellAttr()
173
+ attr.SetTextColour(wx.Colour(255, 0, 0))
174
+ attr.SetBackgroundColour(wx.Colour(255, 255, 255))
175
+ self.grid.SetAttr(index, self.col_index["Connection"], attr)
176
+ else:
177
+ attr = gridlib.GridCellAttr()
178
+ attr.SetTextColour(wx.Colour(0, 0, 0))
179
+ attr.SetBackgroundColour(wx.Colour(255, 255, 255))
180
+ self.grid.SetAttr(index, self.col_index["Connection"], attr)
181
+
182
+ def update_pps_age(self):
183
+ t_now = datetime.datetime.now(datetime.timezone.utc)
184
+ for index, row in self.model.mesh_status_data.iterrows():
185
+ pps_time = row['PPS-time']
186
+ if isinstance(pps_time, datetime.datetime):
187
+ if pps_time.tzinfo is None:
188
+ pps_time = pps_time.replace(tzinfo=datetime.timezone.utc)
189
+ age = (t_now - pps_time).total_seconds()
190
+ value = f"{age:.1f}s" if age <= PPS_AGE_LIMIT_SEC else ''
191
+ else:
192
+ value = ''
193
+ node_id = row.get('nodeID')
194
+ if hasattr(self.model, "_set_node_fields"):
195
+ self.model._set_node_fields(node_id, mark_dirty=False, **{PPS_COL: value})
196
+ else:
197
+ self.model.mesh_status_data.loc[index, PPS_COL] = value
198
+ self.grid.ForceRefresh()
199
+
200
+ def update_pps_flash(self):
201
+ t_now = datetime.datetime.now(datetime.timezone.utc)
202
+ with self.model.pps_flash_lock:
203
+ flash_until_map = dict(self.model.pps_flash_until)
204
+ for index, row in self.model.mesh_status_data.iterrows():
205
+ node_id = row.get('nodeID')
206
+ until = flash_until_map.get(node_id)
207
+ attr = gridlib.GridCellAttr()
208
+ if isinstance(until, datetime.datetime):
209
+ if until.tzinfo is None:
210
+ until = until.replace(tzinfo=datetime.timezone.utc)
211
+ if t_now <= until:
212
+ attr.SetTextColour(wx.Colour(0, 180, 0))
213
+ else:
214
+ attr.SetTextColour(wx.Colour(0, 0, 0))
215
+ else:
216
+ attr.SetTextColour(wx.Colour(0, 0, 0))
217
+ self.grid.SetAttr(index, self.col_index[PPS_COL], attr)
218
+
219
+
220
+ class PlotUpdater:
221
+ def __init__(self, view):
222
+ self.view = view
223
+ self.model = view.model
224
+ self.axes1 = view.axes1
225
+ self.axes2 = view.axes2
226
+ self.timehistory_lines = {}
227
+ self.psd_lines = {}
228
+ self._psd_legend_labels = {}
229
+
230
+ def init_plot(self):
231
+ self.timehistory_lines = {}
232
+ self.psd_lines = {}
233
+ self._psd_legend_labels = {}
234
+
235
+ self.axes1.cla()
236
+ locator = mdates.AutoDateLocator(minticks=3, maxticks=6)
237
+ formatter = mdates.DateFormatter("%H:%M:%S")
238
+ self.axes1.xaxis.set_major_locator(locator)
239
+ self.axes1.xaxis.set_major_formatter(formatter)
240
+ for _, row in self.model.mesh_status_data.iterrows():
241
+ if not str(row['Node ID']).startswith('ACC'):
242
+ continue
243
+ for idx, ch in enumerate(['X', 'Y', 'Z']):
244
+ if self.view.channel_selection[idx]:
245
+ node_id_ch = row['Node ID'] + ch
246
+ self.timehistory_lines[node_id_ch] = self.axes1.plot([], [], label=node_id_ch)[0]
247
+ self.axes1.set_xlabel('Time')
248
+ self.axes1.set_ylabel('Acceleration (g)')
249
+ self.axes1.grid(True)
250
+ self.axes1.set_ylim(-2, 2)
251
+ self.axes1.tick_params(axis='x', labelbottom=True, pad=2)
252
+ self.axes1.legend()
253
+ self.view.m_fig.figure.autofmt_xdate(rotation=0, ha='right')
254
+ self.axes1.tick_params(axis='x', labelbottom=True, pad=2)
255
+
256
+ self.axes2.cla()
257
+ for _, row in self.model.mesh_status_data.iterrows():
258
+ if not str(row['Node ID']).startswith('ACC'):
259
+ continue
260
+ for idx, ch in enumerate(['X', 'Y', 'Z']):
261
+ if self.view.channel_selection[idx]:
262
+ node_id_ch = row['Node ID'] + ch
263
+ self.psd_lines[node_id_ch] = self.axes2.semilogy([], [], label=node_id_ch)[0]
264
+
265
+ self.axes2.set_xlabel('Frequency (Hz)')
266
+ self.axes2.set_ylabel('ASD (g/sq(Hz))')
267
+ self.axes2.grid(True)
268
+ self.axes2.set_ylim(1e-6, 1e1)
269
+ self.axes2.set_xlim(0, 25)
270
+ self.axes2.legend()
271
+
272
+ def figure_update(self):
273
+ for _, row in self.model.mesh_status_data.iterrows():
274
+ node_id = row['nodeID']
275
+ for idx, ch in enumerate(['X', 'Y', 'Z']):
276
+ node_id_ch = row['Node ID'] + ch
277
+ if node_id_ch in self.timehistory_lines and node_id in self.model.timehistory_xdata:
278
+ with self.model.plot_mutex[node_id]:
279
+ if len(self.model.timehistory_xdata[node_id]) > 1:
280
+ 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])
282
+ if self.model.timehistory_xlim:
283
+ self.axes1.set_xlim(self.model.timehistory_xlim)
284
+ try:
285
+ self.view.m_fig.figure.canvas.draw()
286
+ except Exception:
287
+ raise
288
+
289
+ for _, row in self.model.mesh_status_data.iterrows():
290
+ node_id = row['nodeID']
291
+ for idx, ch in enumerate(['X', 'Y', 'Z']):
292
+ node_id_ch = row['Node ID'] + ch
293
+ if node_id_ch in self.psd_lines and node_id in self.model.psd_ydata:
294
+ 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]))
300
+ try:
301
+ self.view.m_fig.figure.canvas.draw()
302
+ except Exception:
303
+ raise
304
+
305
+ def update_psd_legend(self):
306
+ if not self.psd_lines:
307
+ return
308
+ changed = False
309
+ for _, row in self.model.mesh_status_data.iterrows():
310
+ node_id = row['nodeID']
311
+ node_label = row['Node ID']
312
+ psder = self.model.psder.get(node_id)
313
+ seconds_left = None
314
+ if psder is not None:
315
+ try:
316
+ seconds_left = psder.seconds_until_next_update()
317
+ except Exception:
318
+ seconds_left = None
319
+ label_suffix = ""
320
+ if seconds_left is not None:
321
+ sec_int = int(math.ceil(seconds_left))
322
+ if sec_int < 0:
323
+ sec_int = 0
324
+ label_suffix = f" ({sec_int} sec)"
325
+ for ch in ['X', 'Y', 'Z']:
326
+ node_ch = node_label + ch
327
+ line = self.psd_lines.get(node_ch)
328
+ if line is None:
329
+ continue
330
+ new_label = node_ch + label_suffix if label_suffix else node_ch
331
+ if self._psd_legend_labels.get(node_ch) != new_label:
332
+ line.set_label(new_label)
333
+ self._psd_legend_labels[node_ch] = new_label
334
+ changed = True
335
+ if changed:
336
+ self.axes2.legend()
337
+ try:
338
+ self.view.m_fig.figure.canvas.draw_idle()
339
+ except Exception:
340
+ pass
341
+
342
+
343
+ class MergedPlotUpdater:
344
+ def __init__(self, view):
345
+ self.view = view
346
+ self.model = view.model
347
+ self.axes1 = view.axes1_merged
348
+ self.axes2 = view.axes2_merged
349
+ self.timehistory_lines = {}
350
+ self.psd_lines = {}
351
+
352
+ def _node_label(self, node_id):
353
+ return str(node_id).upper()
354
+
355
+ def init_plot(self):
356
+ self.timehistory_lines = {}
357
+ self.psd_lines = {}
358
+
359
+ self.axes1.cla()
360
+ locator = mdates.AutoDateLocator(minticks=3, maxticks=6)
361
+ formatter = mdates.DateFormatter("%H:%M:%S")
362
+ self.axes1.xaxis.set_major_locator(locator)
363
+ self.axes1.xaxis.set_major_formatter(formatter)
364
+ for node_id in self.model.merged_node_ids:
365
+ node_label = self._node_label(node_id)
366
+ for idx, ch in enumerate(['X', 'Y', 'Z']):
367
+ if self.view.channel_selection[idx]:
368
+ label = node_label + ch
369
+ self.timehistory_lines[label] = self.axes1.plot([], [], label=label)[0]
370
+ self.axes1.set_xlabel('Time')
371
+ self.axes1.set_ylabel('Acceleration (g)')
372
+ self.axes1.grid(True)
373
+ self.axes1.set_ylim(-2, 2)
374
+ self.axes1.tick_params(axis='x', labelbottom=True, pad=2)
375
+ self.axes1.legend()
376
+ self.view.m_fig_merged.figure.autofmt_xdate(rotation=0, ha='right')
377
+ self.axes1.tick_params(axis='x', labelbottom=True, pad=2)
378
+
379
+ self.axes2.cla()
380
+ for node_id in self.model.merged_node_ids:
381
+ node_label = self._node_label(node_id)
382
+ for idx, ch in enumerate(['X', 'Y', 'Z']):
383
+ if self.view.channel_selection[idx]:
384
+ label = node_label + ch
385
+ self.psd_lines[label] = self.axes2.semilogy([], [], label=label)[0]
386
+
387
+ self.axes2.set_xlabel('Frequency (Hz)')
388
+ self.axes2.set_ylabel('ASD (g/sq(Hz))')
389
+ self.axes2.grid(True)
390
+ self.axes2.set_ylim(1e-6, 1e1)
391
+ self.axes2.set_xlim(0, 25)
392
+ self.axes2.legend()
393
+
394
+ def figure_update(self):
395
+ for node_id in self.model.merged_node_ids:
396
+ node_label = self._node_label(node_id)
397
+ xdata = self.model.merged_timehistory_xdata.get(node_id)
398
+ ydata = self.model.merged_timehistory_ydata.get(node_id)
399
+ if not xdata or ydata is None:
400
+ continue
401
+ for idx, ch in enumerate(['X', 'Y', 'Z']):
402
+ label = node_label + ch
403
+ line = self.timehistory_lines.get(label)
404
+ if line is not None:
405
+ line.set_xdata(xdata)
406
+ line.set_ydata(ydata[:, idx])
407
+ if self.model.merged_timehistory_xlim:
408
+ self.axes1.set_xlim(self.model.merged_timehistory_xlim)
409
+ try:
410
+ self.view.m_fig_merged.figure.canvas.draw()
411
+ except Exception:
412
+ raise
413
+
414
+ f = self.model.merged_psd_xdata
415
+ if f is None:
416
+ f = []
417
+ for node_id in self.model.merged_node_ids:
418
+ node_label = self._node_label(node_id)
419
+ ydata = self.model.merged_psd_ydata.get(node_id)
420
+ if ydata is None or len(f) == 0:
421
+ continue
422
+ for idx, ch in enumerate(['X', 'Y', 'Z']):
423
+ label = node_label + ch
424
+ line = self.psd_lines.get(label)
425
+ if line is not None:
426
+ line.set_xdata(f)
427
+ line.set_ydata(ydata[:, idx])
428
+
429
+ if len(f) > 0:
430
+ self.axes2.set_xlim(0, round(f[-1]))
431
+ try:
432
+ self.view.m_fig_merged.figure.canvas.draw()
433
+ except Exception:
434
+ raise
435
+
436
+ ###########################################################################
437
+ ## Class View
438
+ ###########################################################################
439
+
440
+ class View(wx.Frame):
441
+
442
+ def __init__(self, parent):
443
+
444
+ wx.Frame.__init__ ( self, None, id = wx.ID_ANY, title = _(u"Wireless Sensor DAQ"), pos = wx.DefaultPosition, size = wx.Size( 1024,600 ), style = wx.DEFAULT_FRAME_STYLE|wx.TAB_TRAVERSAL )
445
+
446
+ self.controller = parent
447
+ self.model = self.controller.model
448
+ if self.model is not None:
449
+ self.timespan_length = self.model.timespan_length
450
+ self.m_table = None
451
+ self.table_updater = None
452
+ self.plot_updater = None
453
+ self.merged_plot_updater = None
454
+ self.PPS_outdate_check = True
455
+ self.channel_selection = [False, True, False]
456
+
457
+ self.SetSizeHints( wx.DefaultSize, wx.DefaultSize )
458
+
459
+ bSizer1 = wx.BoxSizer( wx.VERTICAL )
460
+ # # Fs selection
461
+ # self.m_staticText3 = wx.StaticText( self, wx.ID_ANY, _(u"Fs="), wx.DefaultPosition, wx.DefaultSize, 0 )
462
+ # self.m_staticText3.Wrap(-1)
463
+ # bSizer2.Add( self.m_staticText3, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5 )
464
+
465
+ # m_choice1Choices = [ _(u"62.5"), _(u"125") ]
466
+ # self.m_choice1 = wx.Choice( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, m_choice1Choices, 0 )
467
+ # self.m_choice1.SetSelection( 0 )
468
+ # bSizer2.Add( self.m_choice1, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5 )
469
+
470
+ # self.m_staticText4 = wx.StaticText( self, wx.ID_ANY, _(u"Hz"), wx.DefaultPosition, wx.DefaultSize, 0 )
471
+ # self.m_staticText4.Wrap(-1)
472
+ # bSizer2.Add( self.m_staticText4, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5 )
473
+
474
+ bSizer3 = wx.BoxSizer( wx.VERTICAL )
475
+
476
+ self.m_left_notebook = wx.Notebook(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0)
477
+ self.m_left_notebook.SetMinSize(wx.Size(300, -1))
478
+ self.m_panel_nodes = wx.Panel(self.m_left_notebook, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL)
479
+ self.m_panel_status = wx.Panel(self.m_left_notebook, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL)
480
+ self.m_panel_map = wx.Panel(self.m_left_notebook, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL)
481
+ 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
+ self.m_left_notebook.AddPage(self.m_panel_nodes, _(u"Nodes"), False)
484
+ self.m_left_notebook.AddPage(self.m_panel_status, _(u"Mesh"), True)
485
+ 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)
488
+ self.tab_index_nodes = 0
489
+ self.tab_index_status = 1
490
+ self.tab_index_map = 2
491
+ self.tab_index_rt_plots = 3
492
+ self.tab_index_merged_plots = 4
493
+
494
+ # Sensor Nodes definition tab
495
+ nodes_sizer = wx.BoxSizer(wx.VERTICAL)
496
+ self.m_nodes_box = wx.StaticBoxSizer(wx.StaticBox(self.m_panel_nodes, wx.ID_ANY, _(u"Sensor Nodes")), wx.VERTICAL)
497
+ nodes_grid = wx.FlexGridSizer(4, 2, 4, 6)
498
+ nodes_grid.AddGrowableCol(1, 1)
499
+
500
+ self.m_staticText_acc = wx.StaticText(self.m_panel_nodes, wx.ID_ANY, _(u"ACC:"), wx.DefaultPosition, wx.DefaultSize, 0)
501
+ nodes_grid.Add(self.m_staticText_acc, 0, wx.ALIGN_CENTER_VERTICAL)
502
+ self.m_textCtrl_acc = wx.TextCtrl(
503
+ self.m_panel_nodes,
504
+ wx.ID_ANY,
505
+ self.controller.model.options.get('acc_nums_txt', '[1:3]'),
506
+ wx.DefaultPosition,
507
+ wx.DefaultSize,
508
+ 0,
509
+ )
510
+ nodes_grid.Add(self.m_textCtrl_acc, 1, wx.EXPAND)
511
+
512
+ self.m_staticText_tmp = wx.StaticText(self.m_panel_nodes, wx.ID_ANY, _(u"TMP:"), wx.DefaultPosition, wx.DefaultSize, 0)
513
+ nodes_grid.Add(self.m_staticText_tmp, 0, wx.ALIGN_CENTER_VERTICAL)
514
+ self.m_textCtrl_tmp = wx.TextCtrl(
515
+ self.m_panel_nodes,
516
+ wx.ID_ANY,
517
+ self.controller.model.options.get('tmp_nums_txt', '[]'),
518
+ wx.DefaultPosition,
519
+ wx.DefaultSize,
520
+ 0,
521
+ )
522
+ nodes_grid.Add(self.m_textCtrl_tmp, 1, wx.EXPAND)
523
+
524
+ self.m_staticText_str = wx.StaticText(self.m_panel_nodes, wx.ID_ANY, _(u"STR:"), wx.DefaultPosition, wx.DefaultSize, 0)
525
+ nodes_grid.Add(self.m_staticText_str, 0, wx.ALIGN_CENTER_VERTICAL)
526
+ self.m_textCtrl_str = wx.TextCtrl(
527
+ self.m_panel_nodes,
528
+ wx.ID_ANY,
529
+ self.controller.model.options.get('str_nums_txt', '[]'),
530
+ wx.DefaultPosition,
531
+ wx.DefaultSize,
532
+ 0,
533
+ )
534
+ nodes_grid.Add(self.m_textCtrl_str, 1, wx.EXPAND)
535
+
536
+ self.m_staticText_veh = wx.StaticText(self.m_panel_nodes, wx.ID_ANY, _(u"VEH:"), wx.DefaultPosition, wx.DefaultSize, 0)
537
+ nodes_grid.Add(self.m_staticText_veh, 0, wx.ALIGN_CENTER_VERTICAL)
538
+ self.m_textCtrl_veh = wx.TextCtrl(
539
+ self.m_panel_nodes,
540
+ wx.ID_ANY,
541
+ self.controller.model.options.get('veh_nums_txt', '[]'),
542
+ wx.DefaultPosition,
543
+ wx.DefaultSize,
544
+ 0,
545
+ )
546
+ nodes_grid.Add(self.m_textCtrl_veh, 1, wx.EXPAND)
547
+
548
+ self.m_nodes_box.Add(nodes_grid, 1, wx.EXPAND | wx.ALL, 0)
549
+ nodes_sizer.Add(self.m_nodes_box, 0, wx.ALL | wx.EXPAND, 8)
550
+
551
+ self.m_button_nodes_update = wx.Button(self.m_panel_nodes, wx.ID_ANY, _(u"Update"), wx.DefaultPosition, wx.DefaultSize, 0)
552
+ nodes_sizer.Add(self.m_button_nodes_update, 0, wx.ALL | wx.ALIGN_RIGHT, 8)
553
+ self.m_panel_nodes.SetSizer(nodes_sizer)
554
+
555
+ # WiFi Mesh Status tab
556
+ status_sizer = wx.BoxSizer(wx.VERTICAL)
557
+ self.m_grid2 = wx.grid.Grid(self.m_panel_status, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0)
558
+ self.table_updater = TableUpdater(self)
559
+ self.table_updater.setup_table()
560
+ self.m_table = self.table_updater.table
561
+
562
+ # Grid
563
+ self.nrows = len(self.controller.model.mesh_status_data)
564
+ self.m_grid2.SetRowLabelSize(0)
565
+ self.m_grid2.SetDefaultCellAlignment( wx.ALIGN_CENTER, wx.ALIGN_CENTER)
566
+ self.m_grid2.Bind(wx.grid.EVT_GRID_CELL_CHANGED, self.on_daq_mode_change)
567
+ self.m_grid2.Bind(wx.grid.EVT_GRID_SELECT_CELL, self.on_grid_select_cell)
568
+ status_sizer.Add(self.m_grid2, 1, wx.ALL | wx.EXPAND, 5)
569
+ self.m_panel_status.SetSizer(status_sizer)
570
+
571
+ map_sizer = wx.BoxSizer(wx.VERTICAL)
572
+ if webview is not None:
573
+ self.m_map = webview.WebView.New(self.m_panel_map)
574
+ map_sizer.Add(self.m_map, 1, wx.ALL | wx.EXPAND, 5)
575
+ self.m_map_loaded = False
576
+ self.m_map.Bind(webview.EVT_WEBVIEW_LOADED, self.on_map_loaded)
577
+ self._load_map()
578
+ else:
579
+ self.m_map = None
580
+ msg = wx.StaticText(
581
+ self.m_panel_map,
582
+ wx.ID_ANY,
583
+ _(u"Map view unavailable (wx.html2 not installed)."),
584
+ )
585
+ map_sizer.Add(msg, 0, wx.ALL, 8)
586
+ self.m_panel_map.SetSizer(map_sizer)
587
+
588
+ plots_sizer = wx.BoxSizer(wx.VERTICAL)
589
+ self.m_plot_panel = wx.Panel(self.m_panel_plots, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL)
590
+ plots_sizer.Add(self.m_plot_panel, 1, wx.ALL | wx.EXPAND, 5)
591
+ self.m_panel_plots.SetSizer(plots_sizer)
592
+
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)
597
+
598
+ bSizer3.Add(self.m_left_notebook, 1, wx.ALL | wx.EXPAND, 5)
599
+ bSizer1.Add( bSizer3, 1, wx.EXPAND, 5 )
600
+
601
+ cmd_choice_sizer = wx.BoxSizer(wx.HORIZONTAL)
602
+ self.m_staticText4 = wx.StaticText(self, wx.ID_ANY, _(u"Commands :"), wx.DefaultPosition, wx.DefaultSize, 0)
603
+ self.m_staticText4.Wrap(-1)
604
+ cmd_choice_sizer.Add(self.m_staticText4, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5)
605
+
606
+ self.m_choice_OperationMode = wx.Choice(
607
+ self,
608
+ choices=[
609
+ "DAQ Start",
610
+ "DAQ Stop",
611
+ "Realtime Stream Start",
612
+ "Realtime Stream Stop",
613
+ "SD Stream Start",
614
+ "SD Stream Stop",
615
+ "SD Clear All",
616
+ ],
617
+ )
618
+ cmd_choice_sizer.Add(self.m_choice_OperationMode, 0, wx.ALL | wx.CENTER, 5)
619
+ self.m_choice_OperationMode.SetSelection(0)
620
+
621
+ self.m_button1 = wx.Button(self, wx.ID_ANY, _(u"Send"), wx.DefaultPosition, wx.DefaultSize, 0)
622
+ cmd_choice_sizer.Add(self.m_button1, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5)
623
+
624
+ self.m_textCtrl_cmd = wx.TextCtrl(
625
+ self,
626
+ wx.ID_ANY,
627
+ _(u""),
628
+ wx.DefaultPosition,
629
+ wx.DefaultSize,
630
+ wx.TE_PROCESS_ENTER,
631
+ )
632
+ cmd_choice_sizer.Add(self.m_textCtrl_cmd, 1, wx.ALL | wx.EXPAND, 5)
633
+
634
+ bSizer1.Add(cmd_choice_sizer, 0, wx.EXPAND, 5)
635
+
636
+ bSizer1.Add(cmd_choice_sizer, 0, wx.EXPAND, 5)
637
+
638
+
639
+ self.SetSizer( bSizer1 )
640
+ self.Layout()
641
+
642
+
643
+ # MENU
644
+ self.m_statusBar1 = self.CreateStatusBar(1, wx.STB_SIZEGRIP, wx.ID_ANY)
645
+ self.m_menubar = wx.MenuBar(0)
646
+ self.m_menu_file = wx.Menu()
647
+ self.m_menu_file_open = wx.MenuItem(self.m_menu_file, wx.ID_ANY, _(u"&Open"), wx.EmptyString, wx.ITEM_NORMAL)
648
+ self.m_menu_file_save = wx.MenuItem(self.m_menu_file, wx.ID_ANY, _(u"&Save"), wx.EmptyString, wx.ITEM_NORMAL)
649
+ self.m_menu_file_quit = wx.MenuItem(self.m_menu_file, wx.ID_ANY, _(u"&Quit"), wx.EmptyString, wx.ITEM_NORMAL)
650
+ self.m_menu_file.Append(self.m_menu_file_open)
651
+ self.m_menu_file.Append(self.m_menu_file_save)
652
+ self.m_menu_file.AppendSeparator()
653
+ self.m_menu_file.Append(self.m_menu_file_quit)
654
+ self.m_menubar.Append(self.m_menu_file, _(u"&File"))
655
+
656
+ self.m_menu_data = wx.Menu()
657
+ self.m_menu_data_export = wx.MenuItem(self.m_menu_data, wx.ID_ANY, _(u"Process &RT Streamed Files"), wx.EmptyString, wx.ITEM_NORMAL)
658
+ self.m_menu_data.Append(self.m_menu_data_export)
659
+ self.m_menu_data_plot_rt = wx.MenuItem(
660
+ self.m_menu_data, wx.ID_ANY, _(u"Plot RT Streamed Data"), wx.EmptyString, wx.ITEM_NORMAL
661
+ )
662
+ self.m_menu_data.Append(self.m_menu_data_plot_rt)
663
+ self.m_menu_data.AppendSeparator()
664
+ self.m_menu_data_merge_sd = wx.MenuItem(
665
+ self.m_menu_data, wx.ID_ANY, _(u"Process SD Streamed Files"), wx.EmptyString, wx.ITEM_NORMAL
666
+ )
667
+ self.m_menu_data.Append(self.m_menu_data_merge_sd)
668
+ self.m_menu_data_plot_sd = wx.MenuItem(
669
+ self.m_menu_data, wx.ID_ANY, _(u"Plot SD Streamed Data"), wx.EmptyString, wx.ITEM_NORMAL
670
+ )
671
+ self.m_menu_data.Append(self.m_menu_data_plot_sd)
672
+ self.m_menu_data_clear = wx.Menu()
673
+ self.m_menu_data_clear_rt = wx.MenuItem(
674
+ self.m_menu_data_clear, wx.ID_ANY, _(u"Clear RT Streamed Files"), wx.EmptyString, wx.ITEM_NORMAL
675
+ )
676
+ self.m_menu_data_clear.Append(self.m_menu_data_clear_rt)
677
+ self.m_menu_data_clear_sd = wx.MenuItem(
678
+ self.m_menu_data_clear, wx.ID_ANY, _(u"Clear SD Streamed Files"), wx.EmptyString, wx.ITEM_NORMAL
679
+ )
680
+ self.m_menu_data_clear.Append(self.m_menu_data_clear_sd)
681
+ self.m_menu_data.AppendSubMenu(self.m_menu_data_clear, _(u"Clear"))
682
+ self.m_menubar.Append(self.m_menu_data, _(u"&Data"))
683
+
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
+ self.m_menu_help = wx.Menu()
725
+ self.m_menu_help_about = wx.MenuItem(self.m_menu_help, wx.ID_ANY, _(u"&Help"), wx.EmptyString, wx.ITEM_NORMAL)
726
+ self.m_menu_help.Append(self.m_menu_help_about)
727
+ self.m_menubar.Append(self.m_menu_help, _(u"&Help"))
728
+
729
+ self.SetMenuBar(self.m_menubar)
730
+ self.Centre( wx.BOTH )
731
+ self.Bind(wx.EVT_CHAR_HOOK, self.on_key_shortcut)
732
+
733
+ # FIGURE PANEL
734
+
735
+ self.m_fig = Plot(self.m_plot_panel)
736
+ self.axes1 = self.m_fig.figure.add_subplot(2, 1, 1)
737
+ self.axes2 = self.m_fig.figure.add_subplot(2, 1, 2)
738
+ self.m_fig.figure.subplots_adjust(hspace=0.28, bottom=0.08, top=0.95)
739
+ self.plot_updater = PlotUpdater(self)
740
+ self.plot_updater.init_plot()
741
+ 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
+ self.m_plot_canvas.Bind(wx.EVT_LEFT_DOWN, self._focus_plot_canvas)
746
+
747
+ self.m_fig_merged = Plot(self.m_plot_panel_merged)
748
+ self.axes1_merged = self.m_fig_merged.figure.add_subplot(2, 1, 1)
749
+ self.axes2_merged = self.m_fig_merged.figure.add_subplot(2, 1, 2)
750
+ self.m_fig_merged.figure.subplots_adjust(hspace=0.28, bottom=0.08, top=0.95)
751
+ self.merged_plot_updater = MergedPlotUpdater(self)
752
+ self.merged_plot_updater.init_plot()
753
+ 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
+ self.m_plot_canvas_merged.Bind(wx.EVT_LEFT_DOWN, self._focus_plot_canvas)
758
+
759
+ # UPDATE TIMERS
760
+
761
+ self._last_plot_version = 0
762
+ self._last_plot_log = 0.0
763
+ self.timer_gui = wx.Timer(self)
764
+ self.Bind(wx.EVT_TIMER, self.gui_update, self.timer_gui)
765
+ self.timer_gui.Start(GUI_TIMER_MS)
766
+
767
+ self.timer_pps = wx.Timer(self)
768
+ self.Bind(wx.EVT_TIMER, self.pps_update, self.timer_pps)
769
+ self.timer_pps.Start(PPS_AGE_UPDATE_MS)
770
+
771
+ self.timer_speed = wx.Timer(self)
772
+ self.Bind(wx.EVT_TIMER, self.speed_update, self.timer_speed)
773
+ self.timer_speed.Start(1000)
774
+
775
+ self.timer_gnss = wx.Timer(self)
776
+ self.Bind(wx.EVT_TIMER, self.map_update, self.timer_gnss)
777
+ self.timer_gnss.Start(1000)
778
+
779
+ if hasattr(self, "m_plot_canvas"):
780
+ self.m_plot_canvas.mpl_connect("scroll_event", self.on_mpl_scroll)
781
+ self.m_plot_canvas.mpl_connect("key_press_event", self.on_mpl_key)
782
+ self.m_plot_canvas.Bind(wx.EVT_KEY_DOWN, self._on_plot_key_down)
783
+ if hasattr(self, "m_plot_canvas_merged"):
784
+ self.m_plot_canvas_merged.mpl_connect("scroll_event", self.on_mpl_scroll)
785
+ self.m_plot_canvas_merged.mpl_connect("key_press_event", self.on_mpl_key)
786
+ self.m_plot_canvas_merged.Bind(wx.EVT_KEY_DOWN, self._on_plot_key_down)
787
+
788
+ self._plot_modes = {
789
+ self.m_plot_canvas: "zoom",
790
+ self.m_plot_canvas_merged: "zoom",
791
+ }
792
+ self._plot_toolbars = {
793
+ self.m_plot_canvas: self.m_fig.toolbar,
794
+ self.m_plot_canvas_merged: self.m_fig_merged.toolbar,
795
+ }
796
+ self.m_left_notebook.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.on_tab_changed)
797
+
798
+
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
+ def mesh_status_data_view(self):
808
+ if self.table_updater is None:
809
+ self.table_updater = TableUpdater(self)
810
+ self.table_updater.setup_table()
811
+ self.m_table = self.table_updater.table
812
+
813
+ def __del__( self ):
814
+ pass
815
+
816
+ def on_key_shortcut(self, event):
817
+ focus = wx.Window.FindFocus()
818
+ if focus in (
819
+ self.m_textCtrl_cmd,
820
+ self.m_textCtrl_acc,
821
+ self.m_textCtrl_tmp,
822
+ self.m_textCtrl_str,
823
+ self.m_textCtrl_veh,
824
+ getattr(self, "m_plot_canvas", None),
825
+ getattr(self, "m_plot_canvas_merged", None),
826
+ ):
827
+ event.Skip()
828
+ return
829
+ if event.HasModifiers():
830
+ event.Skip()
831
+ return
832
+ key = event.GetKeyCode()
833
+ if key in (ord('n'), ord('N')):
834
+ self.m_left_notebook.SetSelection(self.tab_index_nodes)
835
+ return
836
+ if key in (ord('m'), ord('M')):
837
+ self.m_left_notebook.SetSelection(self.tab_index_status)
838
+ return
839
+ if key in (ord('p'), ord('P')):
840
+ self.m_left_notebook.SetSelection(self.tab_index_rt_plots)
841
+ return
842
+ if key in (ord('c'), ord('C')):
843
+ self.m_choice_OperationMode.SetFocus()
844
+ if hasattr(self.m_choice_OperationMode, "Popup"):
845
+ self.m_choice_OperationMode.Popup()
846
+ return
847
+ if ord('1') <= key <= ord('9'):
848
+ idx = key - ord('1')
849
+ if idx < self.m_choice_OperationMode.GetCount():
850
+ self.m_choice_OperationMode.SetSelection(idx)
851
+ return
852
+ event.Skip()
853
+
854
+
855
+ # Virtual event handlers, override them in your derived class
856
+ def test( self, event ):
857
+ event.Skip()
858
+
859
+ # def init_plot(self, parent, id=-1, dpi=None, **kwargs):
860
+ # wx.Panel.__init__(parent)
861
+ # self.figure = Figure(dpi=dpi, figsize=(2, 2))
862
+ # self.canvas = FigureCanvas(self, -1, self.figure)
863
+ # self.toolbar = NavigationToolbar(self.canvas)
864
+ # self.toolbar.Realize()
865
+
866
+ # sizer = wx.BoxSizer(wx.VERTICAL)
867
+ # sizer.Add(self.canvas, 1, wx.EXPAND)
868
+ # sizer.Add(self.toolbar, 0, wx.LEFT | wx.EXPAND)
869
+ # self.SetSizer(sizer)
870
+
871
+ # return self.figure
872
+
873
+ def getTimeWindow(self, t1):
874
+ if self.model is not None:
875
+ return self.model.get_time_window(t1)
876
+ return []
877
+
878
+ def init_plot(self):
879
+ if self.plot_updater is None:
880
+ self.plot_updater = PlotUpdater(self)
881
+ self.plot_updater.init_plot()
882
+
883
+ def init_merged_plot(self):
884
+ if self.merged_plot_updater is None:
885
+ self.merged_plot_updater = MergedPlotUpdater(self)
886
+ self.merged_plot_updater.init_plot()
887
+
888
+ def on_merge_sd_files(self, event):
889
+ if self.model is None:
890
+ return
891
+ self.model.merge_sd_files()
892
+
893
+ def on_merge_rt_files(self, event):
894
+ if self.model is None:
895
+ return
896
+ self.model.merge_rt_streamed_files()
897
+
898
+ def figure_update_merged(self):
899
+ if self.merged_plot_updater is None:
900
+ return
901
+ self.merged_plot_updater.figure_update()
902
+
903
+ def show_merged_plots(self):
904
+ if hasattr(self, "tab_index_merged_plots"):
905
+ self.m_left_notebook.SetSelection(self.tab_index_merged_plots)
906
+
907
+ def _focus_plot_canvas(self, event):
908
+ try:
909
+ event.GetEventObject().SetFocus()
910
+ except Exception:
911
+ pass
912
+ event.Skip()
913
+
914
+ def on_mpl_scroll(self, event):
915
+ if event is None or event.inaxes is None:
916
+ return
917
+ if self._plot_modes.get(event.canvas, "zoom") != "zoom":
918
+ return
919
+ if event.button == "up":
920
+ scale = 0.9
921
+ elif event.button == "down":
922
+ scale = 1.1
923
+ else:
924
+ return
925
+
926
+ axes = event.inaxes
927
+ x0, x1 = axes.get_xlim()
928
+ if event.xdata is None or event.ydata is None:
929
+ return
930
+ cx = event.xdata
931
+ new_x0 = cx - (cx - x0) * scale
932
+ new_x1 = cx + (x1 - cx) * scale
933
+ axes.set_xlim(new_x0, new_x1)
934
+ try:
935
+ axes.figure.canvas.draw_idle()
936
+ except Exception:
937
+ pass
938
+
939
+ def _set_plot_mode(self, canvas, mode: str):
940
+ toolbar = self._plot_toolbars.get(canvas)
941
+ if toolbar is None:
942
+ return
943
+ if mode == "pan":
944
+ if toolbar.mode != "pan/zoom":
945
+ toolbar.pan()
946
+ elif mode == "zoom":
947
+ if toolbar.mode == "pan/zoom":
948
+ toolbar.pan()
949
+ self._plot_modes[canvas] = mode
950
+
951
+ def _reset_plot_view(self, canvas):
952
+ if canvas is self.m_plot_canvas:
953
+ self.plot_updater.init_plot()
954
+ self.figure_update()
955
+ elif canvas is self.m_plot_canvas_merged:
956
+ self.init_merged_plot()
957
+ self.figure_update_merged()
958
+
959
+ def on_mpl_key(self, event):
960
+ if event is None or event.canvas is None:
961
+ return
962
+ key = (event.key or "").lower()
963
+ if key == "p":
964
+ self._set_plot_mode(event.canvas, "pan")
965
+ return
966
+ if key == "z":
967
+ self._set_plot_mode(event.canvas, "zoom")
968
+ return
969
+ if key == "r":
970
+ self._reset_plot_view(event.canvas)
971
+ return
972
+
973
+ def _on_plot_key_down(self, event):
974
+ keycode = event.GetKeyCode()
975
+ canvas = event.GetEventObject()
976
+ if keycode in (ord('p'), ord('P')):
977
+ self._set_plot_mode(canvas, "pan")
978
+ return
979
+ if keycode in (ord('z'), ord('Z')):
980
+ self._set_plot_mode(canvas, "zoom")
981
+ return
982
+ if keycode in (ord('r'), ord('R')):
983
+ self._reset_plot_view(canvas)
984
+ return
985
+ event.Skip()
986
+
987
+ def on_tab_changed(self, event):
988
+ 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"):
995
+ try:
996
+ self.m_plot_canvas_merged.SetFocus()
997
+ except Exception:
998
+ pass
999
+ event.Skip()
1000
+
1001
+ def on_map_loaded(self, event):
1002
+ self.m_map_loaded = True
1003
+
1004
+ def map_update(self, event):
1005
+ if self.m_map is None or not self.m_map_loaded:
1006
+ return
1007
+ positions = getattr(self.model, "gnss_positions", {})
1008
+ if not positions:
1009
+ return
1010
+ for node_id, pos in positions.items():
1011
+ if not pos.get("valid"):
1012
+ continue
1013
+ lat = pos.get("lat")
1014
+ lon = pos.get("lon")
1015
+ if lat is None or lon is None:
1016
+ continue
1017
+ safe_id = str(node_id).replace("'", "\\'")
1018
+ self.m_map.RunScript(f"updateMarker('{safe_id}', {lat}, {lon});")
1019
+
1020
+ def _load_map(self):
1021
+ if self.m_map is None:
1022
+ return
1023
+ html = """
1024
+ <!DOCTYPE html>
1025
+ <html>
1026
+ <head>
1027
+ <meta charset="utf-8"/>
1028
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1029
+ <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
1030
+ integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
1031
+ <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
1032
+ integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
1033
+ <style>
1034
+ html, body, #map { height: 100%; margin: 0; padding: 0; }
1035
+ </style>
1036
+ </head>
1037
+ <body>
1038
+ <div id="map"></div>
1039
+ <script>
1040
+ const map = L.map('map').setView([0, 0], 16);
1041
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
1042
+ maxZoom: 19,
1043
+ attribution: '&copy; OpenStreetMap contributors'
1044
+ }).addTo(map);
1045
+ const markers = {};
1046
+ window.updateMarker = function(nodeId, lat, lon) {
1047
+ let m = markers[nodeId];
1048
+ if (!m) {
1049
+ m = L.marker([lat, lon]).addTo(map);
1050
+ m.bindTooltip(nodeId, {permanent: true, direction: 'top'});
1051
+ markers[nodeId] = m;
1052
+ }
1053
+ m.setLatLng([lat, lon]);
1054
+ };
1055
+ </script>
1056
+ </body>
1057
+ </html>
1058
+ """
1059
+ self.m_map.SetPage(html, "")
1060
+
1061
+ def status_table_update(self):
1062
+ if self.table_updater is None:
1063
+ return
1064
+ self.table_updater.update_status_table()
1065
+
1066
+ def update_pps_age(self):
1067
+ if self.table_updater is None:
1068
+ return
1069
+ self.table_updater.update_pps_age()
1070
+
1071
+ def table_update(self):
1072
+ if self.table_updater is None:
1073
+ return
1074
+ self.table_updater.refresh()
1075
+
1076
+ def figure_update(self):
1077
+ if self.plot_updater is None:
1078
+ return
1079
+ self.plot_updater.figure_update()
1080
+
1081
+ def gui_update(self, event):
1082
+ if self.model.gui_dirty:
1083
+ self.table_update()
1084
+ self.model.gui_dirty = False
1085
+ self.update_pps_flash()
1086
+ self.update_psd_legend()
1087
+ if self.model.plot_dirty_version != self._last_plot_version:
1088
+ self.figure_update()
1089
+ self._last_plot_version = self.model.plot_dirty_version
1090
+ self.model.plot_dirty = False
1091
+ # [gui] redraw logging disabled
1092
+
1093
+ def pps_update(self, event):
1094
+ self.update_pps_age()
1095
+
1096
+ def speed_update(self, event):
1097
+ self.model.update_speed(datetime.datetime.now(datetime.timezone.utc))
1098
+
1099
+ def update_pps_flash(self):
1100
+ if self.table_updater is None:
1101
+ return
1102
+ self.table_updater.update_pps_flash()
1103
+
1104
+ def update_psd_title(self):
1105
+ stats = self._psd_stats_text()
1106
+ if stats:
1107
+ self.axes2.set_title(stats)
1108
+ try:
1109
+ self.m_fig.figure.canvas.draw_idle()
1110
+ except Exception:
1111
+ pass
1112
+
1113
+ def _psd_stats_text(self):
1114
+ latest = None
1115
+ for node_id, psder in self.model.psder.items():
1116
+ ts = psder.last_sample_time
1117
+ if ts is None:
1118
+ continue
1119
+ if latest is None or ts > latest[0]:
1120
+ latest = (ts, psder)
1121
+ if latest is None:
1122
+ return ""
1123
+ _, psder = latest
1124
+ try:
1125
+ avg_n = max(psder.n - 1, 0)
1126
+ exp_lambda = math.exp(-psder.Lambda)
1127
+ return (
1128
+ f"λ={psder.Lambda:.4g}, exp(-λ)={exp_lambda:.4f}, avg_n={avg_n}"
1129
+ )
1130
+ except Exception:
1131
+ return ""
1132
+ return ""
1133
+
1134
+ def update_psd_legend(self):
1135
+ if self.plot_updater is None:
1136
+ return
1137
+ self.plot_updater.update_psd_legend()
1138
+
1139
+
1140
+ def onChannelToggled(self, event):
1141
+
1142
+ 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()
1146
+ ]
1147
+
1148
+ self.init_plot()
1149
+ self.figure_update()
1150
+ self.init_merged_plot()
1151
+ self.figure_update_merged()
1152
+
1153
+ def get_daq_mode_choices(self, model_label: str):
1154
+ if self.model is None:
1155
+ return []
1156
+ return self.model.daq_mode_labels.get(model_label, [])
1157
+
1158
+ def _parse_daq_mode(self, value: str):
1159
+ if not value:
1160
+ return None
1161
+ parts = value.split(":", 1)
1162
+ try:
1163
+ return int(parts[0].strip())
1164
+ except (ValueError, TypeError):
1165
+ return None
1166
+
1167
+ def on_daq_mode_change(self, event):
1168
+ col = event.GetCol()
1169
+ if self.model is None or self.table_updater is None:
1170
+ event.Skip()
1171
+ return
1172
+ daq_col = self.table_updater.col_index.get("DAQ Mode")
1173
+ if daq_col is None or col != daq_col:
1174
+ event.Skip()
1175
+ return
1176
+ row = event.GetRow()
1177
+ try:
1178
+ node_id = self.model.mesh_status_data.iloc[row]['nodeID']
1179
+ except Exception:
1180
+ event.Skip()
1181
+ return
1182
+ value = self.m_grid2.GetCellValue(row, col)
1183
+ mode = self._parse_daq_mode(value)
1184
+ if mode is None:
1185
+ event.Skip()
1186
+ return
1187
+ if hasattr(self.controller, "set_daq_mode"):
1188
+ self.controller.set_daq_mode(node_id, mode)
1189
+ event.Skip()
1190
+
1191
+ def on_grid_select_cell(self, event):
1192
+ if self.table_updater is None:
1193
+ event.Skip()
1194
+ return
1195
+ col = event.GetCol()
1196
+ daq_col = self.table_updater.col_index.get("DAQ Mode")
1197
+ if daq_col is None or col != daq_col:
1198
+ event.Skip()
1199
+ return
1200
+ row = event.GetRow()
1201
+ try:
1202
+ model_label = str(self.model.mesh_status_data.iloc[row]['Sensor'] or "")
1203
+ except Exception:
1204
+ event.Skip()
1205
+ return
1206
+ choices = self.get_daq_mode_choices(model_label)
1207
+ if choices:
1208
+ editor = gridlib.GridCellChoiceEditor(choices, allowOthers=False)
1209
+ self.m_grid2.SetCellEditor(row, daq_col, editor)
1210
+ event.Skip()
1211
+
1212
+ def onTimeSpanChange(self, event):
1213
+
1214
+ ts = self.timespan_map.get(event.GetId(), None)
1215
+ if ts:
1216
+ logger.info(f"Timespan selected: {ts} sec")
1217
+ self.timespan_length = ts
1218
+ if self.model is not None:
1219
+ self.model.timespan_length = ts
1220
+ self.init_plot()
1221
+ self.figure_update()
1222
+ self.init_merged_plot()
1223
+ self.figure_update_merged()
1224
+
1225
+ def append_status_message(self, msg: str) -> None:
1226
+ wx.CallAfter(self._append_status_message, msg)
1227
+
1228
+ def _append_status_message(self, msg: str) -> None:
1229
+ if hasattr(self, "m_statusBar1") and self.m_statusBar1 is not None:
1230
+ self.m_statusBar1.SetStatusText(msg)
1231
+ print(msg, flush=True)
1232
+
1233
+