Parcourir la source

feat: 仿真测试环境准备

HeWang il y a 9 mois
Parent
commit
b2b0b462a7
6 fichiers modifiés avec 260 ajouts et 34 suppressions
  1. 110 22
      electron/main.js
  2. 7 2
      electron/preload.js
  3. 1 1
      src/main.ts
  4. 15 0
      src/utils/axios.js
  5. 108 9
      src/views/ReportView.vue
  6. 19 0
      vite.config.ts

+ 110 - 22
electron/main.js

@@ -4,6 +4,7 @@ import path from 'path';
 import {fileURLToPath} from 'url';
 import fs from 'fs';
 import axios from 'axios';
+import {resolve} from "node:dns";
 
 
 const __filename = fileURLToPath(import.meta.url);
@@ -164,36 +165,123 @@ ipcMain.on('generate-world', (event, {rosbag_path}) => {
 });
 
 
-ipcMain.handle('download-file', async (event, { url, fileName, savePath }) => {
-    try {
-        const response = await axios.get(url, { responseType: 'stream' });
+ipcMain.on('start-container', (event, {zip_file_path, image_name, container_name}) => {
+
+    // 使用 spawn 启动脚本
+    const serviceProcess = spawn('bash', ["/home/cicv/work/pji_desktop/simulation/data_preparation.sh", zip_file_path], { detached: true });
+
+    // 设置为后台进程
+    serviceProcess.unref();
+
+    // 监听输出
+    serviceProcess.stdout.on('data', (data) => {
+        console.log(`第一个脚本的输出: ${data}`);
+        // 根据第一个脚本的输出判断其是否准备好
+        if (data.toString().includes('Data preparation done')) {
+            startSecondScript();
+        }
+    });
+
+    // 监听错误
+    serviceProcess.stderr.on('data', (data) => {
+        console.error(`执行第一个脚本时出错: ${data}`);
+    });
+
+    // 监听关闭
+    serviceProcess.on('close', (data) => {
+        console.log(`第一个脚本已关闭: ${data}`);
+    });
 
+    function startSecondScript() {
+        let command = "bash /home/cicv/work/pji_desktop/simulation/start_container.sh " + image_name + " " + container_name
+        // 启动第二个脚本
+        const script2 = exec(command, (error, stdout, stderr) => {
+            if (error) {
+                console.error(`执行第二个脚本时出错: ${error}`);
+                event.sender.send('generate-world-result', { success: false, output: error });
+                return;
+            }
+            console.log(`第二个脚本的输出: ${stdout}`);
+
+            event.sender.send('start-container-result', { success: true, output: stdout });
+        });
+
+        script2.on('exit', (code) => {
+            console.log(`第二个脚本已退出,退出码: ${code}`);
+        });
+    }
+});
+
+
+ipcMain.handle('download-file', async (event, { url, fileName, savePath, overwriteFlag }) => {
+    try {
         // console.log("url", url)
         // console.log("response", response)
 
         const filePath = path.join(savePath, fileName);
 
-        // 写入文件
-        const writer = fs.createWriteStream(filePath);
-
-        return new Promise((resolve, reject) => {
-            response.data.pipe(writer);
-
-            let error = null;
-            writer.on('error', err => {
-                error = err;
-                writer.close();
-                reject(err);
+        // fs.access(filePath, fs.constants.F_OK, async (err) => {
+        //     if (err) {
+        //         console.log(`文件不存在: ${filePath}`);
+        //
+        //     } else {
+        //         console.log(`文件已存在: ${filePath}`);
+        //         return new Promise(
+        //             resolve({success: true, filePath})
+        //         )
+        //     }
+        // });
+
+        // 查找文件是否存在
+        const fileExists = fs.existsSync(filePath)
+        if (fileExists && !overwriteFlag) { // 已存在且不覆盖
+            return {success: true, filePath}
+        } else { // 不存在
+            const response = await axios.get(url, {responseType: 'stream'});
+            // 写入文件
+            const writer = fs.createWriteStream(filePath);
+
+            return new Promise((resolve, reject) => {
+                response.data.pipe(writer);
+
+                let error = null;
+                writer.on('error', err => {
+                    error = err;
+                    writer.close();
+                    reject(err);
+                });
+
+                writer.on('close', () => {
+                    if (!error) {
+                        resolve({success: true, filePath}); // 返回成功信息
+                    } else {
+                        reject({success: false, error: error.message}); // 返回错误信息
+                    }
+                });
             });
+        }
 
-            writer.on('close', () => {
-                if (!error) {
-                    resolve({ success: true, filePath }); // 返回成功信息
-                } else {
-                    reject({ success: false, error: error.message }); // 返回错误信息
-                }
-            });
-        });
+        // // 写入文件
+        // const writer = fs.createWriteStream(filePath);
+        //
+        // return new Promise((resolve, reject) => {
+        //     response.data.pipe(writer);
+        //
+        //     let error = null;
+        //     writer.on('error', err => {
+        //         error = err;
+        //         writer.close();
+        //         reject(err);
+        //     });
+        //
+        //     writer.on('close', () => {
+        //         if (!error) {
+        //             resolve({ success: true, filePath }); // 返回成功信息
+        //         } else {
+        //             reject({ success: false, error: error.message }); // 返回错误信息
+        //         }
+        //     });
+        // });
     } catch (error) {
         // console.error('下载文件出错:', error);
         throw error;

+ 7 - 2
electron/preload.js

@@ -39,9 +39,14 @@ contextBridge.exposeInMainWorld('electronAPI', {
         ipcRenderer.send('generate-world', {rosbag_path});
     },
     onGenerateWorldResponse: (callback) =>  ipcRenderer.on('generate-world-result', callback),
-    downloadFile: async (url, fileName, savePath) => {
+    startContainer: (zip_file_path, image_name, container_name) => {
+        // 发送事件到主进程
+        ipcRenderer.send('start-container', {zip_file_path, image_name, container_name});
+    },
+    onStartContainerResponse: (callback) =>  ipcRenderer.on('start-container-result', callback),
+    downloadFile: async (url, fileName, savePath, overwriteFlag) => {
         try {
-            return await ipcRenderer.invoke('download-file', { url, fileName, savePath });
+            return await ipcRenderer.invoke('download-file', { url, fileName, savePath, overwriteFlag });
         } catch (error) {
             console.error('下载文件时出错:', error);
             return { success: false, error: error.message };

+ 1 - 1
src/main.ts

@@ -13,4 +13,4 @@ app.use(ElementPlus)
 app.use(createPinia())
 app.use(router)
 
-app.mount('#app')
+app.mount('#app')

+ 15 - 0
src/utils/axios.js

@@ -0,0 +1,15 @@
+import axios from 'axios';
+
+// 创建一个默认实例
+const localInstance = axios.create({
+    baseURL: 'https://api.example.com', // 默认的API地址
+    timeout: 60 * 60 * 1000, // 请求超时时间 (ms)
+});
+
+// 创建另一个实例用于不同的API地址或端口
+const pjiInstance = axios.create({
+    baseURL: 'https://api.otherdomain.com:8080', // 另一个API地址和端口
+    timeout: 10000,
+});
+
+export { localInstance, pjiInstance };

+ 108 - 9
src/views/ReportView.vue

@@ -159,7 +159,6 @@
           </div>
         </div>
 
-
         <el-dialog
             v-model="worldDialogVisible"
             title="生成world"
@@ -192,6 +191,22 @@
             </el-upload>
           </template>
         </el-dialog>
+
+        <el-dialog
+            v-model="simulationDialogVisible"
+            title="启动仿真测试环境"
+            width="400"
+            :before-close="handleClose"
+            :close-on-click-modal="false"
+        >
+          <el-steps style="max-width: 600px" :active="simulation_active" finish-status="success">
+            <el-step title="数据拉取"/>
+            <el-step title="环境准备"/>
+          </el-steps>
+          <template #footer>
+            <el-button @click="goToDetail" :disabled="simulation_active !== 2">开启仿真配置</el-button>
+          </template>
+        </el-dialog>
         <el-table stripe style="background-color: rgba(255,0,0,99%);width: 100%" border :data="tableData"
                   fixed ref="multipleTableRef" @selection-change="handleSelectionChange" :cell-style="{ textAlign: 'center'}" :header-cell-style="{ textAlign: 'center'}">
           <el-table-column type="selection" width="55"/>
@@ -206,7 +221,7 @@
           <el-table-column width="300" fixed="right" label="操作">
             <template v-slot="scope">
               <el-button size="small" type="danger" @click="generateWorld(scope.row)">生成world</el-button>
-              <el-button size="small" type="danger" @click="goToDetail">仿真测试</el-button>
+              <el-button size="small" type="danger" @click="startSimulation(scope.row)">仿真测试</el-button>
               <!--            <el-button size="small" type="danger" @click="goToDetail">算法评价</el-button>-->
             </template>
 
@@ -245,6 +260,7 @@ const value
     = ref(true)
 const update_active = ref(0)
 const world_active = ref(0)
+const simulation_active = ref(0)
 const router = useRouter();
 const algorithmContainerState = ref(false)
 const gazeboState = ref(false)
@@ -255,6 +271,7 @@ const algorithmDialogVisible = ref(false)
 const mapDialogVisible = ref(false)
 const updateDialogVisible = ref(false)
 const worldDialogVisible = ref(false)
+const simulationDialogVisible = ref(false)
 
 const upload = ref<UploadInstance>()
 const uploadUrl = ref<string>()
@@ -313,7 +330,7 @@ const uploadSuccess = (response, file, fileList) => {
 }
 
 const getDockerImages = (filter_prefix) => {
-  axios.get('http://localhost:2375/v1.43/images/json').then(function (response) {
+  axios.get('/docker/v1.43/images/json').then(function (response) {
     // console.log(response.data);
     var images = response.data
         .filter(d => d["RepoTags"].length > 0)
@@ -411,6 +428,8 @@ const isPropertySame= (array, propertyName) => {
   return true; // 所有值都是相同的
 }
 
+
+// 地图更新
 const updateMap = async () => {
   // 检查选择的记录数量
   if (multipleSelection.value.length == 0) {
@@ -437,13 +456,13 @@ const updateMap = async () => {
 
   // 根据id下载地图更新需要的数据(压缩包)
   const id = ids[0]
-  const url = "http://127.0.0.1:8888/map/download/zipfile?id=" + id
+  const url = "http://localhost:8888/map/download/zipfile?id=" + id // 此处url不受配置的代理规则影响,应传递后端完整url地址
   const fileName = "data-" + id + ".zip"
   const savePath = "/home/cicv/work/pji_desktop/tmp_download/map_zip"
 
   // 开启loading
   const loadingInstance = ElLoading.service({fullscreen: false, target: '.el-dialog'})
-  const result = await window.electronAPI.downloadFile(url, fileName, savePath);
+  const result = await window.electronAPI.downloadFile(url, fileName, savePath, false);
   if (!result.success) { // 下载失败
     console.error('File download failed:', result.error);
     ElMessage.error("地图更新数据拉取失败!");
@@ -458,7 +477,7 @@ const updateMap = async () => {
 
 const checkMapbufConsistency = async (ids) => {
  try {
-   const response =  await axios.post('http://127.0.0.1:8888/map/check/mapbufconsistency', ids)
+   const response =  await axios.post('/local/map/check/mapbufconsistency', ids)
    console.log(response.data)
    return response.data.status
  } catch (error) {
@@ -511,6 +530,7 @@ const rviz = () => {
   }
 }
 
+// world文件生成
 const generateWorld = async (row) => {
   const id = row.id
   const equipmentNo = row.equipmentNo
@@ -529,7 +549,7 @@ const generateWorld = async (row) => {
   worldDialogVisible.value = true
 
   // 下载map.bag
-  const result = await window.electronAPI.downloadFile(url, fileName, savePath);
+  const result = await window.electronAPI.downloadFile(url, fileName, savePath, false);
   if (!result.success) { // 下载失败
     console.error('File download failed:', result.error);
     ElMessage.error("地图bag数据拉取失败!");
@@ -538,7 +558,7 @@ const generateWorld = async (row) => {
     // 更新步骤条状态
     world_active.value = 1
     console.log("Starting world generation...")
-    // 执行脚本
+    // 执行脚本 - 生成world,启动gazebo
     window.electronAPI.generateWorld(result.filePath);
     // 开启loading
     const loadingInstance = ElLoading.service({fullscreen: false, target: '.el-dialog'})
@@ -566,6 +586,85 @@ const generateWorld = async (row) => {
   }
 }
 
+// 仿真测试环境准备
+const startSimulation = async (row) => {
+  // 检查是否已选择算法镜像
+  let imageName = selectImageName.value
+  let containerName = imageName.replace(':', '_')
+  if (imageName === "") { // 未选择
+    ElMessage.error("请选择算法镜像!")
+    return
+  }
+
+  // 检查data目录是否存在
+  let response = await axios.get('/local/simulation/check/file/data/status?id=' + row.id);
+  if (!response.data.status){ // 不存在
+    ElMessage.error("data目录不存在,无法执行仿真测试!")
+    return
+  }
+
+  // 检查world文件是否存在
+  response = await axios.get('/local/world/check/file/world/status?id=' + row.id)
+  if (!response.data.status){ // 不存在
+    ElMessage.error("world文件不存在,请先生成并上传world文件!")
+    return
+  }
+
+  // 开启对话框
+  simulationDialogVisible.value = true
+  await nextTick()
+
+  // 根据id下载仿真测试需要的数据(压缩包)
+  const id = row.id
+  const url = "http://localhost:8888/simulation/download/zipfile?id=" + id // 此处url不受配置的代理规则影响,应传递后端完整url地址
+  const fileName = "data-" + id + ".zip"
+  const savePath = "/home/cicv/work/pji_desktop/tmp_download/simulation_zip"
+
+  console.log("Starting download files...")
+  // 开启loading
+  let loadingInstance = ElLoading.service({fullscreen: false, target: '.el-dialog'})
+  const result = await window.electronAPI.downloadFile(url, fileName, savePath, false);
+  if (!result.success) { // 下载失败
+    console.error('File download failed:', result.error);
+    ElMessage.error("仿真测试数据拉取失败!");
+  } else { // 下载成功
+    console.log('File downloaded successfully:', result.filePath);
+    loadingInstance.close()
+    // 更新步骤条状态
+    simulation_active.value = 1
+    // 延时
+    setTimeout(() => {}, 1500)
+    // 开启loading
+    loadingInstance = ElLoading.service({fullscreen: false, target: '.el-dialog'})
+    // 执行脚本 - 数据准备,启动容器
+    window.electronAPI.startContainer(result.filePath, imageName, containerName);
+    // 监听脚本执行状态
+    window.electronAPI.onStartContainerResponse( (event, result) => {
+      if (result.success) { // 脚本执行成功
+        console.log('Script execution completed successfully.')
+        // 关闭loading
+        loadingInstance.close()
+        // 更新步骤条状态
+        simulation_active.value = 2
+        // // 使用 Electron 进行文件删除操作
+        // window.electronAPI.deleteFile(fileName, savePath)
+        // // 监听删除文件的响应
+        // window.electronAPI.onDeleteFileResponse((event, response) => {
+        //   if (response.success) { // 删除成功
+        //     console.log("File deleted successfully.")
+        //   } else { // 删除失败
+        //     console.log(`Error: ${response.message}`)
+        //   }
+        // })
+      } else { // 脚本执行过程中发生错误
+        console.error('Script execution failed.');
+        ElMessage.error("仿真测试环境发生错误!");
+      }
+    })
+
+  }
+}
+
 const queryLine = reactive({
   dataName: '',
   equipmentName: '',
@@ -585,7 +684,7 @@ onBeforeMount(() => {
 })
 
 function page() {
-  axios.get('http://36.110.106.156:11121/open/scene/list?equipmentType=YI_DAO_JI_QI_REN&page=' + currentPage.value + '&size=' + pageSize.value,
+  axios.get('/pji/open/scene/list?equipmentType=YI_DAO_JI_QI_REN&page=' + currentPage.value + '&size=' + pageSize.value,
       {
         headers: {
           "Authorization": "4773hd92ysj54paflw2jem3onyhywxt2"

+ 19 - 0
vite.config.ts

@@ -4,6 +4,25 @@ import path from 'path';
 
 export default defineConfig({
   plugins: [vue()],
+  server: {
+    proxy: {
+      '/local': {
+        target:'http://localhost:8888',
+        changeOrigin: true,
+        rewrite: (path)  => path.replace(/^\/local/,''),
+      },
+      '/pji': {
+        target:'http://36.110.106.156:11121',
+        changeOrigin: true,
+        rewrite: (path)  => path.replace(/^\/pji/,''),
+      },
+      '/docker': {
+        target:'http://localhost:2375',
+        changeOrigin: true,
+        rewrite: (path)  => path.replace(/^\/docker/,''),
+      },
+    },
+  },
   resolve: {
     alias: {
       '@': path.resolve(__dirname, './src'),