xterm + vue3 + websocket 终端界面

news2025/1/4 9:20:50

 xterm.js    下载插件

// xterm
npm install --save xterm

// xterm-addon-fit    使终端适应包含元素
npm install --save xterm-addon-fit

// xterm-addon-attach   通过websocket附加到运行中的服务器进程
npm install --save xterm-addon-attach
<template>
  <div :class="props.type ? 'height305' : 'height160'">
    <el-row>
      <el-col :span="20">
        <div
          :class="['xterm', props.type ? 'heightA' : 'heightB']"
          ref="terminal"
          v-loading="loading"
          element-loading-text="拼命连接中"
        >
          <div class="terminal" id="terminal" ref="terminal"></div>
        </div>
        <div class="textarea">
          <textarea ref="textarea" v-model="quickCmd" />
          <div class="bottomOperate flexEnd">
            <el-button type="primary" @click="sendCmd" :disabled="!quickCmd"
              >发送命令</el-button
            >
          </div>
        </div>
      </el-col>
      <el-col :span="4">
        <div :class="['xtermR', props.type ? 'heightA' : 'heightBR']">
          <el-tabs
            v-model="tabActiveName"
            class="demo-tabs"
            @tab-click="handleClick"
          >
            <el-tab-pane label="常用命令" name="first">
              <div v-if="filteredGroups?.length > 0">
                <div class="marginBottom10">
                  <el-button
                    type="primary"
                    size="small"
                    @click="addCmdGroup('addGroup')"
                    >新增命令组</el-button
                  >
                  <el-button type="primary" size="small" @click="addCmd('add')"
                    >新增命令</el-button
                  >
                </div>
                <el-collapse
                  v-loading="loadingR"
                  :class="props.type ? 'listBoxA' : 'listBoxB'"
                >
                  <el-collapse-item
                    v-for="group in filteredGroups"
                    :name="group.name"
                    :key="group.name"
                    class="custom-collapse-item"
                  >
                    <template #title>
                      <div
                        class="flexSpaceBetween"
                        style="width: 100%"
                        @mouseenter="showActions(group.id, true)"
                        @mouseleave="showActions(group.id, false)"
                      >
                        <span class="collapse-title">{{ group.name }}</span>
                        <span v-show="actionStates[group.id]">
                          <el-button
                            link
                            type="primary"
                            @click="addCmdGroup('editGroup', group, $event)"
                            >编辑</el-button
                          >
                          <el-button
                            link
                            type="primary"
                            @click="del(group.id, 'group', $event)"
                            >删除</el-button
                          >
                        </span>
                      </div>
                    </template>
                    <template #default>
                      <div
                        v-for="item in group.device_command"
                        :key="item.id"
                        class="item flexSpaceBetween paddingRight20 marginBottom10"
                        @mouseenter="showActions1(item.id, true)"
                        @mouseleave="showActions1(item.id, false)"
                      >
                        <span
                          class="usualName"
                          @click="getName(item.name)"
                          :title="item.name"
                          >{{ item.name }}</span
                        >
                        <span v-show="actionStates1[item.id]" class="btns">
                          <el-button
                            link
                            type="primary"
                            @click="addCmd('edit', item, group.id)"
                            >编辑</el-button
                          >
                          <el-button link type="primary" @click="del(item.id)"
                            >删除</el-button
                          >
                        </span>
                      </div>
                    </template>
                  </el-collapse-item>
                </el-collapse>
              </div>
              <div class="flexCenter" v-else>暂无常用命令</div>
            </el-tab-pane>
            <el-tab-pane label="命令记录" name="second">
              <div
                :class="props.type ? 'listBoxA' : 'listBoxB'"
                v-if="globalStore.cmdRecordList?.length > 0"
              >
                <div
                  v-for="item in globalStore.cmdRecordList"
                  :key="item"
                  class="item flexSpaceBetween paddingRight20 marginBottom10"
                >
                  <span class="recordName" @click="getName(item)">{{
                    item
                  }}</span>
                </div>
              </div>
              <div class="flexCenter" v-else>暂无命令记录</div>
            </el-tab-pane>
          </el-tabs>
        </div>
      </el-col>
    </el-row>
  </div>
  <!-- 新增命令组 -->
  <AddTerminalGroup ref="addTerminalGroup" />
  <!-- 新增命令 -->
  <AddTerminal ref="addTerminal" />
</template>
<script setup>
import "xterm/css/xterm.css";
import { Terminal } from "xterm";
import { FitAddon } from "xterm-addon-fit";
import { debounce } from "lodash";
import { ElMessage, ElMessageBox } from "element-plus";
import {
  ref,
  reactive,
  onMounted,
  onBeforeUnmount,
  computed,
  nextTick,
  getCurrentInstance,
} from "vue";
import { useGlobalStore } from "@/stores/modules/global.js";
import AddTerminalGroup from "./AddTerminalGroup.vue";
import AddTerminal from "./AddTerminal.vue";
import {
  commandGroupIndex,
  commandGroupDel,
  commandDel,
} from "@/api/equipment";
import { WebSocketUrl } from "@/api/request";

const props = defineProps({
  type: {
    type: String,
    default: () => {
      return "";
    },
  },
  currentPathRes: {
    type: String,
    default: () => {
      return "/";
    },
  },
});
const globalStore = useGlobalStore();
const { proxy } = getCurrentInstance();
const searchTerm = ref("");
const tabActiveName = ref("first");
const cmdRecordList = ref(globalStore.cmdRecordList); // 命令历史记录
const loadingR = ref(false);
const groups = ref([]);
const quickCmd = ref("");
const actionStates = ref({});
const actionStates1 = ref({});

const filteredGroups = computed(() => {
  if (!searchTerm.value) {
    return groups.value;
  }
  return groups.value
    .map((group) => {
      const filteredItems = group.device_command.filter((item) =>
        item.includes(searchTerm.value)
      );
      return {
        ...group,
        device_command: filteredItems,
      };
    })
    .filter((group) => group.device_command.length > 0);
});

const showActions = (id, show) => {
  actionStates.value[id] = show;
};

const showActions1 = (id, show) => {
  actionStates1.value[id] = show;
};

const addCmdGroup = (type, row, event) => {
  if (event) event.stopPropagation();
  nextTick(() => {
    proxy.$refs["addTerminalGroup"].showDialog({
      type,
      row,
    });
  });
};

const addCmd = (type, row, group_id) => {
  nextTick(() => {
    proxy.$refs["addTerminal"].showDialog({
      type,
      groupList: groups.value,
      row,
      group_id,
    });
  });
};

const getName = (val) => {
  quickCmd.value = val;
};

// 发送命令
const sendCmd = () => {
  if (isWsOpen()) {
    terminalSocket.value.send(quickCmd.value);
    // 处理命令历史记录
    handleCmdRecordList(quickCmd.value);
  }
};

const handleCmdRecordList = (newCmd) => {
  if (newCmd) {
    // 对新命令进行trim处理
    const trimmedCmd = newCmd.trim();
    // 检查是否有重复值并删除
    const index = cmdRecordList.value.indexOf(trimmedCmd);
    if (index !== -1) {
      cmdRecordList.value.splice(index, 1);
    }

    // 将新命令添加到数组最前面
    cmdRecordList.value.unshift(trimmedCmd);

    globalStore.setCmdRecordList(cmdRecordList.value);
  }
};

const del = (id, group, event) => {
  if (event) event.stopPropagation();

  ElMessageBox.confirm("确认删除吗?", "删除", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      if (group) {
        commandGroupDel({ id }).then((res) => {
          if (res.status === 200) {
            ElMessage.success("删除成功");
            getTableData();
          }
        });
      } else {
        commandDel({ id }).then((res) => {
          if (res.status === 200) {
            ElMessage.success("删除成功");
            getTableData();
          }
        });
      }
    })
    .catch(() => {});
};

//获取表格数据
const getTableData = () => {
  loadingR.value = true;
  commandGroupIndex()
    .then((res) => {
      loadingR.value = false;
      if (res.status === 200) {
        groups.value = res.data?.list;
      }
    })
    .catch((error) => {
      loadingR.value = false;
    });
};
// 命令列表
getTableData();
//终端信息
const loading = ref(false);
const terminal = ref(null);
const fitAddon = new FitAddon();
let first = ref(true);
let terminalSocket = ref(null);
let term = ref(null);

// 初始化WS
const initWS = () => {
  if (!terminalSocket.value) {
    createWS();
  }

  if (terminalSocket.value && terminalSocket.value.readyState > 1) {
    terminalSocket.value.close();
    createWS();
  }
};

// 创建WS
const createWS = () => {
  loading.value = true;
  terminalSocket.value = new WebSocket(
    WebSocketUrl + globalStore.wsUrl
  );
  terminalSocket.value.onopen = runRealTerminal; //WebSocket 连接已建立
  terminalSocket.value.onmessage = onWSReceive; //收到服务器消息
  terminalSocket.value.onclose = closeRealTerminal; //WebSocket 连接已关闭
  terminalSocket.value.onerror = errorRealTerminal; //WebSocket 连接出错
};

//WebSocket 连接已建立
const runRealTerminal = () => {
  loading.value = false;
  let sendData = JSON.stringify({
    t: "conn",
  });
  terminalSocket.value.send(sendData);
};
//WebSocket收到服务器消息
const onWSReceive = (event) => {
  // 首次接收消息,发送给后端,进行同步适配尺寸
  if (first.value === true) {
    first.value = false;
    resizeRemoteTerminal();
    if (props.type === "termDia") {
      autoWriteInfo();
    }
  }
  const blob = new Blob([event.data.toString()], {
    type: "text/plain",
  });
  //将Blob 对象转换成字符串
  const reader = new FileReader();
  reader.readAsText(blob, "utf-8");
  reader.onload = (e) => {
    // 可以根据返回值判断使用何种颜色或者字体,不过返回值自带了一些字体颜色
    writeOfColor(reader.result);
  };
};

//WebSocket 连接出错
const errorRealTerminal = (ex) => {
  let message = ex.message;
  if (!message) message = "disconnected";
  term.value.write(`\x1b[31m${message}\x1b[m\r\n`);
  loading.value = false;
};
//WebSocket 连接已关闭
const closeRealTerminal = () => {
  loading.value = false;
};

// 初始化Terminal
const initTerm = () => {
  term.value = new Terminal({
    rendererType: "canvas", //渲染类型
    // rows: 50, //行数,影响最小高度
    // cols: 100, // 列数,影响最小宽度
    convertEol: true, //启用时,光标将设置为下一行的开头
    // scrollback: 50, //终端中的滚动条回滚量
    disableStdin: false, //是否应禁用输入。
    cursorStyle: "underline", //光标样式
    cursorBlink: true, //光标闪烁
    theme: {
      foreground: "#F8F8F8",
      background: "#2D2E2C",
      cursor: "help", //设置光标
      lineHeight: 16,
    },
    fontFamily: '"Cascadia Code", Menlo, monospace',
  });
  // writeDefaultInfo();
  // 弹框自动输入
  term.value.open(terminal.value); //挂载dom窗口
  term.value.loadAddon(fitAddon); //自适应尺寸
  term.value.focus();
  termData(); //Terminal 事件挂载
};

const autoWriteInfo = () => {
  let sendData = "\n" + "cd " + props.currentPathRes + "\n";
  // term.value.write(`\x1b[37m${sendData}\x1b[m`);
  // term.value.write("\r\n");
  if (isWsOpen()) {
    terminalSocket.value.send(sendData);
  }
};

const writeDefaultInfo = () => {
  let defaultInfo = [
    "┌\x1b[1m terminals \x1b[0m─────────────────────────────────────────────────────────────────┐ ",
    "│                                                                            │ ",
    "│  \x1b[1;34m 欢迎使用XS  SSH   \x1b[0m                                                       │ ",
    "│                                                                            │ ",
    "└────────────────────────────────────────────────────────────────────────────┘ ",
  ];
  term.value.write(defaultInfo.join("\n\r"));
  term.value.write("\r\n");
  // writeOfColor('我是加粗斜体红色的字呀', '1;3;', '31m')
};

const writeOfColor = (txt, fontCss = "", bgColor = "") => {
  // 在Linux脚本中以 \x1B[ 开始,中间前部分是样式+内容,以 \x1B[0m 结尾
  // 示例 \x1B[1;3;31m 内容 \x1B[0m
  // fontCss
  // 0;-4;字体样式(0;正常 1;加粗 2;变细 3;斜体 4;下划线)
  // bgColor
  // 30m-37m字体颜色(30m:黑色 31m:红色 32m:绿色 33m:棕色字 34m:蓝色 35m:洋红色/紫色 36m:蓝绿色/浅蓝色 37m:白色)
  // 40m-47m背景颜色(40m:黑色 41m:红色 42m:绿色 43m:棕色字 44m:蓝色 45m:洋红色/紫色 46m:蓝绿色/浅蓝色 47m:白色)
  // console.log("writeOfColor", term)
  term.value.write(`\x1b[37m${fontCss}${bgColor}${txt}\x1b[m`);
  // term.value.write(`\x1B[${fontCss}${bgColor}${txt}\x1B[0m`);
};

// 终端输入触发事件
const termData = () => {
  fitAddon.fit();
  // 输入与粘贴的情况,onData不能重复绑定,不然会发送多次
  term.value.onData((data) => {
    // console.log(data, "传入服务器");
    if (isWsOpen()) {
      terminalSocket.value.send(data);
    }
  });
  // 终端尺寸变化触发
  term.value.onResize(() => {
    resizeRemoteTerminal();
  });
};

//尺寸同步 发送给后端,调整后端终端大小,和前端保持一致,不然前端只是范围变大了,命令还是会换行
const resizeRemoteTerminal = () => {
  const { cols, rows } = term.value;
  if (isWsOpen()) {
    terminalSocket.value.send(
      JSON.stringify({
        t: "resize",
        width: rows,
        height: cols,
      })
    );
  }
};

// 是否连接中0 1 2 3 状态
const isWsOpen = () => {
  // console.log(terminalSocket.value, "terminalSocket.value");
  const readyState = terminalSocket.value && terminalSocket.value.readyState;
  return readyState === 1;
};

// 适应浏览器尺寸变化
const fitTerm = () => {
  fitAddon.fit();
};
const onResize = debounce(() => fitTerm(), 500);
const onTerminalResize = () => {
  window.addEventListener("resize", onResize);
};
const removeResizeListener = () => {
  window.removeEventListener("resize", onResize);
};

//*生命周期函数
onMounted(() => {
  initWS();
  initTerm();
  onTerminalResize();
});

onBeforeUnmount(() => {
  removeResizeListener();
  let sendData = JSON.stringify({
    t: "close",
  });
  if (isWsOpen()) {
    terminalSocket.value.send(sendData);
    terminalSocket.value && terminalSocket.value.close();
  }
});

// 暴露方法
defineExpose({ getTableData });
</script>
<style lang="scss" scoped>
.xterm {
  position: relative;
  width: 100%;
  background: rgb(45, 46, 44);
}

.xtermR {
  position: relative;
  width: 100%;
  background: #fff;
  padding: 10px;
  position: relative;
  // overflow: hidden;
  .listBoxA {
    overflow-y: auto;
    height: calc(100vh - 450px);
  }
  .listBoxB {
    overflow-y: auto;
    height: calc(100vh - 300px);
  }
}

.heightA {
  height: calc(100vh - 400px);
}
.heightB {
  height: calc(100vh - 235px);
}
.heightBR {
  height: calc(100vh - 155px);
}

.usualName {
  width: calc(100% - 80px);
  display: inline-block;
  cursor: pointer;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.btns {
  width: 80px;
}

.textarea {
  overflow: hidden;
  position: relative;
  height: 80px;
  background: #ffffff;

  textarea {
    width: 100%;
    height: 90px;
    border: 0 none;
    outline: none;
    resize: none;
    font-size: 15px;
    overflow-y: auto;
    padding: 5px;
    background: #ffffff;
  }
  .bottomOperate {
    position: absolute;
    right: 10px;
    bottom: 10px;
  }
}
.recordName {
  font-size: 13px;
  color: #303133;
  cursor: pointer;
  margin-bottom: 10px;
  width: 100%;
}
.flexCenter {
  font-size: 14px;
  padding-top: 150px;
}
</style>

 此页面兼容了弹框和非弹框页面,做了两种样式处理判断

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2269118.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

记一次护网通过外网弱口令一路到内网

视频教程在我主页简介或专栏里 目录&#xff1a; 资产收集 前期打点 突破 完结 又是年底护网季&#xff0c;地市护网有玄机&#xff0c;一路磕磕又绊绊&#xff0c;终是不负领导盼。 扯远了-_-!!&#xff0c;年底来了一个地市级护网&#xff0c;开头挺顺利的&#xff0c…

XIAO ESP32 S3网络摄像头——2视频获取

本文主要是使用XIAO Esp32 S3制作网络摄像头的第2步,获取摄像头图像。 1、效果如下: 2、所需硬件 3、代码实现 3.1硬件代码: #include "WiFi.h" #include "WiFiClient.h" #include "esp_camera.h" #include "camera_pins.h"// 设…

uniapp:微信小程序文本长按无法出现复制菜单

一、问题描述 在集成腾讯TUI后&#xff0c;为了能让聊天文本可以复制&#xff0c;对消息组件的样式进行修改&#xff0c;主要是移除下面的user-select属性限制&#xff1a; user-select: none;-webkit-user-select: none;-khtml-user-select: none;-moz-user-select: none;-ms…

2025:OpenAI的“七十二变”?

朋友们&#xff0c;准备好迎接AI的狂欢了吗&#xff1f;&#x1f680; 是不是跟我一样&#xff0c;每天醒来的第一件事就是看看AI领域又有什么新动向&#xff1f; 尤其是那个名字如雷贯耳的 OpenAI&#xff0c;简直就是AI界的弄潮儿&#xff0c;一举一动都牵动着我们这些“AI发…

无人机频射信号检测数据集,平均正确识别率在94.3%,支持yolo,coco json,pasical voc xml格式的标注,364张原始图片

无人机频射信号检测数据集&#xff0c;平均正确识别率在94.3&#xff05;&#xff0c;支持yolo&#xff0c;coco json&#xff0c;pasical voc xml格式的标注&#xff0c;364张原始图片 可识别下面的信号&#xff1a; 图像传输信号LFST &#xff08;Image_Transmission_sign…

柱状图中最大的矩形 - 困难

************* c topic: 84. 柱状图中最大的矩形 - 力扣&#xff08;LeetCode&#xff09; ************* chenck the topic first: Think about the topics I have done before. the rains project comes:盛最多水的容器 - 中等难度-CSDN博客https://blog.csdn.net/ElseWhe…

第17篇 使用数码管实现计数器___ARM汇编语言程序<四>

Q&#xff1a;如何使用定时器实现数码管循环计数器&#xff1f; A&#xff1a;DE1-SoC_Computer系统有许多硬件定时器&#xff0c;本次实验使用A9 Private Timer定时器实现延时&#xff1a;定时器首先向Load寄存器写入计数值&#xff0c;然后向Control寄存器中的使能位E写1来启…

SSM 进销存系统

&#x1f942;(❁◡❁)您的点赞&#x1f44d;➕评论&#x1f4dd;➕收藏⭐是作者创作的最大动力&#x1f91e; &#x1f496;&#x1f4d5;&#x1f389;&#x1f525; 支持我&#xff1a;点赞&#x1f44d;收藏⭐️留言&#x1f4dd;欢迎留言讨论 &#x1f525;&#x1f525;&…

通过Cephadm工具搭建Ceph分布式存储以及通过文件系统形式进行挂载的步骤

1、什么是Ceph Ceph是一种开源、分布式存储系统&#xff0c;旨在提供卓越的性能、可靠性和可伸缩性。它是为了解决大规模数据存储问题而设计的&#xff0c;使得用户可以在无需特定硬件支持的前提下&#xff0c;通过普通的硬件设备来部署和管理存储解决方案。Ceph的灵活性和设计…

【Rust自学】8.4. String类型 Pt.2:字节、标量值、字形簇以及字符串的各类操作

8.4.0. 本章内容 第八章主要讲的是Rust中常见的集合。Rust中提供了很多集合类型的数据结构&#xff0c;这些集合可以包含很多值。但是第八章所讲的集合与数组和元组有所不同。 第八章中的集合是存储在堆内存上而非栈内存上的&#xff0c;这也意味着这些集合的数据大小无需在编…

svn分支相关操作(小乌龟操作版)

在开发工作中进行分支开发&#xff0c;涉及新建分支&#xff0c;分支切换&#xff0c;合并分支等 新建远程分支 右键选择branch/tagert按钮 命名分支的路径名称 点击确定后远程分支就会生成一个当时命名的文件夹&#xff08;开发分支&#xff09; 分支切换 一般在开发阶段&a…

24年收尾之作------动态规划<六> 子序列问题(含对应LeetcodeOJ题)

目录 引例 经典LeetCode OJ题 1.第一题 2.第二题 3.第三题 4.第四题 5.第五题 6.第六题 7.第七题 引例 OJ传送门 LeetCode<300>最长递增子序列 画图分析: 使用动态规划解决 1.状态表示 dp[i]表示以i位置元素为结尾的子序列中&#xff0c;最长递增子序列的长度 2.…

蓝牙|软件 Qualcomm S7 Sound Platform开发系列之初级入门指南

本文适用范围 ADK24.2~ 问题/功能描述 S7开发环境搭建与编译介绍 实现方案 本文介绍适用于windows平台Application部分,audio ss的说明会在下一篇文章在做说明,Linux平台如果不进行AI算法的开发,个人认知是没有必要配置,若是做服务器倒是不错的选择.因为编译完成后烧录调试还…

Redis - 4 ( 9000 字 Redis 入门级教程 )

一&#xff1a; Zset 有序集合 1.1 常用命令 有序集合在 Redis 数据结构中相较于字符串、列表、哈希和集合稍显陌生。它继承了集合中元素不允许重复的特点&#xff0c;但与集合不同的是&#xff0c;有序集合的每个元素都关联一个唯一的浮点分数&#xff08;score&#xff09;…

ubuntu 使用samba与windows共享文件[注意权限配置]

在Ubuntu上使用Samba服务与Windows系统共享文件&#xff0c;需要正确配置Samba服务以及相应的权限。以下是详细的步骤&#xff1a; 安装Samba 首先&#xff0c;确保你的Ubuntu系统上安装了Samba服务。 sudo apt update sudo apt install samba配置Samba 安装完成后&#xff0c…

打印进度条

文章目录 1.Python语言实现(1)黑白色(2)彩色&#xff1a;蓝色 2.C语言实现(1)黑白颜色(2)彩色版&#xff1a;红绿色 1.Python语言实现 (1)黑白色 import sys import timedef progress_bar(percentage, width50):"""打印进度条:param percentage: 当前进度百分比…

深度解析 LDA 与聚类结合的文本主题分析实战

🌟作者简介:热爱数据分析,学习Python、Stata、SPSS等统计语言的小高同学~🍊个人主页:小高要坚强的博客🍓当前专栏:《Python之文本分析》🍎本文内容:深度解析 LDA 与聚类结合的文本主题分析实战🌸作者“三要”格言:要坚强、要努力、要学习 目录 引言 技术框架…

点跟踪基准最早的论文学习解读:TAP-Vid: A Benchmark for Tracking Any Point in a Video—前置基础

TAP-Vid: A Benchmark for Tracking Any Point in a Video— TAP-Vid&#xff1a;跟踪视频中任意点的基准、 学习这一篇文章的本来的目的是为了学习一下TAP-NET便于理解后面用到的TAPIR方法的使用。 文章目录 TAP-Vid: A Benchmark for Tracking Any Point in a Video— TAP-V…

C进阶-字符串与内存函数介绍(另加2道典型面试题)

满意的话&#xff0c;记得一键三连哦&#xff01; 我们先看2道面试题 第一道&#xff1a; 我们画图理解&#xff1a; pa&#xff0c;先使用再&#xff0c;pa开始指向a【0】&#xff0c;之后pa向下移动一位&#xff0c;再解引用&#xff0c;指向a【1】&#xff0c;a【1】又指向…

PH47代码框架 24241231 重要更新

仪式感一下&#xff1a;2024年最后一天&#xff0c;发布 PH47 代码框架的一次重要更新。当然这并不是有意的&#xff0c;而是直到现在才把更新的所有工作全部做完&#xff08;希望确实如此&#xff09;。 本次更新要点&#xff1a; 1、加入多IMU支持。本次更新正式加入对 MPU65…