From a775b1e78446fb0ceef8f65af411422cc3213aa9 Mon Sep 17 00:00:00 2001
From: lefengyang <lefengyang@tencent.com>
Date: Tue, 15 Sep 2026 15:12:22 +0800
Subject: [PATCH] 图片转点阵图

---
 ledApp/pages/index/index.vue |  293 ++++++++++++++++++++++++++++++++++++++++++++++++++++++----
 1 files changed, 272 insertions(+), 21 deletions(-)

diff --git a/ledApp/pages/index/index.vue b/ledApp/pages/index/index.vue
index 2c326cc..d5f0d68 100644
--- a/ledApp/pages/index/index.vue
+++ b/ledApp/pages/index/index.vue
@@ -56,6 +56,10 @@
 					</view>
 				</view>
 				<view class="item_btn" v-if="dId == item.id">
+					<!-- <uv-button type="primary" shape="circle" text="已存序列" size="small" :plain="true"
+						@click="commandLIST"></uv-button>
+					<uv-button type="primary" shape="circle" :text="screenOn ? '关闭屏幕' : '打开屏幕'" size="small" :plain="true"
+						@click="toggleScreen()"></uv-button> -->
 					<uv-button type="primary" shape="circle" text="调节亮度" size="small" :plain="true"
 						@click="this.$refs.popupBRIG.open();"></uv-button>
 					<uv-button type="primary" shape="circle" text="发送LED" size="small" :plain="true"
@@ -186,7 +190,11 @@
 				baseURL: '',
 				dId: '' ,//已连接的设备id,
 				devKwh: '',
-				platform: ''
+				platform: '',
+				screenOn: true,
+				waitDallThenSend: false, // 标记:收到DALL清除所有信息指令回应后,再开始发送INST帧
+				instTimer: null, // INST发送超时计时器:发送INST帧后等待回应,超过5秒未收到则判定发送失败
+				instSendAborted: false // 标记:INST发送已失败/中止,收到迟到的回应时忽略,不再继续发送
 			}
 		},
 		onLoad() {
@@ -372,7 +380,7 @@
 				console.log('--------------', item);
 
 				uni.showLoading({
-					title: '发送中...',
+					title: '发送中0%',
 					mask: true
 				});
 
@@ -397,7 +405,12 @@
 				}
 
 				if (this.ledData.length > 0) {
-					this.commandINST(this.ledData[this.ledCurrent]);
+					// 重置发送状态:清除残留计时器、重置失败标记
+					this.clearInstTimer();
+					this.instSendAborted = false;
+					// 向智能壳发送INST帧前,先发送一次清除所有信息指令(DALL),待智能壳回应后再开始发送
+					this.waitDallThenSend = true;
+					this.commandDALL();
 				} else {
 					uni.hideLoading();
 					uni.showToast({
@@ -489,7 +502,7 @@
 				var that = this
 				uni.setBLEMTU({
 					deviceId: this.deviceId,
-					mtu: 80, // 最大值一般 512,蓝牙 4.2 通常支持 247
+					mtu: 512, // 最大值一般 512,蓝牙 4.2 通常支持 247
 					success: (res) => {
 						console.log('设置MTU成功:', res);
 						uni.hideLoading();
@@ -582,7 +595,8 @@
 			},
 
 			//向低功耗蓝牙设备特征值中写入二进制数据
-			writeBLECharacteristicValue(buffer) {
+			//keepLoading: 发送INST帧(含DALL前置指令、结束帧)过程中保持进度loading,发送成功后不关闭
+			writeBLECharacteristicValue(buffer, keepLoading = false) {
 				const that = this
 				uni.writeBLECharacteristicValue({
 					deviceId: this.deviceId,
@@ -592,7 +606,9 @@
 					writeType: 'write',
 					success: (res) => {
 						//console.log('写入指令发送成功----', res);
-						uni.hideLoading();
+						if (!keepLoading) {
+							uni.hideLoading();
+						}
 					},
 					fail: (err) => {
 						//console.log('写入指令发送失败----', err);
@@ -641,6 +657,127 @@
 				}, 500);
 
 			},
+			// 切换屏幕状态
+			toggleScreen() {
+				if (this.screenOn) {
+					this.commandBLAC()
+				} else {
+					this.commandOPAC()
+				}
+			},
+			// 关闭屏幕
+			commandBLAC() {
+				console.log('关闭屏幕BLAC----')
+				const startBytes = new Array(10).fill(0x55);
+				const frameHeader = 0x7E;
+				const packageType = 0x41;
+				const command = [0x42, 0x4C, 0x41, 0x43];
+
+				const data = [
+					...startBytes,
+					frameHeader,
+					packageType,
+					...command
+				];
+
+				const crc = this.calcCRC16(data.slice(11));
+				data.push((crc >> 8) & 0xFF);
+				data.push(crc & 0xFF);
+				data.push(frameHeader);
+				console.log('BLAC data----', this.hexArrayToHexString(data))
+
+				const buffer = new Uint8Array(data).buffer;
+				setTimeout(() => {
+					this.writeBLECharacteristicValue(buffer)
+				}, 500);
+			},
+			// 打开屏幕
+			commandOPAC() {
+				console.log('打开屏幕OPAC----')
+				const startBytes = new Array(10).fill(0x55);
+				const frameHeader = 0x7E;
+				const packageType = 0x41;
+				const command = [0x4F, 0x50, 0x41, 0x43];
+
+				const data = [
+					...startBytes,
+					frameHeader,
+					packageType,
+					...command
+				];
+
+				const crc = this.calcCRC16(data.slice(11));
+				data.push((crc >> 8) & 0xFF);
+				data.push(crc & 0xFF);
+				data.push(frameHeader);
+				console.log('OPAC data----', this.hexArrayToHexString(data))
+
+				const buffer = new Uint8Array(data).buffer;
+				setTimeout(() => {
+					this.writeBLECharacteristicValue(buffer)
+				}, 500);
+			},
+			// 清除所有信息指令(DALL)
+			commandDALL() {
+				// 1.手机发指令:启动字节+帧头+包头段(0x43+简易绑定号+屏大小)+DALL+CRC校验码+帧尾。
+				// 2.智能壳回指令:启动字节+帧头+包头段+DALL+返回标志+加密地址+CRC校验码+帧尾。
+				// 构建指令数据
+				const startBytes = new Array(10).fill(0x55); // 10个启动字节
+				const frameHeader = 0x7E; // 帧头
+				const packageType = 0x43; // 包类型
+				// 使用新的简易绑定号生成函数
+				const simpleBindId = this.generateSimpleBindId();
+
+				// 屏大小 (2 bytes)
+				const screenSize = [0x0C, 0x18];
+				const packageHeader = [packageType, ...simpleBindId, ...screenSize];
+				const command = [0x44, 0x41, 0x4C, 0x4C]; // "DALL"的ASCII码
+
+				// 组合数据
+				const data = [
+					...startBytes,
+					frameHeader,
+					...packageHeader,
+					...command
+				];
+				// 计算CRC校验码(从包头段开始计算)
+				const crc = this.calcCRC16(data.slice(11));
+				// 添加CRC和帧尾
+				data.push((crc >> 8) & 0xFF); // CRC高8位
+				data.push(crc & 0xFF); // CRC低8位
+				data.push(frameHeader); // 帧尾
+				console.log('DALL data----', this.hexArrayToHexString(data))
+
+				const buffer = new Uint8Array(data).buffer;
+				setTimeout(() => {
+					this.writeBLECharacteristicValue(buffer, true)
+				}, 50);
+			},
+			// 启动INST发送超时计时器:发送INST帧后等待智能壳回应,超过5秒未收到则判定发送失败
+			startInstTimer() {
+				this.clearInstTimer();
+				this.instTimer = setTimeout(() => {
+					this.handleInstTimeout();
+				}, 5000);
+			},
+			// 清除INST发送超时计时器
+			clearInstTimer() {
+				if (this.instTimer) {
+					clearTimeout(this.instTimer);
+					this.instTimer = null;
+				}
+			},
+			// INST发送超时处理:停止发送并提示发送失败
+			handleInstTimeout() {
+				this.instTimer = null;
+				this.instSendAborted = true;
+				uni.hideLoading();
+				uni.showToast({
+					title: '发送失败',
+					icon: 'none',
+					mask: true
+				});
+			},
 			//添加信息指令(INST)
 			commandINST(hexString) {
 				// 1.手机发指令:启动+帧头+包头段(0x43+简易绑定号+屏大小)+INST+信息属性+文件名+总帧数+当前帧*+块描述+块数据+CRC校验码+帧尾
@@ -676,8 +813,6 @@
 
 				// 块数据
 				const blockDescription = this.hexStringToArray(hexString)
-				//const blockString = "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FF0000FF000000000000000000000000000000000000000000000000000000000000FF0000000000000000000000000000000000000000000000FF0000000000000000FF0000FF00000000000000000000000000000000FF0000FF000000000000000000FF0000000000FF0000FF00FF0000FF0000FF000000000000000000FF00FF0000FF000000000000000000FF00000000000000000000FF000000FF0000FF0000FF0000FF0000000000FF0000000000000000000000FF0000000000000000000000FF0000FF0000000000000000FF0000FF0000000000000000000000000000FF00FF000000000000000000000000FF00FF0000FF000000000000000000FF00000000000000FF0000FF0000FF0000FF0000FF0000000000000000FF0000FF000000000000000000000000FF0000000000FF0000000000000000000000000000000000000000FF0000000000000000000000000000000000FF0000000000000000000000000000000000FF00000000000000000000000000FF0000FF000000FF0000FF0000000000000000000000FF00000000000000FF0000FF000000FF00FF0000FF000000000000FF0000000000000000000000000000000000000000000000FF0000000000FF0000000000000000000000FF0000000000000000000000000000000000000000FF0000000000000000FF0000000000000000FF0000000000000000FF00000000000000FF0000FF0000FF0000FF000000000000000000FF00000000000000FF0000FF0000FF0000FF0000FF000000000000000000000000000000000000FF0000FF0000000000000000FF0000000000000000FF0000000000000000FF0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FF00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-				//const blockDescription = this.hexStringToArray(blockString)
 
 				// 组合数据
 				const data = [
@@ -701,11 +836,19 @@
 				//console.log(this.ledCurrent, '----INST data----', this.hexArrayToHexString(data))
 				//const buffer = new Uint8Array(data).buffer;
 				console.log('第几帧-------',this.ledCurrent)
-				setTimeout(() => {
+				// 根据总帧数和已发送数量更新发送进度(当前帧发送前,已发送ledCurrent帧),范围10%~99%
+				const sendPercent = Math.max(10, Math.min(99, Math.round(this.ledCurrent / this.ledTotal * 100)));
+				uni.showLoading({
+					title: '发送中' + sendPercent + '%',
+					mask: true
+				});
+				setTimeout(async () => {
 					//this.writeBLECharacteristicValue(buffer)
-					this.sendBLEDataInChunks(
-						this.hexArrayToHexString(data)
-					)
+					const ok = await this.sendBLEDataInChunks(this.hexArrayToHexString(data))
+					if (ok) {
+						// 数据发送成功后启动超时计时器,等待智能壳回应,超过5秒未收到则判定发送失败
+						this.startInstTimer()
+					}
 				}, 50);
 			},
 			// 发送结束针
@@ -758,7 +901,9 @@
 
 				const endbuffer = new Uint8Array(endData).buffer;
 				setTimeout(() => {
-					this.writeBLECharacteristicValue(endbuffer)
+					this.writeBLECharacteristicValue(endbuffer, true)
+					// 结束帧发送后启动超时计时器,等待智能壳回应,超过5秒未收到则判定发送失败
+					this.startInstTimer()
 				}, 50);
 			},
 			async sendBLEDataInChunks(hexStr) {
@@ -789,14 +934,19 @@
 							})
 						})
 					} catch (err) {
-						console.error(`🚫 发送中断,第 ${i + 1} 包发送失败`)
+						console.error(`🚫 发送中断,第 ${i + 1} 帧发送失败`, err)
 						uni.hideLoading();
-						break
+						uni.showToast({
+							title: `🚫 发送中断,第 ${i + 1} 帧发送失败`,
+							duration: 4000
+						});
+						return false
 					}
 					// 延迟下一包发送
 					await new Promise(r => setTimeout(r, interval))
 				}
 				//console.log("🎉 数据发送完成")
+				return true
 			},
 			// 读取电量
 			commandPOWR() {
@@ -891,12 +1041,35 @@
 								//that.deviceList[that.listIndex].isLink = true;
 								//that.commandPOWR()
 							} else if (resName == 'LIST') {
-								//that.parseFileList(result)
+								that.parseFileList(result)
 							} else if (resName == 'BRIG') {
 
+							} else if (resName == 'BLAC') {
+								console.log('BLAC----', result)
+								if (that.screenOn) {
+									that.screenOn = false;
+									uni.showToast({
+										title: '屏幕已关闭',
+										icon: 'none',
+										duration: 1500
+									});
+								} else {
+									that.screenOn = true;
+									uni.showToast({
+										title: '屏幕已打开',
+										icon: 'none',
+										duration: 1500
+									});
+								}
 							} else if (resName == 'PLAY') {
 
 							} else if (resName == 'INST') {
+								// 收到INST回应,清除发送超时计时器
+								that.clearInstTimer();
+								// 若已因超时中止发送,忽略迟到的回应,不再继续发送
+								if (that.instSendAborted) {
+									return;
+								}
 								// 添加动画帧
 								that.ledCurrent++
 								if (that.ledCurrent < that.ledTotal) {
@@ -906,15 +1079,24 @@
 									// });
 									that.commandINST(that.ledData[that.ledCurrent]);
 								} else if (that.ledCurrent == that.ledTotal) {
-									// uni.showLoading({
-									// 	title: '发送结束帧',
-									// 	mask: true
-									// });
+									// 所有数据帧已发送完成,发送结束帧,进度显示100%
+									uni.showLoading({
+										title: '发送中100%',
+										mask: true
+									});
 									that.commandEnd()
+								} else {
+									// 结束帧应答(ledCurrent == ledTotal + 1),全部发送完成,隐藏loading
+									uni.hideLoading();
 								}
 
 							} else if (resName == 'DALL') {
-
+								// 清除所有信息指令回应:返回标志0x30工作无异常,0x31工作有异常
+								if (that.waitDallThenSend) {
+									that.waitDallThenSend = false;
+									// 收到DALL回应后,开始发送第一帧INST
+									that.commandINST(that.ledData[that.ledCurrent]);
+								}
 							} else if (resName == 'POWR') {
 								// 查询电量
 								let capacityHex = result.slice(-8, -6); // "0a"
@@ -945,6 +1127,75 @@
 					}
 				});
 			},
+			//查询智能壳已存信息序列(LIST)
+			commandLIST() {
+				//1.手机发指令:启动字节+帧头+包头段(0x43+简易绑定号+屏大小)+LIST+信息属性+CRC校验码+帧尾。
+				//2.智能壳指令:启动字节+帧头+包头段+LIST+返回标志+加密地址+文件数+[信息名1+信息名2+...+信息名n]+CRC校验码+帧尾。
+				// 构建指令数据
+				const startBytes = new Array(10).fill(0x55); // 10个启动字节
+				const frameHeader = 0x7E; // 帧头
+				const packageType = 0x43; // 包类型
+				// 使用新的简易绑定号生成函数
+				const simpleBindId = this.generateSimpleBindId();
+
+				// 屏大小 (2 bytes): 从ID中获取
+				const screenSize = this.hexStringToArray(this.checId.slice(20, 24));
+				const packageHeader = [packageType, ...simpleBindId, ...screenSize];
+				const command = [0x4C, 0x49, 0x53, 0x54]; // "LIST"的ASCII码
+				const infoAttribute = 0x00; // 信息属性: 0=一般信息
+				// 组合数据
+				const data = [
+					...startBytes,
+					frameHeader,
+					...packageHeader,
+					...command,
+					infoAttribute
+				];
+
+				// 计算CRC校验码(从包头段开始计算)
+				const crc = this.calcCRC16(data.slice(11));
+
+				// 添加CRC和帧尾
+				data.push((crc >> 8) & 0xFF); // CRC高8位
+				data.push(crc & 0xFF); // CRC低8位
+				data.push(frameHeader); // 帧尾
+				console.log('LIST data----', this.hexArrayToHexString(data))
+
+				const buffer = new Uint8Array(data).buffer;
+				setTimeout(() => {
+					this.writeBLECharacteristicValue(buffer)
+				}, 1000);
+			},
+			// 获取文件名称文件数量
+			parseFileList(hexString) {
+				// 文件数占1字节(2个十六进制字符
+				const fileCountHex = hexString.slice(32, 34);
+				const fileCount = parseInt(fileCountHex, 16);
+				console.log('文件数:', fileCount);
+
+				// 获取文件名称列表
+				const fileNames = [];
+				let nameStartIndex = 34; // 文件数后开始
+
+				for (let i = 0; i < fileCount; i++) {
+					// 每个信息名占2个字节(4个十六进制字符)
+					const nameHex = hexString.slice(nameStartIndex, nameStartIndex + 4);
+					const nameBytes = nameHex.match(/.{2}/g);
+					if (nameBytes && nameBytes.length === 2) {
+						// 将两个字节转换为ASCII字符
+						const char1 = String.fromCharCode(parseInt(nameBytes[0], 16));
+						const char2 = String.fromCharCode(parseInt(nameBytes[1], 16));
+						const fileName = char1 + char2;
+						fileNames.push({
+							fileName: fileName,
+							nameHex: nameHex
+						});
+					}
+
+					nameStartIndex += 4; // 移动到下一个文件名
+				}
+				console.log('文件名称列表:', fileNames);
+			},
 			// 生成简易绑定号
 			generateSimpleBindId() {
 				if (!this.checId || !this.password) {

--
Gitblit v1.10.0