一.需求:
作为测试工程师,为了到现场高效的调试,部署工作,需要一个可视化的工具,避免繁琐的记命令行和敲命令行,直接把命令行集成到可视化工具,运行某些命令时,点击按钮即可实现。
- 命令行配置简单,可动态添加命令行
- 支持多窗口运行
- 支持多命令行运行
- 支持命令行的顺序运行
二.原理支撑:
python os库下的system()函数
system("gnome-terminal --window -e 'bash -c \"pwd;ls;exec bash\"' --tab -e 'bash -c \"pwd; exec bash\"'");
其中:
gnome-terminal --window -e 'bash -c "pwd;ls; — 开启一个终端窗口 并运行 pwd和 ls
【先运行pwd后,再运行ls,同一个窗口不同命令用“;”间隔】
–tab -e ‘bash -c “pwd; exec bash”’" ---- 同一个窗口下开启另外一个tab 并运行指令 pwd
三.简单Demo
import os
# 启动ROS2必须操作的前置条件, 运行ros2指令前需要添加的。
ros_path = "source /opt/ros/humble/setup.bash "
# 这些是一些语法格式的需要 不需要修改
win_cmd = "gnome-terminal --window -e 'bash -c \"" + ros_path
tab_cmd = " --tab -e 'bash -c \"" + ros_path
end_cmd = ";exec bash\"'"
def run_win(cmd):
# 运行的第一个窗口终端
cmd = win_cmd+cmd+end_cmd
return cmd
def run_tab(cmd, t):
# 运行的标签终端
delay_t = "sleep {};".format(str(t))
cmd = tab_cmd+delay_t+cmd+end_cmd
return cmd
# t 指令运行之间的时间间隔
def run_cmd(cmds, t = 2):
# 合成命令
cmd = run_win(cmds[0])
for i in range(1, len(cmds)):
cmd = cmd + run_tab(cmds[i], t*i)
#print(cmd)
# 运行指令
os.system(cmd)
# 单窗口多指令 “;” 间隔
cmds = ["export TURTLEBOT3_MODEL=waffle;export GAZEBO_MODEL_PATH=$GAZEBO_MODEL_PATH:/opt/ros/humble/share/turtlebot3_gazebo/models;ros2 launch nav2_bringup tb3_simulation_launch.py headless:=False"]
# 多窗口多指令 列表 , 间隔
# cmds = ["export TURTLEBOT3_MODEL=waffle","export GAZEBO_MODEL_PATH=$GAZEBO_MODEL_PATH:/opt/ros/humble/share/turtlebot3_gazebo/models","ros2 launch nav2_bringup tb3_simulation_launch.py headless:=False"]
run_cmd(cmds)
四.封装成GUI
1.依赖库
PyQt5 5.15.6
PyYAML 5.4.1
pyinstaller 6.13.0
2.代码
import sys
from PyQt5.QtWidgets import QMainWindow,QApplication,QVBoxLayout
from PyQt5.QtWidgets import QSystemTrayIcon, QPushButton,QMenu,QAction,QGridLayout
from PyQt5.QtGui import QIcon
# from system_hotkey import SystemHotkey # 热键#
from Ui_tools import Ui_MainWindow
from PyQt5.QtCore import Qt
import os
import yaml
# 启动ROS2必须操作的前置条件, 运行ros2指令前需要添加的。
ros_path = "source /opt/ros/humble/setup.bash;"
# 这些是一些语法格式的需要 不需要修改
win_cmd = "gnome-terminal --window -e 'bash -c \"" + ros_path
tab_cmd = " --tab -e 'bash -c \"" + ros_path
end_cmd = ";exec bash\"'"
def run_win(cmd):
# 运行的第一个窗口终端
cmd = win_cmd+cmd+end_cmd
return cmd
def run_tab(cmd, t):
# 运行的标签终端
delay_t = "sleep {};".format(str(t))
cmd = tab_cmd+delay_t+cmd+end_cmd
return cmd
# t 指令运行之间的时间间隔
def run_cmd(cmds, t = 2):
# 合成命令
cmd = run_win(cmds[0])
for i in range(1, len(cmds)):
cmd = cmd + run_tab(cmds[i], t*i)
print("命令合成",cmd)
# 运行指令
os.system(cmd)
# # 单窗口多指令 “;” 间隔
# cmds = ["export TURTLEBOT3_MODEL=waffle;export GAZEBO_MODEL_PATH=$GAZEBO_MODEL_PATH:/opt/ros/humble/share/turtlebot3_gazebo/models;ros2 launch nav2_bringup tb3_simulation_launch.py headless:=False"]
# # 多窗口多指令 列表 , 间隔
# # cmds = ["export TURTLEBOT3_MODEL=waffle","export GAZEBO_MODEL_PATH=$GAZEBO_MODEL_PATH:/opt/ros/humble/share/turtlebot3_gazebo/models","ros2 launch nav2_bringup tb3_simulation_launch.py headless:=False"]
# run_cmd(cmds)
# 按钮样式设置
btn_style = """
QPushButton {
background-color: #87CEEB; /* 背景颜色 */
border-radius: 10px; /* 圆角 */
color: #000000; /* 文字颜色 */
font-size: 25px; /* 字体大小 */
font-family: "Microsoft YaHei"; /* 字体类型 */
padding: 10px 10px; /* 内边距 */
}
QPushButton:hover {
background-color: #00FFFF; /* 鼠标悬停时的背景颜色 */
}
QPushButton:pressed {
background-color: #00BFFF; /* 按下时的背景颜色 */
}
"""
show_style ="""
QPushButton {
background-color: #B0C4DE; /* 背景颜色 */
border-radius: 10px; /* 圆角 */
color: #000000; /* 文字颜色 */
font-size: 1px; /* 字体大小 */
font-family: "Microsoft YaHei"; /* 字体类型 */
padding: 10px 10px; /* 内边距 */
background-image: url({0});
background-position: center center;
}
QPushButton:hover {
background-color: #808080; /* 鼠标悬停时的背景颜色 */
}
QPushButton:pressed {
background-color: #696969; /* 按下时的背景颜色 */
}
"""
current_style = """
QPushButton {
background-color: #00BFFF; /* 背景颜色 */
border-radius: 10px; /* 圆角 */
color: #000000; /* 文字颜色 */
font-size: 25px; /* 字体大小 */
font-family: "Microsoft YaHei"; /* 字体类型 */
padding: 10px 10px; /* 内边距 */
}
"""
# 获取当前目录
current_dir = os.getcwd()
# 获取data文件夹的路径
data_dir = os.path.join(current_dir, 'data')
def get_dirs():
'''
获取当前目录下的data文件夹下的所有分区,及分区下的所有文件夹
返回类型:列表下的字典
[{'专业区': []}, {'办公区': ['a2345', 'WPS']}, {'工具区': []}, {'检测区': ['DiskGenius']}]
'''
result_list = []
# 获取data文件夹中所有文件夹的名称
result_list = [name for name in os.listdir(data_dir) if os.path.isdir(os.path.join(data_dir, name))]
return result_list
class MyMainWindow(QMainWindow,Ui_MainWindow):
def __init__(self,parent =None):
super(MyMainWindow,self).__init__(parent)
# 读取命令行配置文件
self.yaml_dict = None
self.yaml_table = []
self.yaml_table_current = None # 当前选中的组项
self.read_yaml()
# 初始化界面
self.createTrayIcon()
# 热键
# self.hk_switch = SystemHotkey()
# 绑定快捷键
# self.hk_switch.register(('control','alt',"z"),callback=lambda x:self.hotkey_callback())
# 创建按钮
self.create_menu_button()
# 第一次加载触发
self.first_load()
# 读取当前cmd_conf.yaml文件
def read_yaml(self):
with open('cmd_conf.yaml', 'r', encoding='utf-8') as f:
result = yaml.load(f.read(), Loader=yaml.FullLoader)
table_list = list(result.keys()) # 获取字典的键
self.yaml_dict = result
self.yaml_table = table_list
# 托盘设置
def createTrayIcon(self):
# 创建窗口
self.setupUi(self)
self.setWindowIcon(QIcon("tray.png"))
self.setWindowTitle("Cmd Visual V1.0")
# 创建系统托盘图标
self.tray_icon = QSystemTrayIcon(self)
self.tray_icon.setIcon(QIcon("tray.png"))
self.tray_icon.setVisible(True)
# 设置托盘图标的菜单
menu = QMenu()
show_action = QAction("显示窗口", menu)
quit_action = QAction("退出", menu)
menu.addAction(show_action)
menu.addAction(quit_action)
self.tray_icon.setContextMenu(menu)
# 绑定菜单事件
show_action.triggered.connect(self.show)
quit_action.triggered.connect(self.close)
# 创建一个表格布局
self.grid_layout = QGridLayout()
# 事件绑定
def hotkey_callback(self):
#判断当前窗口状态
if self.isHidden():
self.show()
self.activateWindow()
else:
self.hide()
# 列表创建菜单按钮
def create_menu_button(self):
layout_menu = QVBoxLayout()
for i,menu_i in enumerate(self.yaml_table):
menu_i = QPushButton(menu_i)
# 设置手形
menu_i.setCursor(Qt.PointingHandCursor)
# 设置背景色为浅蓝色,圆角,选中时为深蓝色
menu_i.setStyleSheet(btn_style)
# 设置最小高度
menu_i.setMinimumHeight(40)
# 绑定事件
menu_i.clicked.connect(self.button_clicked)
# 添加到布局里
layout_menu.addWidget(menu_i)
if(i==0):
menu_i.setStyleSheet(current_style)
self.frame.setLayout(layout_menu)
# 创建展示区按钮
def create_show_button(self, subName):
temp_list = []
# self.temp_dir = os.path.join(data_dir, subName)
# 获取data文件夹中所有文件夹的名称
temp_list = self.yaml_dict.get(subName).keys()
temp_list = list(temp_list)
# print(temp_list)
# # 根据列表创建按钮
self.buttons = [QPushButton(f"{i}") for i in temp_list]
# 在表格布局中添加按钮
exe_icon = QIcon()
for i, button in enumerate(self.buttons):
button.setCursor(Qt.PointingHandCursor)
# button.setStyleSheet(show_style)
button.setMinimumSize(128, 50)
button.setMaximumSize(128, 50)
self.grid_layout.addWidget(button, i // 6, i % 6)
# 绑定事件
button.clicked.connect(self.start_exe)
# # 添加到主窗口
self.frame_2.setLayout(self.grid_layout)
# 第一次加载时触发
def first_load(self):
self.create_show_button(self.yaml_table[0])
self.yaml_table_current = self.yaml_table[0]
# 按钮点击事件
def button_clicked(self):
#判断是哪个按钮的事件
sender = self.sender()
self.yaml_table_current = sender.text()
# 获取self.frame中QVBoxLayout里的所有按钮
for button in self.frame.findChildren(QPushButton):
# 判断当前按钮是否为点击按钮
if button == sender:
# 设计当前按钮的背景色为深蓝色
sender.setStyleSheet(current_style)
# 清除frame2上的控件
for i in reversed(range(self.grid_layout.count())):
self.grid_layout.itemAt(i).widget().setParent(None)
# 这里触发切换展示区的功能按钮
self.create_show_button(sender.text())
else:
# 设置其他按钮保存原样
button.setStyleSheet(btn_style)
# 点击启动事件
def start_exe(self):
cmd_list = []
# 获取当前按钮的文本
current_test = self.sender().text()
# 获取当前触发的指令
current_cmd = self.yaml_dict.get(self.yaml_table_current).get(current_test)
# 判断是多窗口指令【列表类型】,还是单窗口指令【字符串】
if isinstance(current_cmd, list):
cmd_list = current_cmd
else:
cmd_list.append(current_cmd)
print(cmd_list)
# 运行当前指令
run_cmd(cmd_list)
if __name__ == "__main__":
app = QApplication(sys.argv)
myWin = MyMainWindow()
myWin.show()
sys.exit(app.exec_())
五.打包成可执行文件
这里的代码 xxx.py 改成 上面代码的文件名称
pyinstaller -F -w xxx.py
六.命令行的配置
一级:主题
子任务: shell指令
其中:
主题 -左侧标题栏的名称
子任务-按钮名称
shell指令-一个或在一组的指令,根据需求来
注意:
(1)指令之间有前后关系的,可以在同一窗口运行多指令;
(2)无则可配置成多窗口,独立运行;
(3)指令内部有空格,需要用双引号括起,避免与yaml语法冲突。
# 第一个工具栏
建图:
# 多窗口指令 用 "-" 间隔多窗口指令
录制包:
- "ls"
- "pwd"
- "ls -l"
- "ros2"
- "cd ~"
# 单窗口多指令 用 ";" 间隔多条指令
创建地图: "ls;pwd;ros2"
核对地图: "ros2"
# 第二个工具栏
自驾:
打开可视化: "ls"
# 第三个工具栏
边界:
打开配置文件: "ls"
运行脚本: "pwd"
拷贝到车端: "ros2"
打开车端配置文件: "ls"
超声波:
打开配置文件: "cd ~/legion_framework/modules/perception/uss/uss_detection/bin/conf/perception/uss/uss_detection"
数据可视化:
ros_bag: