index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. <template>
  2. <view class="container">
  3. <!-- 头部区域 -->
  4. <BluetoothHeader
  5. @ble-data-received="handleBleDataReceived"
  6. />
  7. <!-- 主要内容区域 -->
  8. <view class="main-content">
  9. <!-- 显示区域 -->
  10. <view class="display-area">
  11. <view class="display-window">
  12. <view
  13. v-for="(item, index) in deviceListDataShow"
  14. :key="item.id"
  15. :class="['list-item', { 'list-item-selected': item.id === selectedDeviceId }]"
  16. hover-class="list-item-hover"
  17. hover-start-time="0"
  18. hover-stay-time="100"
  19. @click="selectblue(item.id)"
  20. >
  21. <image v-if="item.manufacturer==='eciot'" src="/static/img/ecble.png" class="list-item-img"></image>
  22. <text class="list-item-name">{{item.id}}:{{ item.name }}&nbsp;</text>
  23. <image v-if="item.rssi >= -41" src="/static/img/s5.png" mode="aspectFit" class="list-item-rssi-img"></image>
  24. <image v-else-if="item.rssi >= -55" src="/static/img/s4.png" mode="aspectFit"
  25. class="list-item-rssi-img"></image>
  26. <image v-else-if="item.rssi >= -65" src="/static/img/s3.png" mode="aspectFit"
  27. class="list-item-rssi-img"></image>
  28. <image v-else-if="item.rssi >= -75" src="/static/img/s2.png" mode="aspectFit"
  29. class="list-item-rssi-img"></image>
  30. <image v-else="item.rssi < -75" src="/static/img/s1.png" mode="aspectFit"
  31. class="list-item-rssi-img"></image>
  32. </view>
  33. </view>
  34. </view>
  35. <!-- 控制按钮区域 -->
  36. <view class="control-area">
  37. <!-- 蓝牙控制行 -->
  38. <view class="control-row">
  39. <u-button type="info" size="medium" class="control-btn" @click="closeBlue()">断开蓝牙</u-button>
  40. <u-button type="info" size="medium" class="control-btn" @click="openBluetoothAdapter()">搜索蓝牙</u-button>
  41. <u-button type="info" size="medium" class="control-btn" @click="listViewTap()">连接蓝牙</u-button>
  42. <u-button type="info" size="medium" class="control-btn" @click="readAddress()">读取地址</u-button>
  43. </view>
  44. <!-- 设备地址行 -->
  45. <u-row gutter="10" customStyle="margin-bottom: 10px">
  46. <u-col span="3">
  47. <view class="label-box">设备地址:</view>
  48. </u-col>
  49. <u-col span="5">
  50. <u-input v-model="deviceAddress" type="number" class="address-input"></u-input>
  51. </u-col>
  52. <u-col span="4">
  53. <u-button type="info" size="medium" class="control-btn" @click="updataAddress()">更新通讯地址</u-button>
  54. </u-col>
  55. </u-row>
  56. <!-- 新设备地址行 -->
  57. <u-row gutter="10" customStyle="margin-bottom: 10px">
  58. <u-col span="3">
  59. <view class="label-box">设备新地址:</view>
  60. </u-col>
  61. <u-col span="5">
  62. <u-input v-model="newDeviceAddress" type="number" placeholder="" class="address-input"></u-input>
  63. </u-col>
  64. <u-col span="4">
  65. <u-button type="info" size="medium" class="control-btn" @click="writeAddress()">写地址</u-button>
  66. </u-col>
  67. </u-row>
  68. </view>
  69. <DeviceStatusInfo/>
  70. </view>
  71. </view>
  72. </template>
  73. <script>
  74. import BluetoothHeader from '@/pages/components/header.vue';
  75. import DeviceStatusInfo from '@/pages/components/DeviceStatusInfo.vue';
  76. import {ecBLE,ecUI,initBLE,writeRegister,startHeartbeat,stopHeartbeat,setGlobalSlaveAddress,getGlobalSlaveAddress,setAgreement,getConnected} from '@/utils/modbus.js';
  77. import i18 from '@/utils/i18.js';
  78. let deviceListData = [];
  79. let ctx;
  80. export default {
  81. components: {
  82. BluetoothHeader,
  83. DeviceStatusInfo
  84. },
  85. data() {
  86. return {
  87. step:"",
  88. editStatus: false,
  89. selectedDeviceId: null, //选中的设备
  90. timer: "", //设备列表数据定时刷新
  91. deviceListDataShow: [
  92. {
  93. id: 1,
  94. name: "设备1",
  95. rssi: -55
  96. },
  97. {
  98. id: 2,
  99. name: "设备2",
  100. rssi: -55
  101. },
  102. ], //蓝牙设备列表
  103. showTimer: null,
  104. connected: false,
  105. sendData: "",
  106. newDeviceAddress:'',
  107. deviceAddress:'',
  108. communicationLink_one: false,
  109. communicationTimer: null, // 添加这一行用于保存定时器 ID
  110. }
  111. },
  112. onLoad() {
  113. uni.setNavigationBarTitle({
  114. title: "蓝牙配网"
  115. })
  116. ecUI.showLoading("正在初始化蓝牙模块")
  117. ctx = this;
  118. clearInterval(this.timer);
  119. this.timer = setInterval(() => {
  120. // 在添加或更新设备后对数组进行排序
  121. deviceListData.sort((a, b) => b.rssi - a.rssi);
  122. ctx.deviceListDataShow = JSON.parse(JSON.stringify(deviceListData))
  123. }, 800)
  124. initBLE();
  125. },
  126. onUnload() {
  127. ecBLE.stopBluetoothDevicesDiscovery();
  128. ecBLE.closeBLEConnection()
  129. },
  130. onShow() {
  131. if (this.showTimer != null) {
  132. clearTimeout(this.showTimer);
  133. }
  134. this.showTimer = setTimeout(() => {
  135. ctx.openBluetoothAdapter()
  136. }, 100)
  137. },
  138. methods: {
  139. // 处理从子组件传递过来的蓝牙数据
  140. handleBleDataReceived(data) {
  141. console.log('在父组件中接收到蓝牙数据:', data);
  142. this.updateSensorData(data);
  143. },
  144. agreementTypeSelect(e){
  145. this.agreementTypeName = e.name;
  146. setAgreement(this.agreementTypeName);
  147. },
  148. async readAddress(){
  149. try {
  150. // 发送读取地址指令
  151. writeRegister("GET_ADDRESS", null);
  152. // 等待地址响应,最多3秒
  153. const data = await this.waitForAddressResponse(3000);
  154. console.log('接收到的数据地址:', data.Addres_23);
  155. this.deviceAddress = data.Addres_23;
  156. // 如果有地址则更新通讯地址
  157. if (this.deviceAddress != null && this.deviceAddress != '') {
  158. this.updataAddress();
  159. }
  160. return data;
  161. } catch (error) {
  162. console.error('读取地址失败:', error);
  163. this.$modal.showToast("读取地址超时");
  164. throw error;
  165. }
  166. },
  167. closeBlue(){
  168. ecUI.showLoading("正在断开并重新扫描");
  169. ecBLE.closeBLEConnection();
  170. stopHeartbeat();
  171. // 重新开始蓝牙扫描以获取设备列表
  172. deviceListData = []; // 清空当前设备列表
  173. this.startBluetoothDevicesDiscovery(); // 开始重新扫描
  174. // 可选:在一段时间后隐藏加载提示
  175. setTimeout(() => {
  176. this.communicationLink_one = false
  177. ecUI.hideLoading();
  178. }, 2000);
  179. },
  180. updateSensorData(data){
  181. console.log('接收到的数据地址:', data.Addres_23);
  182. this.deviceAddress = data.Addres_23;
  183. },
  184. updataAddress(){
  185. if (this.deviceAddress == null || this.deviceAddress == '') {
  186. this.$modal.showToast("请输入通讯地址");
  187. return;
  188. }
  189. let value = parseInt(this.deviceAddress, 10);
  190. setGlobalSlaveAddress(value);
  191. startHeartbeat();
  192. },
  193. writeAddress(){
  194. ecUI.showLoading("正在写入地址,请稍后!")
  195. stopHeartbeat();
  196. setTimeout(() => {
  197. writeRegister("WRITE_ADDRESS",this.newDeviceAddress);
  198. }, 1000);
  199. setTimeout(() => {
  200. this.communicationLink_one = false
  201. ecUI.hideLoading();
  202. }, 2000);
  203. },
  204. i18(text){
  205. return text;
  206. },
  207. $t(title) {
  208. return title;
  209. },
  210. selectblue(id){ //选中需要连接的蓝牙
  211. this.selectedDeviceId = id;
  212. },
  213. listViewTap(){ //连接蓝牙
  214. const id = this.selectedDeviceId;
  215. // 校验设备ID
  216. if (!id) {
  217. this.$modal.showToast("请选择一个设备");
  218. return;
  219. }
  220. // 清除旧的连接状态
  221. this.connected = false;
  222. ecUI.showLoading("正在连接蓝牙");
  223. // 建立连接
  224. ecBLE.createBLEConnection(id);
  225. // 设置连接超时
  226. setTimeout(() => {
  227. this.handleConnectionTimeout();
  228. }, 5000);
  229. },
  230. // 超时处理函数
  231. handleConnectionTimeout() {
  232. this.connected = getConnected();
  233. ecUI.hideLoading();
  234. if (!this.connected) {
  235. this.$modal.showToast(i18('连接失败'));
  236. this.startBluetoothDevicesDiscovery();
  237. }
  238. },
  239. openBluetoothAdapter() { //打开蓝牙
  240. ecBLE.onBluetoothAdapterStateChange(res => {
  241. ecUI.hideLoading()
  242. if (res.ok) {
  243. ctx.startBluetoothDevicesDiscovery()
  244. } else {
  245. ecUI.showModal(
  246. this.$t('buletooth.tip'),
  247. `Bluetooth adapter error | ${res.errCode} | ${res.errMsg}`,
  248. () => {
  249. }
  250. )
  251. }
  252. })
  253. ecBLE.openBluetoothAdapter()
  254. },
  255. startBluetoothDevicesDiscovery() {
  256. ecBLE.stopBluetoothDevicesDiscovery();
  257. console.log('start search')
  258. ecBLE.onBluetoothDeviceFound(res => {
  259. let isRight = true;
  260. if (!isRight) {
  261. return;
  262. }
  263. for (const item of deviceListData) {
  264. if (item.id === res.id) {
  265. item.name = res.name
  266. item.rssi = res.rssi
  267. return
  268. }
  269. }
  270. let manufacturer = ''
  271. if (res.name.length === 11 && res.name.startsWith('@')) {
  272. manufacturer = 'eciot'
  273. }
  274. if (res.name.length === 15 && res.name.startsWith('BT_')) {
  275. manufacturer = 'eciot'
  276. }
  277. manufacturer = 'eciot'
  278. deviceListData.push({
  279. id: res.id,
  280. name: res.name,
  281. rssi: res.rssi,
  282. manufacturer,
  283. })
  284. })
  285. ecBLE.startBluetoothDevicesDiscovery()
  286. },
  287. async autoLinkList() {
  288. // 获取前五个设备进行配网通讯
  289. for (let i = 0; i < Math.min(5, this.deviceListDataShow.length); i++) {
  290. const device = this.deviceListDataShow[i];
  291. try {
  292. this.step = `正在处理设备 ${i+1}/${Math.min(5, this.deviceListDataShow.length)}: 断开当前连接`;
  293. if (getConnected()){
  294. // 断开蓝牙
  295. ecBLE.closeBLEConnection();
  296. // 等待断开连接(等待connected为false)
  297. const isDisconnected = await this.waitForBluetoothState(false, 2000);
  298. if (!isDisconnected) {
  299. this.step = `设备 ${device.name} 断开连接失败,跳过该设备`;
  300. continue;
  301. }
  302. }
  303. // 连接蓝牙
  304. this.step = `正在连接设备: ${device.name}`;
  305. ecUI.showLoading(`正在连接 ${device.name}`);
  306. // 建立连接
  307. ecBLE.createBLEConnection(device.id);
  308. // 等待连接建立(等待connected为true)
  309. const isConnected = await this.waitForBluetoothState(true, 5000);
  310. if (isConnected) {
  311. this.step = `连接成功,正在读取设备地址信息`;
  312. // 获取设备地址并等待响应
  313. try {
  314. const addressData = await this.readAddress();
  315. this.step = `设备地址读取成功: ${this.deviceAddress}`;
  316. // 进行其他通讯操作
  317. // ...
  318. this.step = `设备 ${device.name} 处理完成`;
  319. } catch (error) {
  320. console.error('读取设备地址失败:', error);
  321. this.step = `读取设备 ${device.name} 地址失败,跳过该设备`;
  322. }
  323. } else {
  324. this.step = `连接 ${device.name} 失败,跳过该设备`;
  325. }
  326. ecUI.hideLoading();
  327. // 设备间间隔1秒
  328. await this.sleep(1000);
  329. } catch (error) {
  330. console.error(`处理设备 ${device.name} 时出错:`, error);
  331. this.step = `处理设备 ${device.name} 出错,跳过`;
  332. await this.sleep(1000);
  333. }
  334. }
  335. this.step = "批量连接处理已完成";
  336. },
  337. // 通用的等待状态方法
  338. waitForBluetoothState(targetState, timeout = 2000, interval = 200) {
  339. return new Promise((resolve) => {
  340. const startTime = Date.now();
  341. const checkState = () => {
  342. const currentState = getConnected();
  343. const isTargetState = currentState === targetState;
  344. // 如果达到目标状态或者超时,则resolve
  345. if (isTargetState || (Date.now() - startTime) >= timeout) {
  346. resolve(isTargetState);
  347. } else {
  348. // 继续检查
  349. setTimeout(checkState, interval);
  350. }
  351. };
  352. checkState();
  353. });
  354. },
  355. // 延时方法
  356. sleep(ms) {
  357. return new Promise(resolve => setTimeout(resolve, ms));
  358. },
  359. waitForAddressResponse(timeout = 3000) {
  360. return new Promise((resolve, reject) => {
  361. // 保存原始的 handleBleDataReceived 方法
  362. const originalHandler = this.handleBleDataReceived;
  363. let timeoutId;
  364. // 创建新的处理函数
  365. const newHandler = (data) => {
  366. // 检查是否包含地址数据
  367. if (data && data.Addres_23 !== undefined) {
  368. clearTimeout(timeoutId);
  369. // 恢复原始处理函数
  370. this.handleBleDataReceived = originalHandler;
  371. resolve(data);
  372. } else {
  373. // 如果不是地址数据,调用原始处理函数
  374. originalHandler.call(this, data);
  375. }
  376. };
  377. // 替换处理函数
  378. this.handleBleDataReceived = newHandler;
  379. // 设置超时
  380. timeoutId = setTimeout(() => {
  381. this.handleBleDataReceived = originalHandler;
  382. reject(new Error('等待地址响应超时'));
  383. }, timeout);
  384. });
  385. },
  386. }
  387. }
  388. </script>
  389. <style lang="scss" >
  390. .container {
  391. height: calc(100vh - 100px);
  392. background-color: #f5f5f5;
  393. display: flex;
  394. flex-direction: column;
  395. }
  396. // 主要内容区域
  397. .main-content {
  398. flex: 1;
  399. padding: 15px;
  400. display: flex;
  401. flex-direction: column;
  402. gap: 20px;
  403. }
  404. // 显示区域
  405. .display-area {
  406. .display-window {
  407. width: 100%;
  408. height: 230px;
  409. background-color: #000;
  410. border: 2px solid #ddd;
  411. border-radius: 8px;
  412. overflow: auto;
  413. }
  414. }
  415. // 控制区域
  416. .control-area {
  417. display: flex;
  418. flex-direction: column;
  419. gap: 15px;
  420. margin-top: 10px;
  421. }
  422. .control-row {
  423. display: flex;
  424. align-items: center;
  425. gap: 10px;
  426. margin-bottom: 10px;
  427. .control-btn {
  428. flex: 1;
  429. background-color: white;
  430. color: #333;
  431. border: 1px solid #ddd;
  432. border-radius: 6px;
  433. font-size: 14px;
  434. height: 40px;
  435. font-size: 13px;
  436. }
  437. .label-box {
  438. width: 80px;
  439. }
  440. .address-input {
  441. flex: 1;
  442. background-color: white;
  443. border: 1px solid #ddd;
  444. border-radius: 6px;
  445. height: 40px;
  446. padding: 0 12px;
  447. }
  448. }
  449. .list-item {
  450. height: 40px;
  451. background-color: #000;
  452. color: #fff;
  453. position: relative;
  454. }
  455. .list-item-selected {
  456. background-color: #767a82;
  457. color: #000;
  458. }
  459. .list-item-name {
  460. position: absolute;
  461. font-size: 16px;
  462. left: 20px;
  463. top: 0px;
  464. line-height: 40px;
  465. color: #fff;
  466. max-width: 80%;
  467. overflow-x: hidden;
  468. }
  469. .list-item-rssi-img {
  470. position: absolute;
  471. width: 20px;
  472. height: 20px;
  473. right: 20px;
  474. top: 13px;
  475. }
  476. .list-item-rssi {
  477. position: absolute;
  478. width: 40px;
  479. height: 40px;
  480. right: 0px;
  481. font-size: 12px;
  482. font-weight: bold;
  483. display: flex;
  484. justify-content: center;
  485. color: white;
  486. line-height: 50px;
  487. }
  488. .list-item-line {
  489. position: absolute;
  490. height: 1px;
  491. width: 100vw;
  492. left: 20px;
  493. background-color: #c6c6c8;
  494. }
  495. ::v-deep .u-button--medium{
  496. font-size: 13px !important;
  497. }
  498. </style>