3232
3333#include < string>
3434#include < vector>
35+ #include < algorithm>
3536
3637#include < QApplication>
3738#include < QDesktopWidget>
4647#include < QGroupBox>
4748#include < QTabWidget>
4849#include < QPixmap>
50+ #include < QGridLayout>
51+ #include < QHBoxLayout>
52+ #include < QVBoxLayout>
53+ #include < QSizePolicy>
54+ #include < QProcess>
55+ #include < QDir>
56+ #include < QTextStream>
57+ #include < QFile>
58+ #include < QTemporaryFile>
4959
5060#include " rclcpp/rclcpp.hpp"
5161#include " sensor_msgs/msg/image.hpp"
@@ -230,7 +240,8 @@ MainWindow::MainWindow(QWidget* parent) :
230240 camera_display_ (nullptr ),
231241 main_layout_ (nullptr ),
232242 display_ (nullptr ),
233- status_label_ (nullptr ) {
243+ status_label_ (nullptr ),
244+ stop_config_button_ (nullptr ) {
234245 this ->setWindowTitle (" UT AUTOmataGUI" );
235246 camera_display_ = new CameraDisplay ();
236247
@@ -259,28 +270,64 @@ MainWindow::MainWindow(QWidget* parent) :
259270 QSizePolicy expanding_policy;
260271 expanding_policy.setVerticalPolicy (QSizePolicy::Expanding);
261272 expanding_policy.setHorizontalPolicy (QSizePolicy::Expanding);
262- QPushButton* start_ros = new QPushButton (" Start roscore" );
263- QPushButton* start_camera = new QPushButton (" Start Camera" );
264- QPushButton* start_car = new QPushButton (" Start Car" );
265- QPushButton* stop_all = new QPushButton (" Stop all nodes" );
266- start_ros->setFont (font);
267- start_camera->setFont (font);
268- start_car->setFont (font);
269- stop_all->setFont (font);
270- start_ros->setSizePolicy (expanding_policy);
271- start_camera->setSizePolicy (expanding_policy);
272- start_car->setSizePolicy (expanding_policy);
273- stop_all->setSizePolicy (expanding_policy);
274- connect (start_car, SIGNAL (clicked ()), this , SLOT (StartCar ()));
275- connect (start_ros, SIGNAL (clicked ()), this , SLOT (StartRos ()));
276- connect (start_camera, SIGNAL (clicked ()), this , SLOT (StartCamera ()));
277- connect (stop_all, SIGNAL (clicked ()), this , SLOT (StopAll ()));
278- QVBoxLayout* vbox = new QVBoxLayout ();
279- vbox->addWidget (start_ros);
280- vbox->addWidget (start_camera);
281- vbox->addWidget (start_car);
282- vbox->addWidget (stop_all);
283- ros_group->setLayout (vbox);
273+
274+ // Create main horizontal layout
275+ QHBoxLayout* main_config_layout = new QHBoxLayout ();
276+
277+ // Left side - Tmux configurations
278+ QWidget* left_widget = new QWidget ();
279+ QVBoxLayout* left_layout = new QVBoxLayout ();
280+ QLabel* config_label = new QLabel (" Available Configurations:" );
281+ config_label->setFont (font);
282+ left_layout->addWidget (config_label);
283+
284+ // Get tmux configuration names (excluding "sim")
285+ tmux_config_names_.clear ();
286+ tmux_config_buttons_.clear ();
287+ QDir tmux_dir (" /home/orin/roboracer_ws/tmux" );
288+ QStringList subdirs = tmux_dir.entryList (QDir::Dirs | QDir::NoDotAndDotDot);
289+
290+ for (const QString& subdir : subdirs) {
291+ if (subdir != " sim" ) {
292+ tmux_config_names_.push_back (subdir.toStdString ());
293+ QPushButton* config_button = new QPushButton (subdir);
294+ config_button->setFont (font);
295+ config_button->setSizePolicy (expanding_policy);
296+ config_button->setCheckable (true );
297+ connect (config_button, SIGNAL (clicked ()), this , SLOT (StartTmuxConfiguration ()));
298+ tmux_config_buttons_.push_back (config_button);
299+ left_layout->addWidget (config_button);
300+ }
301+ }
302+
303+ left_layout->addStretch ();
304+ left_widget->setLayout (left_layout);
305+
306+ // Right side - Control buttons
307+ QWidget* right_widget = new QWidget ();
308+ QVBoxLayout* right_layout = new QVBoxLayout ();
309+
310+ stop_config_button_ = new QPushButton (" Stop Configuration" );
311+ stop_config_button_->setFont (font);
312+ stop_config_button_->setSizePolicy (expanding_policy);
313+ stop_config_button_->setEnabled (false ); // Initially disabled
314+ connect (stop_config_button_, SIGNAL (clicked ()), this , SLOT (StopTmuxConfiguration ()));
315+
316+ QPushButton* shutdown_button = new QPushButton (" Shutdown Car" );
317+ shutdown_button->setFont (font);
318+ shutdown_button->setSizePolicy (expanding_policy);
319+ connect (shutdown_button, SIGNAL (clicked ()), this , SLOT (ShutdownCar ()));
320+
321+ right_layout->addWidget (stop_config_button_);
322+ right_layout->addStretch (); // Gap in the middle
323+ right_layout->addWidget (shutdown_button);
324+ right_widget->setLayout (right_layout);
325+
326+ // Add left and right widgets to main layout
327+ main_config_layout->addWidget (left_widget, 2 ); // Give more space to left side
328+ main_config_layout->addWidget (right_widget, 1 );
329+
330+ ros_group->setLayout (main_config_layout);
284331
285332 // Grid layout
286333 QWidget* main_widget = new QWidget ();
@@ -302,7 +349,7 @@ MainWindow::MainWindow(QWidget* parent) :
302349 main_widget->setLayout (main_layout);
303350
304351 tab_widget_->addTab (main_widget, " Main" );
305- tab_widget_->addTab (ros_group, tr (" Startup / Shutdown " ));
352+ tab_widget_->addTab (ros_group, tr (" Configurations " ));
306353 }
307354
308355 main_layout_ = new QVBoxLayout (this );
@@ -347,19 +394,11 @@ std::vector<std::string> Split(const std::string& s) {
347394}
348395
349396void Exec (const string& cmd) {
350- auto params = Split (cmd);
351- if (params.empty ()) return ;
352- vector<const char *> param_ptrs;
353- for (string& s : params) {
354- param_ptrs.push_back (s.c_str ());
355- printf (" '%s'," , s.c_str ());
356- }
357- param_ptrs.push_back (NULL );
358- printf (" \n " );
397+ if (cmd.empty ()) return ;
398+ printf (" Executing: %s\n " , cmd.c_str ());
359399 const int pid = fork ();
360400 if (pid == 0 ) {
361- if (execv (param_ptrs[0 ],
362- const_cast <char * const *>( param_ptrs.data ())) == -1 ) {
401+ if (execl (" /bin/bash" , " bash" , " -c" , cmd.c_str (), NULL ) == -1 ) {
363402 perror (" Error executing command" );
364403 exit (1 );
365404 }
@@ -398,6 +437,7 @@ void MainWindow::UpdateIP() {
398437 }
399438 ipaddr_label_->setText (QString::fromUtf8 (s.c_str ()));
400439 ros_led_->SetStatus (rclcpp::ok ());
440+ UpdateTmuxConfigurations ();
401441}
402442
403443void MainWindow::UpdateStatus (int mode,
@@ -456,6 +496,156 @@ void MainWindow::UpdateCameraSlot(const QPixmap& image) {
456496 camera_display_->UpdateImage (image);
457497}
458498
499+ void MainWindow::UpdateTmuxConfigurations () {
500+ // Get list of running tmux sessions
501+ QProcess process;
502+ process.start (" tmux" , QStringList () << " ls" );
503+ process.waitForFinished (1000 );
504+
505+ std::vector<std::string> running_sessions;
506+ bool any_session_running = false ;
507+
508+ if (process.exitCode () == 0 ) {
509+ QString output = process.readAllStandardOutput ();
510+ QStringList sessions = output.split (' \n ' , Qt::SkipEmptyParts);
511+
512+ for (const QString& session : sessions) {
513+ // Session name is everything before the first colon
514+ QString session_name = session.split (' :' ).first ();
515+ running_sessions.push_back (session_name.toStdString ());
516+ any_session_running = true ;
517+ }
518+ }
519+
520+ // Update button states
521+ for (size_t i = 0 ; i < tmux_config_buttons_.size (); ++i) {
522+ bool is_running = std::find (running_sessions.begin (), running_sessions.end (),
523+ tmux_config_names_[i]) != running_sessions.end ();
524+ tmux_config_buttons_[i]->setChecked (is_running);
525+
526+ // Change button text to indicate running state
527+ QString button_text = QString::fromStdString (tmux_config_names_[i]);
528+ if (is_running) {
529+ button_text += " (Running)" ;
530+ }
531+ tmux_config_buttons_[i]->setText (button_text);
532+ }
533+
534+ // Enable/disable stop button based on whether any session is running
535+ stop_config_button_->setEnabled (any_session_running);
536+ }
537+
538+ bool MainWindow::IsGuiNodeRunning () {
539+ QProcess process;
540+ process.start (" ros2" , QStringList () << " node" << " list" );
541+ process.waitForFinished (3000 );
542+
543+ if (process.exitCode () == 0 ) {
544+ QString output = process.readAllStandardOutput ();
545+ return output.contains (" /ut_automata_gui" );
546+ }
547+ return false ;
548+ }
549+
550+ std::string MainWindow::CreateTmuxConfigWithoutGui (const std::string& config_name) {
551+ QString original_path = QString (" /home/orin/roboracer_ws/tmux/%1/.tmuxinator.yaml" ).arg (QString::fromStdString (config_name));
552+
553+ QFile original_file (original_path);
554+ if (!original_file.open (QIODevice::ReadOnly | QIODevice::Text)) {
555+ return " " ; // Return empty string on error
556+ }
557+
558+ QTextStream in (&original_file);
559+ QString content = in.readAll ();
560+ original_file.close ();
561+
562+ // Remove lines containing "ros2 run ut_automata gui"
563+ QStringList lines = content.split (' \n ' );
564+ QStringList filtered_lines;
565+
566+ for (const QString& line : lines) {
567+ if (!line.contains (" ros2 run ut_automata gui" )) {
568+ filtered_lines.append (line);
569+ }
570+ }
571+
572+ // Create temporary file
573+ QTemporaryFile* temp_file = new QTemporaryFile ();
574+ temp_file->setFileTemplate (QString (" /tmp/tmux_%1_nogui_XXXXXX.yaml" ).arg (QString::fromStdString (config_name)));
575+ temp_file->setAutoRemove (false ); // We'll clean it up manually later
576+
577+ if (!temp_file->open ()) {
578+ delete temp_file;
579+ return " " ;
580+ }
581+
582+ QTextStream out (temp_file);
583+ out << filtered_lines.join (' \n ' );
584+ temp_file->close ();
585+
586+ std::string temp_path = temp_file->fileName ().toStdString ();
587+ delete temp_file;
588+ return temp_path;
589+ }
590+
591+ void MainWindow::StartTmuxConfiguration () {
592+ QPushButton* button = qobject_cast<QPushButton*>(sender ());
593+ if (!button) return ;
594+
595+ // Find which configuration was clicked
596+ for (size_t i = 0 ; i < tmux_config_buttons_.size (); ++i) {
597+ if (tmux_config_buttons_[i] == button) {
598+ std::string config_name = tmux_config_names_[i];
599+
600+ // Check if GUI node is already running
601+ bool gui_running = IsGuiNodeRunning ();
602+
603+ if (gui_running) {
604+ // Create a temporary config without GUI
605+ std::string temp_config_path = CreateTmuxConfigWithoutGui (config_name);
606+ if (!temp_config_path.empty ()) {
607+ std::string command = " tmuxinator start -p " + temp_config_path +
608+ " --name " + config_name +
609+ " && rm " + temp_config_path; // Clean up temp file
610+ Exec (command);
611+ } else {
612+ printf (" Error: Could not create temporary config without GUI\n " );
613+ }
614+ } else {
615+ // Use original config
616+ std::string command = " cd /home/orin/roboracer_ws/tmux/" + config_name + " && tmuxinator start" ;
617+ Exec (command);
618+ }
619+ break ;
620+ }
621+ }
622+ }
623+
624+ void MainWindow::StopTmuxConfiguration () {
625+ // Get list of running tmux sessions and kill each one individually
626+ QProcess process;
627+ process.start (" tmux" , QStringList () << " ls" );
628+ process.waitForFinished (1000 );
629+
630+ if (process.exitCode () == 0 ) {
631+ QString output = process.readAllStandardOutput ();
632+ QStringList sessions = output.split (' \n ' , Qt::SkipEmptyParts);
633+
634+ for (const QString& session : sessions) {
635+ // Session name is everything before the first colon
636+ QString session_name = session.split (' :' ).first ();
637+ std::string kill_command = " tmux kill-session -t " + session_name.toStdString ();
638+ Exec (kill_command);
639+ }
640+ }
641+ }
642+
643+ void MainWindow::ShutdownCar () {
644+ // Stop all tmux sessions and shutdown ROS nodes
645+ StopTmuxConfiguration ();
646+ StopAll ();
647+ }
648+
459649
460650
461651} // namespace ut_automata_gui
0 commit comments