calibrate-suite 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.
Files changed (47) hide show
  1. calibrate_suite-0.1.0.dist-info/METADATA +761 -0
  2. calibrate_suite-0.1.0.dist-info/RECORD +47 -0
  3. calibrate_suite-0.1.0.dist-info/WHEEL +5 -0
  4. calibrate_suite-0.1.0.dist-info/entry_points.txt +3 -0
  5. calibrate_suite-0.1.0.dist-info/licenses/LICENSE +201 -0
  6. calibrate_suite-0.1.0.dist-info/top_level.txt +4 -0
  7. fleet_server/__init__.py +32 -0
  8. fleet_server/app.py +377 -0
  9. fleet_server/config.py +91 -0
  10. fleet_server/templates/error.html +57 -0
  11. fleet_server/templates/index.html +137 -0
  12. fleet_server/templates/viewer.html +490 -0
  13. fleet_server/utils.py +178 -0
  14. gui/__init__.py +2 -0
  15. gui/assets/2d-or-3d-fleet-upload.png +0 -0
  16. gui/assets/2d_3d_overlay_output.jpg +0 -0
  17. gui/assets/3d-or-2d-overlay_page.png +0 -0
  18. gui/assets/3d-or-2d-record-page.png +0 -0
  19. gui/assets/3d_3d_overlay_output.png +0 -0
  20. gui/assets/3d_or_2d_calibrate-page.png +0 -0
  21. gui/assets/GUI_homepage.png +0 -0
  22. gui/assets/hardware_setup.jpeg +0 -0
  23. gui/assets/single_lidar_calibrate_page.png +0 -0
  24. gui/assets/single_lidar_output.png +0 -0
  25. gui/assets/single_lidar_record_page.png +0 -0
  26. gui/assets/virya.jpg +0 -0
  27. gui/main.py +23 -0
  28. gui/widgets/calibrator_widget.py +977 -0
  29. gui/widgets/extractor_widget.py +561 -0
  30. gui/widgets/home_widget.py +117 -0
  31. gui/widgets/recorder_widget.py +127 -0
  32. gui/widgets/single_lidar_widget.py +673 -0
  33. gui/widgets/three_d_calib_widget.py +87 -0
  34. gui/widgets/two_d_calib_widget.py +86 -0
  35. gui/widgets/uploader_widget.py +151 -0
  36. gui/widgets/validator_widget.py +614 -0
  37. gui/windows/main_window.py +56 -0
  38. gui/windows/main_window_ui.py +65 -0
  39. rviz_configs/2D-3D.rviz +183 -0
  40. rviz_configs/3D-3D.rviz +184 -0
  41. rviz_configs/default_calib.rviz +167 -0
  42. utils/__init__.py +13 -0
  43. utils/calibration_common.py +23 -0
  44. utils/cli_calibrate.py +53 -0
  45. utils/cli_fleet_server.py +64 -0
  46. utils/data_extractor_common.py +87 -0
  47. utils/gui_helpers.py +25 -0
@@ -0,0 +1,673 @@
1
+ from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
2
+ QPushButton, QStackedWidget, QFileDialog,
3
+ QLineEdit, QSpinBox, QMessageBox, QGroupBox, QTextEdit,
4
+ QComboBox, QRadioButton, QButtonGroup, QCheckBox, QSizePolicy,
5
+ QListWidget, QListWidgetItem, QAbstractItemView, QTabWidget)
6
+ from PyQt5.QtCore import Qt, QThread, pyqtSignal, QProcess
7
+ import os
8
+ import json
9
+ import subprocess
10
+ import glob
11
+
12
+ # --- Widget ---
13
+ class SingleLidarWidget(QWidget):
14
+ go_home = pyqtSignal() # Signal to return to main menu
15
+
16
+ def __init__(self):
17
+ super().__init__()
18
+ self.calib_process = None
19
+ self.record_process = None
20
+ self.diag_process = None
21
+ self.initUI()
22
+
23
+ def initUI(self):
24
+ # Apply dark style to inputs for legibility
25
+ self.setStyleSheet("""
26
+ QLineEdit, QComboBox, QListWidget, QSpinBox, QDoubleSpinBox {
27
+ background-color: #404040;
28
+ color: #ffffff;
29
+ border: 1px solid #666;
30
+ padding: 5px;
31
+ }
32
+ QLineEdit:disabled, QComboBox:disabled, QListWidget:disabled {
33
+ background-color: #2a2a2a;
34
+ color: #888;
35
+ }
36
+ QMessageBox {
37
+ background-color: #2b2b2b;
38
+ color: #ffffff;
39
+ }
40
+ QMessageBox QLabel {
41
+ color: #ffffff;
42
+ }
43
+ QComboBox QAbstractItemView {
44
+ background-color: #404040;
45
+ color: #ffffff;
46
+ selection-background-color: #4CAF50;
47
+ selection-color: white;
48
+ border: 1px solid #666;
49
+ }
50
+ QListWidget::item {
51
+ padding: 5px;
52
+ }
53
+ QListWidget::item:selected {
54
+ background-color: #4CAF50;
55
+ color: white;
56
+ }
57
+ /* Removed global QPushButton style to allow buttons to retain default or specific colors */
58
+ /* Removed global QPushButton style */
59
+ """)
60
+
61
+ main_layout = QVBoxLayout()
62
+
63
+ # Header (Home Button)
64
+ header = QHBoxLayout()
65
+ btn_back = QPushButton("← Home")
66
+ btn_back.clicked.connect(self.go_home.emit)
67
+ btn_back.setFixedWidth(100)
68
+ btn_back.setStyleSheet("""
69
+ QPushButton { background-color: #2E8B57; border-radius: 8px; color: white; }
70
+ QPushButton:hover { background-color: #689F38; }
71
+ """)
72
+ header.addWidget(btn_back)
73
+
74
+ title = QLabel("Single LiDAR Calibration")
75
+ title.setStyleSheet("font-size: 20px; font-weight: bold; color: #4CAF50;")
76
+ title.setAlignment(Qt.AlignCenter)
77
+ header.addWidget(title)
78
+ header.addStretch()
79
+ main_layout.addLayout(header)
80
+
81
+ # Tabs
82
+ self.tabs = QTabWidget()
83
+ self.tabs.setStyleSheet("""
84
+ QTabBar::tab {
85
+ min-width: 150px;
86
+ padding: 10px;
87
+ font-weight: bold;
88
+ }
89
+ QTabBar::tab:selected {
90
+ background-color: #2E8B57;
91
+ color: white;
92
+ }
93
+ """)
94
+
95
+ # Tab 1: Recorder
96
+ self.tab_record = self.create_recorder_ui()
97
+ self.tabs.addTab(self.tab_record, "Record / Capture Data")
98
+
99
+ # Tab 2: Calibration
100
+ self.tab_files = self.create_file_ui()
101
+ self.tabs.addTab(self.tab_files, "Calibrate from Files")
102
+
103
+ main_layout.addWidget(self.tabs)
104
+ self.setLayout(main_layout)
105
+
106
+
107
+
108
+ def create_recorder_ui(self):
109
+ w = QWidget()
110
+ layout = QVBoxLayout()
111
+ layout.setSpacing(6) # Adjusted to 5px (+1)
112
+ layout.setContentsMargins(5, 5, 5, 5)
113
+
114
+ # Reduced top margin stye for GroupBoxes to save vertical space
115
+ group_style = """
116
+ QGroupBox {
117
+ font-weight: bold;
118
+ border: 1px solid #555;
119
+ border-radius: 5px;
120
+ margin-top: 7px; /* Compact top margin */
121
+ padding-top: 5px;
122
+ }
123
+ QGroupBox::title {
124
+ subcontrol-origin: margin;
125
+ left: 10px;
126
+ padding: 0 3px 0 3px;
127
+ }
128
+ """
129
+
130
+ # 1. Source Type
131
+ type_group = QGroupBox("Input Type")
132
+ type_group.setStyleSheet(group_style)
133
+ type_layout = QHBoxLayout()
134
+ type_layout.setContentsMargins(5, 5, 5, 5) # Slight internal padding
135
+ self.rb_live = QRadioButton("Live Stream (ROS2)")
136
+ self.rb_bag = QRadioButton("Rosbag Recording")
137
+ self.rb_live.setChecked(True)
138
+ self.rb_live.toggled.connect(self.update_rec_ui_state)
139
+ type_layout.addWidget(self.rb_live)
140
+ type_layout.addWidget(self.rb_bag)
141
+ type_group.setLayout(type_layout)
142
+ layout.addWidget(type_group, 0) # No stretch
143
+
144
+ # 2. Configuration
145
+ self.config_stack = QStackedWidget()
146
+ # No fixed height, let it size naturally but minimally
147
+
148
+ # Live
149
+ live_w = QWidget()
150
+ live_l = QHBoxLayout()
151
+ live_l.setContentsMargins(0, 0, 0, 0)
152
+
153
+ # MULTI-SELECTION LIST
154
+ self.topic_list = QListWidget()
155
+ self.topic_list.setSelectionMode(QAbstractItemView.NoSelection) # We use checkboxes
156
+ self.topic_list.setFixedHeight(80) # Fixed height to not consume too much space
157
+
158
+ btn_refresh = QPushButton("šŸ”„ Scan Topics")
159
+ btn_refresh.setFixedWidth(150)
160
+ btn_refresh.clicked.connect(self.scan_topics)
161
+
162
+ live_l.addWidget(QLabel("LiDAR Topics:"))
163
+ live_l.addWidget(self.topic_list, 1)
164
+ live_l.addWidget(btn_refresh)
165
+ live_w.setLayout(live_l)
166
+
167
+ # Bag
168
+ bag_w = QWidget()
169
+ bag_l = QVBoxLayout()
170
+ bag_l.setContentsMargins(1, 1, 1, 1)
171
+
172
+ # Row 1: Folder
173
+ row_folder = QHBoxLayout()
174
+ self.bag_path = QLineEdit()
175
+ self.bag_path.setPlaceholderText("Path to Rosbag Folder")
176
+ btn_browse_bag = QPushButton("Browse")
177
+ btn_browse_bag.clicked.connect(self.browse_bag)
178
+ self.cb_loop = QCheckBox("Loop")
179
+ self.cb_loop.setChecked(True)
180
+
181
+ row_folder.addWidget(QLabel("Folder:"))
182
+ row_folder.addWidget(self.bag_path, 1)
183
+ row_folder.addWidget(btn_browse_bag)
184
+ row_folder.addWidget(self.cb_loop)
185
+ bag_l.addLayout(row_folder)
186
+
187
+ # Row 2: Topic Selection (List Widget style)
188
+ row_topic = QHBoxLayout()
189
+ row_topic.setContentsMargins(0, 0, 0, 0)
190
+
191
+ self.bag_topic_list = QListWidget()
192
+ self.bag_topic_list.setSelectionMode(QAbstractItemView.NoSelection)
193
+ self.bag_topic_list.setFixedHeight(80)
194
+
195
+ btn_scan_bag = QPushButton("šŸ”„ Scan Topics")
196
+ btn_scan_bag.setFixedWidth(150)
197
+ btn_scan_bag.clicked.connect(self.scan_bag_topics)
198
+
199
+ row_topic.addWidget(QLabel("LiDAR Topics:"))
200
+ row_topic.addWidget(self.bag_topic_list, 1)
201
+ row_topic.addWidget(btn_scan_bag)
202
+ bag_l.addLayout(row_topic)
203
+
204
+ # Info Label
205
+ lbl_info = QLabel("ā„¹ļø LiDAR topic should be for 3D cloud (PointCloud2)")
206
+ lbl_info.setStyleSheet("color: #AAA; font-size: 10px; margin-left: 5px;")
207
+ bag_l.addWidget(lbl_info)
208
+
209
+ bag_w.setLayout(bag_l)
210
+
211
+ self.config_stack.addWidget(live_w)
212
+ self.config_stack.addWidget(bag_w)
213
+ layout.addWidget(self.config_stack, 0)
214
+
215
+ # 3. Output Settings (Expanded with RViz)
216
+ out_group = QGroupBox("Output & Visual Settings")
217
+ out_group.setStyleSheet(group_style)
218
+ out_layout = QHBoxLayout()
219
+ out_layout.setContentsMargins(6, 6, 6, 6)
220
+
221
+ # Sub-layout for Names
222
+ name_v = QVBoxLayout()
223
+ out_layout.addLayout(name_v, 1)
224
+
225
+ self.out_folder = QLineEdit()
226
+ self.out_folder.setPlaceholderText("Folder Name (e.g 'dataset_1')")
227
+ self.pose_prefix = QLineEdit()
228
+ self.pose_prefix.setPlaceholderText("Pose Prefix (e.g 'scan')")
229
+
230
+ row1 = QHBoxLayout()
231
+ row1.addWidget(QLabel("Folder:"))
232
+ row1.addWidget(self.out_folder)
233
+ name_v.addLayout(row1)
234
+
235
+ row2 = QHBoxLayout()
236
+ row2.addWidget(QLabel("Prefix:"))
237
+ row2.addWidget(self.pose_prefix)
238
+ name_v.addLayout(row2)
239
+
240
+ # RViz settings
241
+ rviz_v = QVBoxLayout()
242
+ out_layout.addLayout(rviz_v, 1)
243
+
244
+ self.rviz_combo = QComboBox()
245
+ self.scan_rviz_configs()
246
+
247
+ self.rviz_new = QLineEdit()
248
+ self.rviz_new.setPlaceholderText("New Config Name (optional)")
249
+
250
+ r1 = QHBoxLayout()
251
+ r1.addWidget(QLabel("RViz Config:"))
252
+ r1.addWidget(self.rviz_combo)
253
+ rviz_v.addLayout(r1)
254
+
255
+ r2 = QHBoxLayout()
256
+ r2.addWidget(QLabel("Create New:"))
257
+ r2.addWidget(self.rviz_new)
258
+ rviz_v.addLayout(r2)
259
+
260
+ out_group.setLayout(out_layout)
261
+ layout.addWidget(out_group, 0)
262
+
263
+ # 4. Output Log
264
+ self.rec_log = QTextEdit()
265
+ self.rec_log.setReadOnly(True)
266
+ self.rec_log.setStyleSheet("background-color: #1e1e1e; color: #00ff00; font-family: Monospace;")
267
+ # Give the log window the stretch priority (1) while others are 0
268
+ layout.addWidget(self.rec_log, 1)
269
+
270
+ # 6. Controls
271
+ ctrl_layout = QHBoxLayout()
272
+ self.btn_start_rec = QPushButton("ā–¶ Start")
273
+ self.btn_start_rec.setStyleSheet("""
274
+ QPushButton { background-color: #4CAF50; color: white; padding: 10px; font-weight: bold; }
275
+ QPushButton:hover { background-color: #66BB6A; }
276
+ """)
277
+ self.btn_start_rec.clicked.connect(self.start_listening)
278
+
279
+ self.btn_play_pause = QPushButton("āÆ Play/Pause")
280
+ self.btn_play_pause.setStyleSheet("""
281
+ QPushButton { background-color: #2196F3; color: white; padding: 10px; }
282
+ QPushButton:hover { background-color: #42A5F5; }
283
+ """)
284
+ self.btn_play_pause.clicked.connect(self.toggle_pause)
285
+ self.btn_play_pause.setEnabled(False)
286
+
287
+ self.btn_capture = QPushButton("šŸ“ø Capture Frame")
288
+ self.btn_capture.setStyleSheet("""
289
+ QPushButton { background-color: #FF9800; color: white; padding: 10px; font-weight: bold; }
290
+ QPushButton:hover { background-color: #FFA726; }
291
+ """)
292
+ self.btn_capture.clicked.connect(self.capture_pose)
293
+ self.btn_capture.setEnabled(False)
294
+
295
+ self.btn_stop_rec = QPushButton("ā¹ Stop")
296
+ self.btn_stop_rec.setStyleSheet("""
297
+ QPushButton { background-color: #d32f2f; color: white; padding: 10px; }
298
+ QPushButton:hover { background-color: #ef5350; }
299
+ """)
300
+ self.btn_stop_rec.clicked.connect(self.stop_listening)
301
+ self.btn_stop_rec.setEnabled(False)
302
+
303
+ ctrl_layout.addWidget(self.btn_start_rec)
304
+ ctrl_layout.addWidget(self.btn_play_pause)
305
+ ctrl_layout.addWidget(self.btn_capture)
306
+ ctrl_layout.addWidget(self.btn_stop_rec)
307
+ layout.addLayout(ctrl_layout, 0)
308
+
309
+ # 7. Back Button
310
+ btn_back = QPushButton("← Back")
311
+ btn_back.clicked.connect(self.back_from_record)
312
+ btn_back.setStyleSheet("""
313
+ QPushButton { background-color: #2E8B57; border-radius: 8px; color: white; padding: 8px; font-weight: bold; }
314
+ QPushButton:hover { background-color: #689F38; }
315
+ """)
316
+ layout.addWidget(btn_back, 0)
317
+
318
+
319
+ w.setLayout(layout)
320
+ return w
321
+
322
+ def create_file_ui(self):
323
+ w = QWidget()
324
+ layout = QVBoxLayout()
325
+
326
+
327
+
328
+ # Title
329
+ lbl = QLabel("Select 2 Point Clouds (PCD)")
330
+ lbl.setStyleSheet("font-size: 18px; font-weight: bold; margin: 10px;")
331
+ lbl.setAlignment(Qt.AlignCenter)
332
+ layout.addWidget(lbl)
333
+
334
+ # Form
335
+ form_group = QGroupBox()
336
+ form_layout = QVBoxLayout()
337
+
338
+ # Source
339
+ src_layout = QHBoxLayout()
340
+ self.src_path = QLineEdit()
341
+ self.src_path.setPlaceholderText("Path to Source .pcd")
342
+ self.src_path.textChanged.connect(self.validate_file_inputs)
343
+ btn_src = QPushButton("Browse")
344
+ btn_src.clicked.connect(lambda: self.browse_file(self.src_path))
345
+ src_layout.addWidget(QLabel("Source Cloud:"))
346
+ src_layout.addWidget(self.src_path)
347
+ src_layout.addWidget(btn_src)
348
+ form_layout.addLayout(src_layout)
349
+
350
+ # Target
351
+ tgt_layout = QHBoxLayout()
352
+ self.tgt_path = QLineEdit()
353
+ self.tgt_path.setPlaceholderText("Path to Target .pcd")
354
+ self.tgt_path.textChanged.connect(self.validate_file_inputs)
355
+ btn_tgt = QPushButton("Browse")
356
+ btn_tgt.clicked.connect(lambda: self.browse_file(self.tgt_path))
357
+ tgt_layout.addWidget(QLabel("Target Cloud:"))
358
+ tgt_layout.addWidget(self.tgt_path)
359
+ tgt_layout.addWidget(btn_tgt)
360
+ form_layout.addLayout(tgt_layout)
361
+
362
+ form_group.setLayout(form_layout)
363
+ layout.addWidget(form_group)
364
+
365
+ # Action
366
+ self.btn_calib = QPushButton("Begin Calibration")
367
+ self.btn_calib.clicked.connect(self.run_calibration)
368
+ self.btn_calib.setEnabled(False)
369
+ self.btn_calib.setStyleSheet("""
370
+ QPushButton {
371
+ background-color: #E65100;
372
+ color: white;
373
+ padding: 15px;
374
+ font-size: 16px;
375
+ font-weight: bold;
376
+ border-radius: 12px;
377
+ }
378
+ QPushButton:hover {
379
+ background-color: #F57C00;
380
+ }
381
+ QPushButton:disabled {
382
+ background-color: #555;
383
+ color: #888;
384
+ }
385
+ """)
386
+ layout.addWidget(self.btn_calib)
387
+
388
+ # Logs
389
+ layout.addWidget(QLabel("Process Output:"))
390
+ self.calib_log = QTextEdit()
391
+ self.calib_log.setReadOnly(True)
392
+ self.calib_log.setStyleSheet("font-family: Monospace; font-size: 12px; background-color: #1e1e1e; color: #00ff00;")
393
+ layout.addWidget(self.calib_log)
394
+
395
+ # Back Button
396
+ btn_back = QPushButton("← Back")
397
+ btn_back.clicked.connect(self.go_home.emit)
398
+ btn_back.setStyleSheet("""
399
+ QPushButton { background-color: #2E8B57; border-radius: 8px; color: white; padding: 8px; font-weight: bold; }
400
+ QPushButton:hover { background-color: #689F38; }
401
+ """)
402
+ layout.addWidget(btn_back)
403
+
404
+ w.setLayout(layout)
405
+ return w
406
+
407
+ # --- Recorder Logic ---
408
+ def update_rec_ui_state(self):
409
+ is_live = self.rb_live.isChecked()
410
+ self.config_stack.setCurrentIndex(0 if is_live else 1)
411
+ self.btn_play_pause.setEnabled(False) # Reset
412
+
413
+ def scan_topics(self):
414
+ self.rec_log.append("Scanning ROS2 topics...")
415
+ self.topic_list.clear()
416
+
417
+ script_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../single_data_extractor.py"))
418
+ proc = subprocess.run(['python3', script_path, '--mode', 'discover', '--format', 'json'],
419
+ capture_output=True, text=True)
420
+
421
+ try:
422
+ data = json.loads(proc.stdout)
423
+ topics = data.get('lidar', [])
424
+ for t in topics:
425
+ item = QListWidgetItem(t)
426
+ item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
427
+ item.setCheckState(Qt.Unchecked)
428
+ self.topic_list.addItem(item)
429
+
430
+ self.rec_log.append(f"Found {len(topics)} LiDAR topics.")
431
+ except json.JSONDecodeError:
432
+ self.rec_log.append("Error scanning topics.")
433
+
434
+ def scan_bag_topics(self):
435
+ bag_path = self.bag_path.text().strip()
436
+ if not bag_path or not os.path.exists(bag_path):
437
+ QMessageBox.warning(self, "Warning", "Please select a valid rosbag folder first.")
438
+ return
439
+
440
+ self.rec_log.append("Scanning rosbag topics...")
441
+ self.bag_topic_list.clear() # Clear list
442
+
443
+ script_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../single_data_extractor.py"))
444
+ proc = subprocess.run(['python3', script_path, '--mode', 'discover_bag', '--bag', bag_path, '--format', 'json'],
445
+ capture_output=True, text=True)
446
+
447
+ try:
448
+ data = json.loads(proc.stdout)
449
+ lidar_topics = data.get('lidar', [])
450
+
451
+ # Populate ListWidget with Checkboxes
452
+ for t in lidar_topics:
453
+ if t == 'None': continue
454
+ item = QListWidgetItem(t)
455
+ item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
456
+ item.setCheckState(Qt.Unchecked)
457
+ self.bag_topic_list.addItem(item)
458
+
459
+ self.rec_log.append(f"Rosbag topics scanned. Found {self.bag_topic_list.count()} LiDAR topics.")
460
+ except json.JSONDecodeError:
461
+ self.rec_log.append("Error scanning topics.")
462
+ self.rec_log.append(f"Output: {proc.stdout}")
463
+
464
+ def scan_rviz_configs(self):
465
+ self.rviz_combo.clear()
466
+ # Default dir
467
+ config_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../rviz_configs"))
468
+ os.makedirs(config_dir, exist_ok=True)
469
+
470
+ files = glob.glob(os.path.join(config_dir, "*.rviz"))
471
+ names = [os.path.basename(f) for f in files]
472
+
473
+ if not names:
474
+ self.rviz_combo.addItem("default_calib.rviz")
475
+ else:
476
+ self.rviz_combo.addItems(sorted(names))
477
+
478
+ def browse_bag(self):
479
+ path = QFileDialog.getExistingDirectory(self, "Select Rosbag Folder", "")
480
+ if path:
481
+ self.bag_path.setText(path)
482
+
483
+ def start_listening(self):
484
+ script_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../single_data_extractor.py"))
485
+ is_live = self.rb_live.isChecked()
486
+
487
+ args = ["-u", script_path]
488
+
489
+ if is_live:
490
+ # Collect checked items
491
+ selected_topics = []
492
+ for i in range(self.topic_list.count()):
493
+ item = self.topic_list.item(i)
494
+ if item.checkState() == Qt.Checked:
495
+ selected_topics.append(item.text())
496
+
497
+ if not selected_topics:
498
+ QMessageBox.warning(self, "Warning", "Select at least one LiDAR topic.")
499
+ return
500
+
501
+ # Join with commas
502
+ topic_str = ",".join(selected_topics)
503
+ args.extend(['--mode', 'live', '--lidar_topic', topic_str])
504
+ self.btn_play_pause.setEnabled(True) # Enabled for Master Pause feature
505
+ else:
506
+ bag = self.bag_path.text().strip()
507
+ if not os.path.exists(bag):
508
+ QMessageBox.warning(self, "Warning", "Select a valid rosbag folder.")
509
+ return
510
+ args.extend(['--mode', 'bag', '--bag', bag])
511
+ if not self.cb_loop.isChecked():
512
+ args.append('--play_once')
513
+
514
+ # Add topics if checked
515
+ bag_topics = []
516
+ for i in range(self.bag_topic_list.count()):
517
+ item = self.bag_topic_list.item(i)
518
+ if item.checkState() == Qt.Checked:
519
+ bag_topics.append(item.text())
520
+
521
+ if bag_topics:
522
+ # Comma separated for multi-topic support
523
+ args.extend(['--lidar_topic', ",".join(bag_topics)])
524
+ else:
525
+ QMessageBox.warning(self, "Warning", "Please select at least one LiDAR topic from the list.")
526
+ return
527
+
528
+ self.btn_play_pause.setEnabled(True)
529
+
530
+ # args.extend(['--calib_mode', 'single_lidar']) # Deprecated in single_data_extractor
531
+
532
+ # Naming Args
533
+ folder_name = self.out_folder.text().strip()
534
+ if folder_name:
535
+ args.extend(['--output_name', folder_name])
536
+
537
+ pose_prefix = self.pose_prefix.text().strip()
538
+ if pose_prefix:
539
+ args.extend(['--pose_prefix', pose_prefix])
540
+
541
+ # RViz Logic
542
+ new_rviz = self.rviz_new.text().strip()
543
+ selected_rviz = self.rviz_combo.currentText().strip()
544
+
545
+ if new_rviz:
546
+ if not new_rviz.endswith('.rviz'): new_rviz += '.rviz'
547
+ args.extend(['--rviz_config', new_rviz])
548
+ elif selected_rviz:
549
+ args.extend(['--rviz_config', selected_rviz])
550
+
551
+ self.rec_log.clear()
552
+ self.rec_log.append(f"🟢 Starting Process: {'Live' if is_live else 'Bag'} Mode")
553
+
554
+ self.record_process = QProcess()
555
+ self.record_process.readyReadStandardOutput.connect(self.handle_rec_stdout)
556
+ self.record_process.readyReadStandardError.connect(self.handle_rec_stderr)
557
+ self.record_process.finished.connect(self.on_rec_finished)
558
+
559
+ self.record_process.start("python3", args)
560
+
561
+ self.btn_start_rec.setEnabled(False)
562
+ self.btn_capture.setEnabled(True)
563
+ self.btn_stop_rec.setEnabled(True)
564
+
565
+ # Disable inputs
566
+ self.rb_live.setEnabled(False)
567
+ self.rb_bag.setEnabled(False)
568
+ self.out_folder.setEnabled(False)
569
+ self.pose_prefix.setEnabled(False)
570
+ self.rviz_combo.setEnabled(False)
571
+ self.rviz_new.setEnabled(False)
572
+ self.topic_list.setEnabled(False)
573
+
574
+ def stop_listening(self):
575
+ if self.record_process and self.record_process.state() == QProcess.Running:
576
+ self.record_process.write(b'q\n')
577
+ self.record_process.waitForFinished(1000)
578
+ if self.record_process.state() == QProcess.Running:
579
+ self.record_process.kill()
580
+ # Finished signal will handle UI reset
581
+
582
+ def on_rec_finished(self):
583
+ self.btn_start_rec.setEnabled(True)
584
+ self.btn_capture.setEnabled(False)
585
+ self.btn_stop_rec.setEnabled(False)
586
+ self.btn_play_pause.setEnabled(False)
587
+
588
+ # Re-enable inputs
589
+ self.rb_live.setEnabled(True)
590
+ self.rb_bag.setEnabled(True)
591
+ self.out_folder.setEnabled(True)
592
+ self.pose_prefix.setEnabled(True)
593
+ self.rviz_combo.setEnabled(True)
594
+ self.rviz_new.setEnabled(True)
595
+ self.topic_list.setEnabled(True)
596
+
597
+ # SCAN REFRESH in case a new RViz was created
598
+ self.scan_rviz_configs()
599
+
600
+ self.rec_log.append("\nāœ… Process Stopped.")
601
+
602
+ def capture_pose(self):
603
+ if self.record_process and self.record_process.state() == QProcess.Running:
604
+ self.record_process.write(b'n\n')
605
+
606
+ def toggle_pause(self):
607
+ if self.record_process and self.record_process.state() == QProcess.Running:
608
+ self.record_process.write(b'p\n')
609
+
610
+ def handle_rec_stdout(self):
611
+ data = self.record_process.readAllStandardOutput()
612
+ self.rec_log.append(data.data().decode().strip())
613
+
614
+ def handle_rec_stderr(self):
615
+ data = self.record_process.readAllStandardError()
616
+ self.rec_log.append(data.data().decode().strip())
617
+
618
+ def back_from_record(self):
619
+ self.stop_listening()
620
+ self.go_home.emit()
621
+
622
+ # --- Calibration Logic ---
623
+ def browse_file(self, line_edit):
624
+ fname, _ = QFileDialog.getOpenFileName(self, "Select PCD", "", "Point Clouds (*.pcd)")
625
+ if fname:
626
+ line_edit.setText(fname)
627
+
628
+ def validate_file_inputs(self):
629
+ src = self.src_path.text().strip()
630
+ tgt = self.tgt_path.text().strip()
631
+ valid = bool(src) and bool(tgt) and os.path.exists(src) and os.path.exists(tgt)
632
+ self.btn_calib.setEnabled(valid)
633
+
634
+ def run_calibration(self):
635
+ src = self.src_path.text()
636
+ tgt = self.tgt_path.text()
637
+
638
+ self.calib_log.clear()
639
+ self.calib_log.append(f"Starting Calibration...\nSource: {src}\nTarget: {tgt}")
640
+ self.btn_calib.setEnabled(False)
641
+ self.btn_calib.setText("Running...")
642
+
643
+ self.calib_process = QProcess()
644
+ self.calib_process.readyReadStandardOutput.connect(self.handle_calib_stdout)
645
+ self.calib_process.readyReadStandardError.connect(self.handle_calib_stderr)
646
+ self.calib_process.finished.connect(self.on_calib_finished)
647
+
648
+ script_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../single_lidar_calib.py"))
649
+
650
+ cmd = "python3"
651
+ args = ["-u", script_path, "--src", src, "--tgt", tgt]
652
+ self.calib_process.start(cmd, args)
653
+
654
+ def handle_calib_stdout(self):
655
+ data = self.calib_process.readAllStandardOutput()
656
+ self.calib_log.append(data.data().decode())
657
+
658
+ def handle_calib_stderr(self):
659
+ data = self.calib_process.readAllStandardError()
660
+ self.calib_log.append(data.data().decode())
661
+
662
+ def on_calib_finished(self):
663
+ self.btn_calib.setEnabled(True)
664
+ self.btn_calib.setText("Begin Calibration")
665
+ self.calib_log.append("\nāœ… Calibration Process Finished.")
666
+
667
+ def cleanup(self):
668
+ if self.calib_process and self.calib_process.state() == QProcess.Running:
669
+ self.calib_process.kill()
670
+ if self.record_process and self.record_process.state() == QProcess.Running:
671
+ self.record_process.kill()
672
+ if self.diag_process and self.diag_process.state() == QProcess.Running:
673
+ self.diag_process.kill()