Skip to content

Commit b4562bb

Browse files
committed
control via tmux
1 parent a0cca93 commit b4562bb

2 files changed

Lines changed: 243 additions & 35 deletions

File tree

src/gui/gui_mainwindow.cc

Lines changed: 225 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232

3333
#include <string>
3434
#include <vector>
35+
#include <algorithm>
3536

3637
#include <QApplication>
3738
#include <QDesktopWidget>
@@ -46,6 +47,15 @@
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

349396
void 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

403443
void 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

src/gui/gui_mainwindow.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,14 @@
3131
#include <QLabel>
3232
#include <QPixmap>
3333
#include <QResizeEvent>
34+
#include <QPushButton>
3435

3536
class QVBoxLayout;
3637
class QTabWidget;
38+
class QHBoxLayout;
39+
class QGridLayout;
40+
class QProcess;
41+
class QDir;
3742

3843
namespace vector_display {
3944
class VectorDisplay;
@@ -149,6 +154,10 @@ public slots:
149154
float throttle,
150155
float steering);
151156
void UpdateCameraSlot(const QPixmap& image);
157+
void UpdateTmuxConfigurations();
158+
void StartTmuxConfiguration();
159+
void StopTmuxConfiguration();
160+
void ShutdownCar();
152161

153162
signals:
154163
void UpdateQuestion(std::string question,
@@ -185,6 +194,15 @@ public slots:
185194

186195
// Robot status display.
187196
QLabel* status_label_;
197+
198+
// Tmux configuration widgets
199+
std::vector<QPushButton*> tmux_config_buttons_;
200+
QPushButton* stop_config_button_;
201+
std::vector<std::string> tmux_config_names_;
202+
203+
// Helper functions for GUI-aware tmux configuration
204+
bool IsGuiNodeRunning();
205+
std::string CreateTmuxConfigWithoutGui(const std::string& config_name);
188206
};
189207

190208

0 commit comments

Comments
 (0)