plot_new.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  1. import sys
  2. import numpy as np
  3. import string
  4. from PySide6.QtWidgets import (
  5. QApplication, QMainWindow, QWidget, QDockWidget,
  6. QVBoxLayout, QHBoxLayout,
  7. QPushButton, QLabel, QLineEdit, QFileDialog,
  8. QListWidget, QListWidgetItem, QGroupBox, QMessageBox,
  9. QCheckBox
  10. )
  11. from PySide6.QtCore import Qt
  12. # Для встроенного тулбара matplotlib
  13. from matplotlib.backends.backend_qtagg import NavigationToolbar2QT as NavigationToolbar
  14. from scipy.io import savemat # <-- Импорт для .mat сохранения
  15. from mpl_canvas import MplCanvas
  16. from plotting import (
  17. plot_time_domain,
  18. plot_spectrum,
  19. plot_demodulated_time_domain
  20. )
  21. class MainWindow(QMainWindow):
  22. def __init__(self):
  23. super().__init__()
  24. self.csv_data = None
  25. self.demod_data = None
  26. self.demod_time_ns = None
  27. self.sampling_rate_ns = 13
  28. self.loaded_file_path = None
  29. self.init_ui()
  30. def init_ui(self):
  31. self.setWindowTitle("Demo: Signal / Spectrum / Demodulation plotting by Vladimir Pugovkin")
  32. central_widget = QWidget()
  33. self.setCentralWidget(central_widget)
  34. main_layout = QVBoxLayout(central_widget)
  35. # ------------ Первая строка с кнопками -----------
  36. top_bar_1 = QHBoxLayout()
  37. top_bar_1.setSpacing(8)
  38. self.load_btn = QPushButton("Load CSV")
  39. self.load_btn.setFixedWidth(130)
  40. self.load_btn.clicked.connect(self.load_csv)
  41. top_bar_1.addWidget(self.load_btn)
  42. # Чекбокс для bandpass
  43. self.use_bandpass_checkbox = QCheckBox("Bandpass Filter")
  44. top_bar_1.addWidget(self.use_bandpass_checkbox)
  45. bandpass_label_low = QLabel("Lower freq (MHz):")
  46. self.bandpass_low_input = QLineEdit("10")
  47. self.bandpass_low_input.setFixedWidth(60)
  48. bandpass_label_high = QLabel("Upper freq (MHz):")
  49. self.bandpass_high_input = QLineEdit("50")
  50. self.bandpass_high_input.setFixedWidth(60)
  51. top_bar_1.addWidget(bandpass_label_low)
  52. top_bar_1.addWidget(self.bandpass_low_input)
  53. top_bar_1.addWidget(bandpass_label_high)
  54. top_bar_1.addWidget(self.bandpass_high_input)
  55. top_bar_1.setSpacing(15)
  56. self.plot_time_btn = QPushButton("Plot Time Domain")
  57. self.plot_time_btn.setFixedWidth(180)
  58. self.plot_time_btn.clicked.connect(self.plot_time_domain)
  59. top_bar_1.addWidget(self.plot_time_btn)
  60. # --- Дополнительные поля для спектра ---
  61. freq_range_label = QLabel("Spectrum range (MHz):")
  62. self.freq_min_input = QLineEdit("-100")
  63. self.freq_min_input.setFixedWidth(60)
  64. self.freq_max_input = QLineEdit("100")
  65. self.freq_max_input.setFixedWidth(60)
  66. top_bar_1.addWidget(freq_range_label)
  67. top_bar_1.addWidget(self.freq_min_input)
  68. top_bar_1.addWidget(self.freq_max_input)
  69. # Убрали чекбокс "Shift spectrum to 0?" так как он не нужен
  70. # Изменили текст чекбокса "Plot demod data?" -> "Plot demod data"
  71. self.plot_demod_checkbox = QCheckBox("Plot demod data")
  72. top_bar_1.addWidget(self.plot_demod_checkbox)
  73. self.plot_spectrum_btn = QPushButton("Plot Spectrum")
  74. self.plot_spectrum_btn.setFixedWidth(180)
  75. self.plot_spectrum_btn.clicked.connect(self.plot_spectrum)
  76. top_bar_1.addWidget(self.plot_spectrum_btn)
  77. # ---------------- Кнопка экспорта в .mat -------------
  78. self.export_btn = QPushButton("Export to .mat")
  79. self.export_btn.setFixedWidth(180)
  80. self.export_btn.clicked.connect(self.export_data_to_mat)
  81. top_bar_1.addWidget(self.export_btn)
  82. top_bar_1.addStretch()
  83. main_layout.addLayout(top_bar_1)
  84. # ------------ Вторая строка с элементами -----------
  85. top_bar_2 = QHBoxLayout()
  86. top_bar_2.setSpacing(15)
  87. label_fc = QLabel("Central freq (MHz):")
  88. self.fc_input = QLineEdit("0")
  89. self.fc_input.setFixedWidth(60)
  90. label_st = QLabel("Start time (ns):")
  91. self.start_time_input = QLineEdit("0")
  92. self.start_time_input.setFixedWidth(70)
  93. label_end = QLabel("End time (ns):")
  94. self.end_time_input = QLineEdit("1000")
  95. self.end_time_input.setFixedWidth(70)
  96. label_dec = QLabel("Decimation factor:")
  97. self.decimation_factor_input = QLineEdit("4")
  98. self.decimation_factor_input.setFixedWidth(60)
  99. top_bar_2.addWidget(label_fc)
  100. top_bar_2.addWidget(self.fc_input)
  101. top_bar_2.addSpacing(5)
  102. top_bar_2.addWidget(label_st)
  103. top_bar_2.addWidget(self.start_time_input)
  104. top_bar_2.addWidget(label_end)
  105. top_bar_2.addWidget(self.end_time_input)
  106. top_bar_2.addSpacing(5)
  107. top_bar_2.addWidget(label_dec)
  108. top_bar_2.addWidget(self.decimation_factor_input)
  109. self.demod_dec_btn = QPushButton("Demodulation + Decimation")
  110. self.demod_dec_btn.setFixedWidth(300)
  111. self.demod_dec_btn.clicked.connect(self.demodulate_and_decimate)
  112. top_bar_2.addWidget(self.demod_dec_btn)
  113. top_bar_2.addStretch()
  114. main_layout.addLayout(top_bar_2)
  115. # ============ Блоки с графиками (Time, Spectrum, Demod) =============
  116. plot_row_1 = QHBoxLayout()
  117. # --- Группа "Time Domain" ---
  118. self.time_group = QGroupBox("Time Domain")
  119. time_layout = QVBoxLayout(self.time_group)
  120. self.time_canvas = MplCanvas(self, width=5, height=4)
  121. time_layout.addWidget(self.time_canvas)
  122. # Добавляем тулбар:
  123. self.time_toolbar = NavigationToolbar(self.time_canvas, self.time_group)
  124. time_layout.addWidget(self.time_toolbar)
  125. # --- Группа "Spectrum" ---
  126. self.freq_group = QGroupBox("Spectrum")
  127. freq_layout = QVBoxLayout(self.freq_group)
  128. self.freq_canvas = MplCanvas(self, width=5, height=4)
  129. freq_layout.addWidget(self.freq_canvas)
  130. self.freq_toolbar = NavigationToolbar(self.freq_canvas, self.freq_group)
  131. freq_layout.addWidget(self.freq_toolbar)
  132. plot_row_1.addWidget(self.time_group, stretch=1)
  133. plot_row_1.addWidget(self.freq_group, stretch=1)
  134. main_layout.addLayout(plot_row_1)
  135. # --- Вторая строка с графиком демодулированного сигнала ---
  136. plot_row_2 = QHBoxLayout()
  137. self.demod_group = QGroupBox("Demodulated Signal")
  138. demod_layout = QVBoxLayout(self.demod_group)
  139. self.demod_time_canvas = MplCanvas(self, width=5, height=3)
  140. demod_layout.addWidget(self.demod_time_canvas)
  141. self.demod_toolbar = NavigationToolbar(self.demod_time_canvas, self.demod_group)
  142. demod_layout.addWidget(self.demod_toolbar)
  143. plot_row_2.addWidget(self.demod_group, stretch=1)
  144. main_layout.addLayout(plot_row_2)
  145. self.demod_group.raise_()
  146. # ============ DockWidget для выбора каналов ============
  147. dock = QDockWidget("Channels", self)
  148. dock.setFeatures(
  149. QDockWidget.DockWidgetFloatable
  150. | QDockWidget.DockWidgetMovable
  151. | QDockWidget.DockWidgetVerticalTitleBar
  152. )
  153. self.channels_list = QListWidget()
  154. self.channels_list.setSelectionMode(QListWidget.MultiSelection)
  155. self.channels_list.setMaximumWidth(250)
  156. dock_widget = QWidget()
  157. vbox = QVBoxLayout(dock_widget)
  158. label_ch = QLabel("Select channels:")
  159. vbox.addWidget(label_ch)
  160. vbox.addWidget(self.channels_list)
  161. dock_widget.setLayout(vbox)
  162. dock.setWidget(dock_widget)
  163. self.addDockWidget(Qt.RightDockWidgetArea, dock)
  164. # ================== Стили ===================
  165. self.setStyleSheet("""
  166. QMainWindow {
  167. background-color: #FAFAFA;
  168. }
  169. QDockWidget {
  170. background-color: #F4F7F9;
  171. }
  172. QDockWidget::title {
  173. background-color: #34495E;
  174. color: white;
  175. padding: 8px;
  176. font-size: 16px;
  177. }
  178. QGroupBox {
  179. border: 2px solid #3498db;
  180. border-radius: 9px;
  181. margin-top: 30px;
  182. font-weight: bold;
  183. font-size: 20px;
  184. }
  185. QGroupBox::title {
  186. subcontrol-origin: margin;
  187. subcontrol-position: top center;
  188. padding: -5px 25px;
  189. font-size: 20px;
  190. }
  191. QPushButton {
  192. background-color: #3DAEE9;
  193. color: white;
  194. border-radius: 9px;
  195. padding: 6px 16px;
  196. font-size: 16px;
  197. }
  198. QPushButton:hover {
  199. background-color: #3592CC;
  200. }
  201. QPushButton:pressed {
  202. background-color: #2A7098;
  203. }
  204. QCheckBox {
  205. font-size: 18px;
  206. }
  207. QLineEdit {
  208. padding: 3px;
  209. font-size: 18px;
  210. }
  211. QLabel {
  212. font-size: 18px;
  213. }
  214. """)
  215. self.showMaximized()
  216. # ========== Загрузка CSV ==========
  217. def load_csv(self):
  218. file_dialog = QFileDialog(self, "Select .csv file", ".", "CSV Files (*.csv)")
  219. if file_dialog.exec():
  220. file_path = file_dialog.selectedFiles()[0]
  221. else:
  222. return
  223. if not file_path:
  224. return
  225. try:
  226. with open(file_path, 'r', encoding='utf-8') as f:
  227. cleaned_lines = []
  228. for line in f:
  229. line = line.strip()
  230. if line.endswith(','):
  231. line = line[:-1]
  232. cleaned_lines.append(line)
  233. loaded_array = np.loadtxt(cleaned_lines, delimiter=',')
  234. if loaded_array.ndim == 1:
  235. loaded_array = loaded_array.reshape(-1, 1)
  236. self.csv_data = loaded_array
  237. num_cols = self.csv_data.shape[1]
  238. self.channels_list.clear()
  239. for i in range(num_cols):
  240. channel_name = string.ascii_uppercase[i] if i < 26 else f"CH{i}"
  241. item = QListWidgetItem(f"Channel {channel_name}")
  242. if i == 0:
  243. item.setCheckState(Qt.Checked)
  244. else:
  245. item.setCheckState(Qt.Unchecked)
  246. self.channels_list.addItem(item)
  247. self.loaded_file_path = file_path
  248. QMessageBox.information(
  249. self, "File loaded", f"CSV file loaded:\n{file_path}"
  250. )
  251. except Exception as e:
  252. QMessageBox.warning(self, "Error loading", f"Could not load file:\n{e}")
  253. self.csv_data = None
  254. # ========== Определение интервала времени ==========
  255. def get_time_slice_indices(self):
  256. if self.csv_data is None:
  257. return None, None
  258. try:
  259. start_ns = float(self.start_time_input.text())
  260. end_ns = float(self.end_time_input.text())
  261. if end_ns <= start_ns:
  262. raise ValueError("end <= start")
  263. except ValueError:
  264. return None, None
  265. dt_ns = self.sampling_rate_ns
  266. start_idx = int(round(start_ns / dt_ns))
  267. end_idx = int(round(end_ns / dt_ns))
  268. if start_idx < 0:
  269. start_idx = 0
  270. if end_idx > self.csv_data.shape[0]:
  271. end_idx = self.csv_data.shape[0]
  272. if start_idx >= end_idx:
  273. return None, None
  274. return start_idx, end_idx
  275. # ========== Выбор каналов ==========
  276. def get_selected_channels(self):
  277. indices = []
  278. for i in range(self.channels_list.count()):
  279. item = self.channels_list.item(i)
  280. if item.checkState() == Qt.Checked:
  281. indices.append(i)
  282. return indices
  283. # ========== Полосовой фильтр через FFT ==========
  284. def bandpass_filter_fft(self, data, dt_s, f_low_hz, f_high_hz):
  285. N = data.shape[0]
  286. num_ch = data.shape[1]
  287. filtered = np.zeros_like(data, dtype=float)
  288. freqs = np.fft.fftfreq(N, d=dt_s)
  289. for ch in range(num_ch):
  290. y = data[:, ch]
  291. Y = np.fft.fft(y)
  292. # Создаём маску в нужном диапазоне
  293. mask = (np.abs(freqs) >= f_low_hz) & (np.abs(freqs) <= f_high_hz)
  294. Y[~mask] = 0.0
  295. y_filt = np.fft.ifft(Y)
  296. filtered[:, ch] = np.real(y_filt)
  297. return filtered
  298. # ========== Применить фильтр при необходимости ==========
  299. def apply_bandpass_if_needed(self, data, dt_s):
  300. out = data.copy()
  301. if self.use_bandpass_checkbox.isChecked():
  302. try:
  303. f_low_mhz = float(self.bandpass_low_input.text())
  304. f_high_mhz = float(self.bandpass_high_input.text())
  305. if f_high_mhz <= f_low_mhz:
  306. raise ValueError
  307. except ValueError:
  308. QMessageBox.warning(self, "Bandpass Error", "Incorrect bandpass frequencies.")
  309. else:
  310. f_low_hz = f_low_mhz * 1e6
  311. f_high_hz = f_high_mhz * 1e6
  312. out = self.bandpass_filter_fft(out, dt_s, f_low_hz, f_high_hz)
  313. return out
  314. # ========== Построение временной области ==========
  315. def plot_time_domain(self):
  316. if self.csv_data is None:
  317. QMessageBox.warning(self, "Error", "No CSV loaded.")
  318. return
  319. start_idx, end_idx = self.get_time_slice_indices()
  320. if start_idx is None or end_idx is None:
  321. QMessageBox.warning(self, "Error", "Invalid time interval.")
  322. return
  323. selected_channels = self.get_selected_channels()
  324. if not selected_channels:
  325. QMessageBox.warning(self, "Error", "No channels selected.")
  326. return
  327. raw_cropped = self.csv_data[start_idx:end_idx, :]
  328. # Перевод в вольты
  329. raw_cropped_v = raw_cropped / 32767.0 * 5.0
  330. dt_s = self.sampling_rate_ns * 1e-9
  331. data_for_plot = self.apply_bandpass_if_needed(raw_cropped_v, dt_s)
  332. self.time_canvas.ax.clear()
  333. time_axis_ns = np.arange(start_idx, end_idx) * self.sampling_rate_ns
  334. channel_labels = [
  335. f"Channel {string.ascii_uppercase[ch] if ch<26 else ch}"
  336. for ch in selected_channels
  337. ]
  338. plot_time_domain(
  339. axis=self.time_canvas.ax,
  340. time_axis_ns=time_axis_ns,
  341. data=data_for_plot[:, selected_channels],
  342. channels=channel_labels
  343. )
  344. self.time_canvas.draw()
  345. # ========== Построение спектра ==========
  346. def plot_spectrum(self):
  347. if self.csv_data is None:
  348. QMessageBox.warning(self, "Error", "No CSV loaded.")
  349. return
  350. try:
  351. frequency_mhz = float(self.fc_input.text())
  352. except ValueError:
  353. frequency_mhz = 0.0
  354. try:
  355. freq_min = float(self.freq_min_input.text())
  356. freq_max = float(self.freq_max_input.text())
  357. except ValueError:
  358. freq_min, freq_max = None, None
  359. start_idx, end_idx = self.get_time_slice_indices()
  360. if start_idx is None or end_idx is None:
  361. QMessageBox.warning(self, "Error", "Invalid time interval.")
  362. return
  363. selected_channels = self.get_selected_channels()
  364. if not selected_channels:
  365. QMessageBox.warning(self, "Error", "No channels selected.")
  366. return
  367. # Если включён режим построения спектра демодулированных данных
  368. if self.plot_demod_checkbox.isChecked():
  369. if self.demod_data is None or self.demod_time_ns is None:
  370. QMessageBox.warning(self, "Error", "No demodulated data. Please demodulate first.")
  371. return
  372. decimation_factor = self.get_decimation_factor()
  373. if not decimation_factor:
  374. return
  375. dt_s = (self.sampling_rate_ns * 1e-9) * decimation_factor
  376. data_for_fft = self.demod_data[:, selected_channels]
  377. n_win = data_for_fft.shape[0]
  378. freq_axis_mhz = np.fft.fftfreq(n_win, d=dt_s) / 1e6
  379. self.freq_canvas.ax.clear()
  380. channel_labels = [
  381. f"Channel {string.ascii_uppercase[ch] if ch<26 else ch}"
  382. for ch in selected_channels
  383. ]
  384. plot_spectrum(
  385. axis=self.freq_canvas.ax,
  386. frequency=freq_axis_mhz,
  387. data=data_for_fft,
  388. channels=channel_labels,
  389. middle_frequency=0.0 # демодулированные данные находятся в базовой полосе
  390. )
  391. if freq_min is not None and freq_max is not None and freq_max > freq_min:
  392. self.freq_canvas.ax.set_xlim([freq_min, freq_max])
  393. self.freq_canvas.draw()
  394. else:
  395. raw_cropped = self.csv_data[start_idx:end_idx, :]
  396. raw_cropped_v = raw_cropped / 32767.0 * 5.0
  397. dt_s = self.sampling_rate_ns * 1e-9
  398. data_for_fft = self.apply_bandpass_if_needed(raw_cropped_v, dt_s)
  399. n_win = data_for_fft.shape[0]
  400. freq_axis_mhz = np.fft.fftfreq(n_win, d=dt_s) / 1e6
  401. self.freq_canvas.ax.clear()
  402. channel_labels = [
  403. f"Channel {string.ascii_uppercase[ch] if ch<26 else ch}"
  404. for ch in selected_channels
  405. ]
  406. plot_spectrum(
  407. axis=self.freq_canvas.ax,
  408. frequency=freq_axis_mhz,
  409. data=data_for_fft[:, selected_channels],
  410. channels=channel_labels,
  411. middle_frequency=frequency_mhz # центральная частота берётся из поля
  412. )
  413. if freq_min is not None and freq_max is not None and freq_max > freq_min:
  414. self.freq_canvas.ax.set_xlim([freq_min, freq_max])
  415. self.freq_canvas.draw()
  416. def get_decimation_factor(self):
  417. try:
  418. decimation_factor = int(self.decimation_factor_input.text())
  419. if decimation_factor < 1:
  420. raise ValueError
  421. return decimation_factor
  422. except ValueError:
  423. QMessageBox.warning(self, "Error", "Invalid decimation factor.")
  424. return None
  425. # ========== Демодуляция и децимация ==========
  426. def demodulate_and_decimate(self):
  427. if self.csv_data is None:
  428. QMessageBox.warning(self, "Error", "No CSV loaded.")
  429. return
  430. try:
  431. frequency_mhz = float(self.fc_input.text())
  432. except ValueError:
  433. frequency_mhz = 0.0
  434. decimation_factor = self.get_decimation_factor()
  435. if not decimation_factor:
  436. return
  437. start_idx, end_idx = self.get_time_slice_indices()
  438. if start_idx is None or end_idx is None:
  439. QMessageBox.warning(self, "Error", "Invalid time interval.")
  440. return
  441. selected_channels = self.get_selected_channels()
  442. if not selected_channels:
  443. QMessageBox.warning(self, "Error", "Select at least one channel.")
  444. return
  445. cropped_data = self.csv_data[start_idx:end_idx, :]
  446. cropped_data_v = cropped_data / 32767.0 * 5.0
  447. dt_s = self.sampling_rate_ns * 1e-9
  448. data_for_demod = self.apply_bandpass_if_needed(cropped_data_v, dt_s)
  449. n_win = data_for_demod.shape[0]
  450. t_vec = np.arange(n_win) * dt_s
  451. fc_hz = frequency_mhz * 1e6
  452. # Демодуляция: перенос на нулевую частоту
  453. self.demod_data = np.zeros((n_win, len(selected_channels)), dtype=np.complex64)
  454. for idx, ch in enumerate(selected_channels):
  455. y = data_for_demod[:, ch]
  456. mixer = np.exp(-1j * 2 * np.pi * fc_hz * t_vec)
  457. self.demod_data[:, idx] = y * mixer
  458. # Децимация
  459. self.demod_data = self.demod_data[::decimation_factor, :]
  460. decimated_dt_s = dt_s * decimation_factor
  461. self.demod_time_ns = np.arange(self.demod_data.shape[0]) * decimated_dt_s * 1e9
  462. # Рисуем демодулированный сигнал (I-компонент)
  463. self.demod_time_canvas.ax.clear()
  464. channel_labels = [
  465. f"Channel {string.ascii_uppercase[ch] if ch<26 else ch}"
  466. for ch in selected_channels
  467. ]
  468. plot_demodulated_time_domain(
  469. ax=self.demod_time_canvas.ax,
  470. time_ns=self.demod_time_ns,
  471. demod_data=self.demod_data,
  472. selected_channels=channel_labels
  473. )
  474. self.demod_time_canvas.draw()
  475. QMessageBox.information(
  476. self, "Demodulation",
  477. f"Demod on {frequency_mhz} MHz + decim x{decimation_factor} done!"
  478. )
  479. # ========== Экспорт в .mat ==========
  480. def export_data_to_mat(self):
  481. if self.demod_data is None or self.demod_time_ns is None:
  482. QMessageBox.warning(self, "Error", "No demodulated data.")
  483. return
  484. file_dialog = QFileDialog(self, "Save as .mat", ".", "MAT files (*.mat)")
  485. file_dialog.setAcceptMode(QFileDialog.AcceptSave)
  486. file_dialog.setDefaultSuffix("mat")
  487. if file_dialog.exec():
  488. save_path = file_dialog.selectedFiles()[0]
  489. else:
  490. return
  491. if not save_path:
  492. return
  493. try:
  494. savemat(save_path, {
  495. 'time_ns': self.demod_time_ns,
  496. 'demod_data': self.demod_data
  497. })
  498. QMessageBox.information(self, "Export", f"Saved:\n{save_path}")
  499. except Exception as e:
  500. QMessageBox.warning(self, "Error saving", f"Could not save:\n{e}")
  501. def main():
  502. app = QApplication(sys.argv)
  503. window = MainWindow()
  504. window.show()
  505. sys.exit(app.exec())
  506. main()