sonarcal 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.
- sonarcal/__init__.py +0 -0
- sonarcal/__main__.py +4 -0
- sonarcal/calculate_gains.py +7 -0
- sonarcal/calibration_data.py +19 -0
- sonarcal/calibration_gui.py +317 -0
- sonarcal/controller.py +108 -0
- sonarcal/do_estimate_gains.py +363 -0
- sonarcal/echogram_plotter.py +382 -0
- sonarcal/file_ops.py +198 -0
- sonarcal/gui_utils.py +129 -0
- sonarcal/sonar_calibration.ini +11 -0
- sonarcal/sonar_calibration.toml +21 -0
- sonarcal/utils.py +183 -0
- sonarcal-0.1.0.dist-info/METADATA +81 -0
- sonarcal-0.1.0.dist-info/RECORD +18 -0
- sonarcal-0.1.0.dist-info/WHEEL +4 -0
- sonarcal-0.1.0.dist-info/entry_points.txt +2 -0
- sonarcal-0.1.0.dist-info/licenses/LICENSE +21 -0
sonarcal/__init__.py
ADDED
|
File without changes
|
sonarcal/__main__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
class calibrationData():
|
|
4
|
+
"""Storage for sonar caliration results."""
|
|
5
|
+
|
|
6
|
+
def __init__(self):
|
|
7
|
+
self.data = pd.DataFrame(columns=['Time', 'Gain (dB)', 'RMS (dB)', 'Range (m)', 'Echoes'])
|
|
8
|
+
self.data.index.name = 'Beam'
|
|
9
|
+
|
|
10
|
+
def update(self, beam: int, timestamp: str, gain: float, rms: float, r: float, num: int):
|
|
11
|
+
self.data.loc[beam] = (timestamp, gain, rms, r, num)
|
|
12
|
+
|
|
13
|
+
def remove(self, beams: list[int]):
|
|
14
|
+
"""Remove data for given beam."""
|
|
15
|
+
self.data.drop(index=beams, inplace=True)
|
|
16
|
+
|
|
17
|
+
def df(self):
|
|
18
|
+
return self.data # eventually return a better form of the data?
|
|
19
|
+
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
|
|
2
|
+
import webbrowser
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
import tkinter as tk
|
|
7
|
+
from tkinter import ttk
|
|
8
|
+
from tkinter import filedialog as fd
|
|
9
|
+
from tkinter import messagebox
|
|
10
|
+
from importlib.metadata import version
|
|
11
|
+
from PIL import Image, ImageTk
|
|
12
|
+
from .utils import window_closed, app_name
|
|
13
|
+
from .calibration_data import calibrationData
|
|
14
|
+
from .calculate_gains import calculate_gain
|
|
15
|
+
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
|
16
|
+
from platformdirs import PlatformDirs
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(app_name)
|
|
19
|
+
icon_file = Path(__file__).parent.parent.parent/'assets'/'icon.png' # TODO get via a config file
|
|
20
|
+
|
|
21
|
+
class calibrationGUI:
|
|
22
|
+
"""Provides the main GUI container and misc labels/buttons."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, echogram, title='TITLE', help_uri=None):
|
|
25
|
+
self.echogram = echogram
|
|
26
|
+
self.help_uri = help_uri
|
|
27
|
+
|
|
28
|
+
# Calibration gains are stored in here
|
|
29
|
+
self.cal_data = calibrationData()
|
|
30
|
+
# sphere ts for the current beam calibration is stored in here
|
|
31
|
+
self.sphere_ts = []
|
|
32
|
+
|
|
33
|
+
# The GUI window
|
|
34
|
+
self.echogram.root.title(title)
|
|
35
|
+
|
|
36
|
+
# Dialogs that we keep around
|
|
37
|
+
self.results_dialog = None
|
|
38
|
+
|
|
39
|
+
# The toolbar and window icon/logo
|
|
40
|
+
self.icon = ImageTk.PhotoImage(Image.open(icon_file))
|
|
41
|
+
self.echogram.root.iconphoto(False, self.icon)
|
|
42
|
+
|
|
43
|
+
# Things to do with new pings
|
|
44
|
+
self.echogram.set_ping_callback(self.new_ping)
|
|
45
|
+
|
|
46
|
+
# Put the matplotlib plots into the GUI window.
|
|
47
|
+
canvas = FigureCanvasTkAgg(self.echogram.fig, master=self.echogram.root)
|
|
48
|
+
canvas.get_tk_widget().pack(side='top', fill='both', expand=True, padx=5, pady=5)
|
|
49
|
+
|
|
50
|
+
# Styles. These apply to all widgets, not just the ones create in this function
|
|
51
|
+
s = ttk.Style()
|
|
52
|
+
s.configure('TButton', font=('Arial', 16))
|
|
53
|
+
s.configure('TLabel', font=('Arial', 12))
|
|
54
|
+
s.configure('TCheckbutton', font=('Arial', 16))
|
|
55
|
+
s.configure('Treeview.Heading', font=('Arial', 12, 'bold'))
|
|
56
|
+
s.configure('Treeview', font=('Arial', 12))
|
|
57
|
+
|
|
58
|
+
# A label to show the last received message time
|
|
59
|
+
self.label = ttk.Label(self.echogram.root)
|
|
60
|
+
self.label.pack(side=tk.TOP, fill=tk.BOTH)
|
|
61
|
+
self.label.config(text='Waiting for data...', width=100, anchor=tk.W)
|
|
62
|
+
|
|
63
|
+
ttk.Separator(self.echogram.root, orient='horizontal').pack(fill='x', padx=10, pady=5)
|
|
64
|
+
|
|
65
|
+
# Buttons for help, on-axis toggle, config dialog, and close
|
|
66
|
+
self.onaxis_value = tk.BooleanVar(value=False)
|
|
67
|
+
|
|
68
|
+
frame = ttk.Frame(self.echogram.root)
|
|
69
|
+
results = ttk.Button(frame, text='Results', command=self.results)
|
|
70
|
+
config = ttk.Button(frame, text='Config', command=self.config)
|
|
71
|
+
onaxis = ttk.Checkbutton(frame, text='On-axis', variable=self.onaxis_value,
|
|
72
|
+
command=self.onaxis_changed)
|
|
73
|
+
help = ttk.Button(frame, text='Help', command=self.help)
|
|
74
|
+
about = ttk.Button(frame, text='About', command=self.about)
|
|
75
|
+
close = ttk.Button(frame, text='Close', command=self.close)
|
|
76
|
+
|
|
77
|
+
onaxis.pack(side=tk.LEFT)
|
|
78
|
+
close.pack(side=tk.RIGHT)
|
|
79
|
+
about.pack(side=tk.RIGHT)
|
|
80
|
+
help.pack(side=tk.RIGHT)
|
|
81
|
+
config.pack(side=tk.RIGHT)
|
|
82
|
+
results.pack(side=tk.RIGHT)
|
|
83
|
+
|
|
84
|
+
frame.pack(side=tk.TOP, fill=tk.BOTH)
|
|
85
|
+
|
|
86
|
+
# Start listening for sonar data
|
|
87
|
+
self.echogram.newPing(self.status_label())
|
|
88
|
+
|
|
89
|
+
def job(self):
|
|
90
|
+
return self.echogram.job
|
|
91
|
+
|
|
92
|
+
def root(self):
|
|
93
|
+
return self.echogram.root
|
|
94
|
+
|
|
95
|
+
def onaxis_changed(self):
|
|
96
|
+
"""A beam calibration has either started or ended."""
|
|
97
|
+
if self.onaxis_value.get(): # start calibrating a beam
|
|
98
|
+
self.echogram.beamLine.freeze(True)
|
|
99
|
+
logger.info('Beam %d calibration started', self.echogram.beam)
|
|
100
|
+
else: # finished calibrating a beam
|
|
101
|
+
logger.info('Beam %d calibration complete', self.echogram.beam)
|
|
102
|
+
self.echogram.beamLine.freeze(False)
|
|
103
|
+
self.sphere_ts = []
|
|
104
|
+
if self.results_dialog:
|
|
105
|
+
self.results_dialog.update_rows(None) # unhighlights the previously active row
|
|
106
|
+
|
|
107
|
+
def new_ping(self):
|
|
108
|
+
"""Orchestrates things for each new ping."""
|
|
109
|
+
e = self.echogram
|
|
110
|
+
if e.beamLine.frozen(): # a beam is being calibrated
|
|
111
|
+
# store the current ping's sphere echo info
|
|
112
|
+
self.sphere_ts.append((datetime.now().isoformat(), e.amp[1, -1], e.rangeMax))
|
|
113
|
+
# calculate the beam gain and other stats
|
|
114
|
+
(gain, rms, r, num) = calculate_gain(self.sphere_ts)
|
|
115
|
+
# store the latest beam gain values
|
|
116
|
+
self.cal_data.update(e.beam, datetime.now().strftime('%H:%M:%S'), gain, rms, r, num)
|
|
117
|
+
# update the results dialog if present
|
|
118
|
+
if self.results_dialog:
|
|
119
|
+
self.results_dialog.update_with(self.cal_data, e.beam)
|
|
120
|
+
|
|
121
|
+
def about(self):
|
|
122
|
+
message = (f'Sonarcal, version {version("sonarcal")}\n\n'
|
|
123
|
+
'A program to assist with calibrating omni-directional sonars.\n\n'
|
|
124
|
+
'Developed by Aqualyd Ltd\n\n'
|
|
125
|
+
'www.aqualyd.nz')
|
|
126
|
+
|
|
127
|
+
messagebox.showinfo(title='About', message=message)
|
|
128
|
+
|
|
129
|
+
def close(self):
|
|
130
|
+
window_closed(self.echogram.root, self.echogram.job)
|
|
131
|
+
|
|
132
|
+
def results(self):
|
|
133
|
+
"""Open the Results dialog box."""
|
|
134
|
+
# want one lasting instance of this dialog so manage that here
|
|
135
|
+
if not self.results_dialog:
|
|
136
|
+
self.results_dialog = resultsDialog(self.echogram.root, self.cal_data, self.icon)
|
|
137
|
+
else:
|
|
138
|
+
self.results_dialog.reopen()
|
|
139
|
+
|
|
140
|
+
def help(self):
|
|
141
|
+
"""Open the help documentation in a web browser."""
|
|
142
|
+
if not webbrowser.open(self.help_uri, new=2):
|
|
143
|
+
logging.warning('Failed to start a webbrowser to show the help documentation')
|
|
144
|
+
|
|
145
|
+
def config(self):
|
|
146
|
+
"""Open the Config dialog box."""
|
|
147
|
+
configDialog(self.echogram.root, self.icon)
|
|
148
|
+
|
|
149
|
+
def status_label(self):
|
|
150
|
+
return self.label
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class configDialog:
|
|
154
|
+
"""A dialog box to set and change application parameters."""
|
|
155
|
+
def __init__(self, parent, icon=None):
|
|
156
|
+
self.top = tk.Toplevel(parent)
|
|
157
|
+
self.top.title("Config")
|
|
158
|
+
if icon:
|
|
159
|
+
self.top.iconphoto(False, icon)
|
|
160
|
+
|
|
161
|
+
ttk.Label(self.top, text="Configs").pack(padx=20, pady=10)
|
|
162
|
+
ttk.Button(self.top, text="Close", command=self.close_dialog).pack(pady=5)
|
|
163
|
+
ttk.Button(self.top, text="Apply", command=self.apply).pack(pady=5)
|
|
164
|
+
|
|
165
|
+
def apply(self):
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
def close_dialog(self):
|
|
169
|
+
self.top.destroy()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class resultsDialog:
|
|
173
|
+
"""A dialog box to show completed calibration results per beam."""
|
|
174
|
+
|
|
175
|
+
def __init__(self, parent, data: dict=None, icon=None):
|
|
176
|
+
|
|
177
|
+
self.data = data
|
|
178
|
+
|
|
179
|
+
self.top = tk.Toplevel(parent)
|
|
180
|
+
self.top.title("Results")
|
|
181
|
+
if icon:
|
|
182
|
+
self.top.iconphoto(False, icon)
|
|
183
|
+
|
|
184
|
+
tree_frame = ttk.Frame(self.top)
|
|
185
|
+
|
|
186
|
+
# use a ttk.Treeview to show a table of the results
|
|
187
|
+
self.item_ids = {} # contains ids to rows that get added to the treeview
|
|
188
|
+
self.setup_treeview(tree_frame, self.data) # creates self.tree
|
|
189
|
+
|
|
190
|
+
# Make scrollbars for the treeview widget
|
|
191
|
+
vsb = ttk.Scrollbar(tree_frame, orient="vertical", command=self.tree.yview)
|
|
192
|
+
self.tree.configure(yscrollcommand=vsb.set)
|
|
193
|
+
|
|
194
|
+
# pack the scrollbars and treeview
|
|
195
|
+
vsb.pack(side="right", fill="y")
|
|
196
|
+
self.tree.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=5)
|
|
197
|
+
|
|
198
|
+
# create and pack the buttons
|
|
199
|
+
btn_frame = ttk.Frame(self.top)
|
|
200
|
+
remove = ttk.Button(btn_frame, text="Remove selected", command=self.remove_rows)
|
|
201
|
+
save = ttk.Button(btn_frame, text="Save", command=self.save)
|
|
202
|
+
close = ttk.Button(btn_frame, text="Close", command=self.close_dialog)
|
|
203
|
+
|
|
204
|
+
close.pack(side=tk.RIGHT)
|
|
205
|
+
save.pack(side=tk.RIGHT)
|
|
206
|
+
remove.pack(side=tk.RIGHT)
|
|
207
|
+
|
|
208
|
+
tree_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
|
|
209
|
+
btn_frame.pack(side=tk.BOTTOM, fill=tk.X)
|
|
210
|
+
|
|
211
|
+
def setup_treeview(self, top, data):
|
|
212
|
+
"""Create the treeview columns, etc."""
|
|
213
|
+
|
|
214
|
+
headings = [data.df().index.name] + list(data.df().columns)
|
|
215
|
+
|
|
216
|
+
self.tree = ttk.Treeview(top, columns=headings, show='headings')
|
|
217
|
+
self.tree.bind('<Button-1>', self.on_row_click)
|
|
218
|
+
|
|
219
|
+
# colour odd and even rows
|
|
220
|
+
self.tree.tag_configure('evenrow', background='white smoke')
|
|
221
|
+
self.tree.tag_configure('oddrow', background='white')
|
|
222
|
+
self.tree.tag_configure('active', background='orange2')
|
|
223
|
+
|
|
224
|
+
for col in headings:
|
|
225
|
+
self.tree.heading(col, text=col, anchor='e')
|
|
226
|
+
self.tree.column(col, width=100, anchor='e')
|
|
227
|
+
|
|
228
|
+
# Add rows (if any)
|
|
229
|
+
self.update_with(data)
|
|
230
|
+
|
|
231
|
+
def update_with(self, data, active_beam: int|None = None):
|
|
232
|
+
"""Update dialog's display with given calibration data."""
|
|
233
|
+
|
|
234
|
+
for beam, row in data.df().iterrows():
|
|
235
|
+
|
|
236
|
+
# TODO
|
|
237
|
+
# This is hacky and fragile - find a better way that doesn't need to name the
|
|
238
|
+
# columns
|
|
239
|
+
values = [beam,
|
|
240
|
+
row['Time'],
|
|
241
|
+
f"{row['Gain (dB)']:0.1f}",
|
|
242
|
+
f"{row['RMS (dB)']:0.1f}",
|
|
243
|
+
f"{row['Range (m)']:0.1f}",
|
|
244
|
+
row['Echoes']]
|
|
245
|
+
# values = [beam] + list(row)
|
|
246
|
+
|
|
247
|
+
if beam in self.item_ids:
|
|
248
|
+
# row for this beam already exists, so update it
|
|
249
|
+
self.tree.item(self.item_ids[beam], values=values)
|
|
250
|
+
else:
|
|
251
|
+
# new beam, so add a row
|
|
252
|
+
item_id = self.tree.insert('', 'end', values=values)
|
|
253
|
+
self.item_ids[beam] = item_id
|
|
254
|
+
|
|
255
|
+
self.update_rows(active_beam)
|
|
256
|
+
|
|
257
|
+
# keep this to use in the save and remove functionalities
|
|
258
|
+
self.data = data
|
|
259
|
+
|
|
260
|
+
def update_rows(self, active_beam: int|None):
|
|
261
|
+
"""Sorts calibration results rows by beam and sets background colours to look nice."""
|
|
262
|
+
for beam, index in zip(sorted(self.item_ids.keys()), range(len(self.item_ids))):
|
|
263
|
+
self.tree.move(self.item_ids[beam], '', index)
|
|
264
|
+
|
|
265
|
+
rowness = 'oddrow' if index % 2 == 0 else 'evenrow'
|
|
266
|
+
if active_beam and beam == active_beam:
|
|
267
|
+
rowness = 'active'
|
|
268
|
+
|
|
269
|
+
self.tree.item(self.item_ids[beam], tags=(rowness,))
|
|
270
|
+
|
|
271
|
+
def remove_rows(self):
|
|
272
|
+
"""Remove selected rows from the results table."""
|
|
273
|
+
|
|
274
|
+
selected = self.tree.selection() # returns a tuple of item_ids
|
|
275
|
+
|
|
276
|
+
# Pick out the beam numbers of the selected rows
|
|
277
|
+
to_remove = []
|
|
278
|
+
for iid in selected:
|
|
279
|
+
item_data = self.tree.item(iid)
|
|
280
|
+
values = item_data['values']
|
|
281
|
+
to_remove.append(values[0])
|
|
282
|
+
|
|
283
|
+
if to_remove:
|
|
284
|
+
for beam in to_remove:
|
|
285
|
+
# remove selected rows from treeview
|
|
286
|
+
self.tree.delete(self.item_ids[beam])
|
|
287
|
+
# remove beam imtem from the map between beam and item ids
|
|
288
|
+
self.item_ids.pop(beam)
|
|
289
|
+
# remove beams from the cal_data store
|
|
290
|
+
self.data.remove(to_remove)
|
|
291
|
+
self.update_rows(None)
|
|
292
|
+
|
|
293
|
+
def save(self):
|
|
294
|
+
"""Save the results to a file."""
|
|
295
|
+
timestamp = datetime.now().strftime('%Y%m%dT%H%M%S')
|
|
296
|
+
default_filename = 'sonar_calibration_' + timestamp + '.csv'
|
|
297
|
+
save_filename = fd.asksaveasfilename(title='Save as CSV', defaultextension='.csv',
|
|
298
|
+
initialdir=PlatformDirs.user_documents_dir,
|
|
299
|
+
initialfile=default_filename,
|
|
300
|
+
filetypes=[('CSV', '*.csv')])
|
|
301
|
+
if save_filename:
|
|
302
|
+
logger.info('Saved results to %s', save_filename)
|
|
303
|
+
self.data.df().to_csv(save_filename)
|
|
304
|
+
|
|
305
|
+
def on_row_click(self, event):
|
|
306
|
+
"""Implement selection and deselection."""
|
|
307
|
+
# TODO - work out why it can take multiple clicks on a row to get it unselected
|
|
308
|
+
item_id = self.tree.identify_row(event.y)
|
|
309
|
+
if item_id and item_id in self.tree.selection():
|
|
310
|
+
self.tree.selection_remove(item_id)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def reopen(self):
|
|
314
|
+
self.top.deiconify()
|
|
315
|
+
|
|
316
|
+
def close_dialog(self):
|
|
317
|
+
self.top.withdraw()
|
sonarcal/controller.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Omnisonar calibration program
|
|
2
|
+
|
|
3
|
+
Provides omni and echogram displays and sphere amplitude plots for use when
|
|
4
|
+
calibrating omni-directional sonars.
|
|
5
|
+
"""
|
|
6
|
+
# TODO:
|
|
7
|
+
# Choose beam_group based on beam type rather than requiring it in the config file
|
|
8
|
+
|
|
9
|
+
import configparser
|
|
10
|
+
import tkinter as tk
|
|
11
|
+
from functools import partial
|
|
12
|
+
import threading
|
|
13
|
+
import queue
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from platformdirs import PlatformDirs
|
|
18
|
+
|
|
19
|
+
from .echogram_plotter import echogramPlotter
|
|
20
|
+
from .utils import setupLogging, app_name, on_exit, window_closed
|
|
21
|
+
from .file_ops import file_listen, file_replay
|
|
22
|
+
from .calibration_gui import calibrationGUI
|
|
23
|
+
|
|
24
|
+
if sys.platform == "win32":
|
|
25
|
+
import win32api
|
|
26
|
+
|
|
27
|
+
# Configure logging
|
|
28
|
+
dirs = PlatformDirs(appname=app_name, appauthor="Aqualyd")
|
|
29
|
+
log_dir = Path(dirs.user_log_dir)
|
|
30
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
setupLogging(log_dir, app_name)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def main():
|
|
35
|
+
"""Omnisonar calibration graphical user interface."""
|
|
36
|
+
##########################################
|
|
37
|
+
# Sort out the configuration file
|
|
38
|
+
config_filename = Path(dirs.user_config_dir)/'config.ini'
|
|
39
|
+
config_filename.parent.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
|
|
41
|
+
config = configparser.ConfigParser()
|
|
42
|
+
c = config.read(config_filename, encoding='utf8')
|
|
43
|
+
|
|
44
|
+
if not c: # config file not found, so make one
|
|
45
|
+
config['DEFAULT'] = {'numPingsToShow': 100,
|
|
46
|
+
'maxRange': 50,
|
|
47
|
+
'maxSv': -20,
|
|
48
|
+
'minSv': -60,
|
|
49
|
+
'replayRate': 'realtime',
|
|
50
|
+
'horizontalBeamGroupPath': 'Sonar/Beam_group1',
|
|
51
|
+
'watchDir': '.',
|
|
52
|
+
'liveData': 'yes',
|
|
53
|
+
'helpURI': 'https://aqualyd-limited.github.io/sonarCal/'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
with open(config_filename, 'w', encoding='utf-8') as configfile:
|
|
57
|
+
config.write(configfile)
|
|
58
|
+
# TODO - open config dialog instead of exitting here
|
|
59
|
+
print('No config file was found, so ' + str(config_filename) +
|
|
60
|
+
' was created. You may need to edit this file.')
|
|
61
|
+
sys.exit()
|
|
62
|
+
|
|
63
|
+
# Pull out the settings in the config file.
|
|
64
|
+
numPings = config.getint('DEFAULT', 'numPingsToShow')
|
|
65
|
+
maxRange = config.getfloat('DEFAULT', 'maxRange')
|
|
66
|
+
maxSv = config.getfloat('DEFAULT', 'maxSv')
|
|
67
|
+
minSv = config.getfloat('DEFAULT', 'minSv')
|
|
68
|
+
replayRate = config.get('DEFAULT', 'replayRate')
|
|
69
|
+
horizontalBeamGroup = config.get('DEFAULT', 'horizontalBeamGroupPath')
|
|
70
|
+
watchDir = Path(config.get('DEFAULT', 'watchDir'))
|
|
71
|
+
liveData = config.getboolean('DEFAULT', 'liveData')
|
|
72
|
+
helpURI = config.get('DEFAULT', 'helpURI')
|
|
73
|
+
|
|
74
|
+
##########################################
|
|
75
|
+
# Start things...
|
|
76
|
+
|
|
77
|
+
# queue to communicate between two threads
|
|
78
|
+
msg_queue = queue.Queue()
|
|
79
|
+
|
|
80
|
+
# Tk GUI
|
|
81
|
+
root = tk.Tk()
|
|
82
|
+
|
|
83
|
+
# handle to the function that does the echogram drawing
|
|
84
|
+
# job = None
|
|
85
|
+
|
|
86
|
+
echogram = echogramPlotter(numPings, maxRange, maxSv, minSv, msg_queue, root)
|
|
87
|
+
gui = calibrationGUI(echogram, title='Sonar calibration', help_uri=helpURI)
|
|
88
|
+
# Check periodically for new echogram data
|
|
89
|
+
# job = root.after(echogram.checkQueueInterval, echogram.newPing, gui.status_label())
|
|
90
|
+
|
|
91
|
+
# Start receive in a separate thread
|
|
92
|
+
if liveData:
|
|
93
|
+
t = threading.Thread(target=file_listen, args=(watchDir, horizontalBeamGroup, msg_queue))
|
|
94
|
+
else:
|
|
95
|
+
t = threading.Thread(target=file_replay, args=(watchDir, horizontalBeamGroup, msg_queue,
|
|
96
|
+
replayRate))
|
|
97
|
+
t.daemon = True # makes the thread close when main() ends
|
|
98
|
+
t.start()
|
|
99
|
+
|
|
100
|
+
# For Windows, catch when the console is closed
|
|
101
|
+
if sys.platform == "win32":
|
|
102
|
+
win32api.SetConsoleCtrlHandler(partial(on_exit, gui.root(), gui.job()), True)
|
|
103
|
+
|
|
104
|
+
# And start things...
|
|
105
|
+
root.protocol("WM_DELETE_WINDOW", lambda: window_closed(gui.root(), gui.job()))
|
|
106
|
+
root.mainloop()
|
|
107
|
+
|
|
108
|
+
|