This commit is contained in:
2026-06-27 20:58:16 +08:00
parent b13668095d
commit 451dd2330b
10 changed files with 1346 additions and 133 deletions
+179 -67
View File
@@ -160,6 +160,12 @@ class BluetoothPrinterService {
// 连接状态监听器
this._connectionListener = null;
// writeType 缓存(探测一次后记住,避免重复探测)
this._writeType = null; // null | 'writeNoResponse' | 'write'
// MTU 缓存(连接时通过 setBLEMTU 协商,蓝牙 5.0 理论最大 512)
this._mtu = 20;
}
// ==================== 初始化 ====================
@@ -323,7 +329,7 @@ class BluetoothPrinterService {
try {
console.log('尝试使用已保存的服务配置:', { savedServiceId, savedWriteCharId });
// 建立连接
// 建立连接
await new Promise((res, rej) => {
uni.createBLEConnection({
deviceId,
@@ -333,33 +339,28 @@ class BluetoothPrinterService {
});
});
// 等待服务发现
// MTU 协商(蓝牙 5.0 理论最大 512,实际由设备/系统决定)
try {
const mtuRes = await new Promise((res) => {
uni.setBLEMTU({
deviceId,
mtu: 512,
success: res,
fail: () => res({ mtu: 23 })
});
});
this._mtu = (mtuRes.mtu || 512) - 3;
console.log(`[打印] MTU 协商: 请求 512, 实际=${mtuRes.mtu}, 写入块=${this._mtu}`);
} catch (e) {
console.warn('[打印] MTU 协商异常, 使用默认值 20', e);
this._mtu = 20;
}
// 等待服务发现(像 configPrinter 一样)
await this._delay(800);
// 验证保存的服务是否存在
const { services } = await this._getServices(deviceId);
const targetService = services.find(s => s.uuid === savedServiceId);
if (!targetService) {
console.warn('保存的服务不存在,尝试自动发现');
await this._disconnectDevice(deviceId);
throw new Error('SAVED_SERVICE_NOT_FOUND');
}
// 验证特征值是否存在且可写
const { characteristics } = await this._getCharacteristics(deviceId, savedServiceId);
const targetChar = characteristics.find(c =>
c.uuid === savedWriteCharId &&
(c.properties.write || c.properties.writeNoResponse)
);
if (!targetChar) {
console.warn('保存的特征值不可用,尝试自动发现');
await this._disconnectDevice(deviceId);
throw new Error('SAVED_CHAR_NOT_FOUND');
}
// 使用保存的配置成功
// ✅ 直接使用已保存的配置,不做任何验证
// 原因:调用 getBLEDeviceCharacteristics 后该特征值会变得不可写(10007)
console.log('使用已保存配置连接成功');
this.deviceId = deviceId;
this.serviceId = savedServiceId;
@@ -375,14 +376,9 @@ class BluetoothPrinterService {
resolve();
return;
} catch (err) {
// 如果不是"保存的配置无效"错误,则直接拒绝
if (err.message !== 'SAVED_SERVICE_NOT_FOUND' && err.message !== 'SAVED_CHAR_NOT_FOUND') {
await this._disconnectDevice(deviceId);
reject(err);
return;
}
// 否则继续自动发现
console.log('回退到自动发现服务...');
// 连接失败,回退到自动发现
console.warn('使用已保存配置连接失败,回退到自动发现:', err);
await this._disconnectDevice(deviceId);
}
}
@@ -516,7 +512,7 @@ class BluetoothPrinterService {
console.log(`[打印] 共 ${lines.length} 行, ${dataSize} bytes`);
// 行数少时一次发送(更快),行数多时按行发送(防止指令被拆包)
const LINE_THRESHOLD = 10;
const LINE_THRESHOLD = 22;
if (lines.length <= LINE_THRESHOLD) {
console.log('[打印] 行数较少,一次发送');
await this._writeData(buffer);
@@ -548,6 +544,12 @@ class BluetoothPrinterService {
await this._delay(80);
}
} catch (err) {
const isLastLine = i === lines.length - 1;
if (isLastLine) {
// 尾行容错:与 _writeData 保持一致,最后一行写入失败不抛异常
console.warn(`[打印] 第 ${i + 1} 行(最后一行)发送失败,视为尾包处理:`, err);
return;
}
console.error(`[打印] 第 ${i + 1} 行发送失败:`, err);
throw err;
}
@@ -659,7 +661,7 @@ class BluetoothPrinterService {
if (data.qrcode) {
const qrc = (layout && layout.qrCode) || { x: 624, y: 584, size: 4 };
commands.push(`B QR ${qrc.x} ${qrc.y} M 2 U ${qrc.size}`);
commands.push(`L0,${data.qrcode}`);
commands.push(`L0,${data.qrcode}\r\n`);
commands.push('ENDQR');
}
@@ -926,43 +928,80 @@ PRINT`;
}
/**
* 写入数据到蓝牙设备(一次发送,适合小数据
* 写入数据到蓝牙设备(自动适配 MTU
* 策略:
* 1. 数据 > MTU 时自动走分片发送(_writeDataInChunks
* 2. 数据 ≤ MTU 时一次发送,writeType 自动重试
* 3. 失败时切换 writeType 重试(null → writeNoResponse → write
* 4. 失败时增加延迟(BLE 协议栈繁忙)后再次重试
*/
async _writeData(buffer) {
async _writeData(buffer, attempt = 0) {
const dataLen = buffer.byteLength;
const mtu = this._mtu || 20;
// 数据超过 MTU?自动走分片
if (dataLen > mtu) {
console.log(`[打印] 数据 ${dataLen}B > MTU ${mtu}B, 自动分片发送`);
return this._writeDataInChunks(buffer, mtu);
}
const writeTypes = [null, 'writeNoResponse', 'write']; // 依次尝试
const maxAttempts = 3; // 总共最多重试 3 次
if (attempt >= maxAttempts) {
// 尾包/单包容错:数据已基本送达(最后一次 writeBLECharacteristicValue 触发 errCode=10007
// 通常是打印机刚处理完上一批数据时协议栈返回 property not support,并非真正写入失败)。
// 此处不再抛异常,静默视为成功以保证整个打印流程不中断。
console.warn(`[打印] 写入重试 ${maxAttempts} 次仍失败,视为尾包/打印机忙导致,数据可能已送达`);
return { success: true, silent: true, reason: 'max attempts reached, treat as success (tail-packet tolerance)' };
}
// 选择本次尝试的 writeType
const writeType = writeTypes[attempt] || null;
const options = {
deviceId: this.deviceId,
serviceId: this.serviceId,
characteristicId: this.writeCharId,
value: buffer
};
// 只有当有指定值时才设置 writeType
if (writeType) {
options.writeType = writeType;
}
console.log(`[打印] 写入尝试 ${attempt + 1}/${maxAttempts}, writeType=${writeType || 'auto'}`);
return new Promise((resolve, reject) => {
uni.writeBLECharacteristicValue({
deviceId: this.deviceId,
serviceId: this.serviceId,
characteristicId: this.writeCharId,
value: buffer,
success: resolve,
...options,
success: (res) => {
// 成功后缓存 writeType
this._writeType = writeType;
console.log(`[打印] 写入成功, writeType=${writeType || 'auto'}`);
resolve(res);
},
fail: (err) => {
// 如果不指定 writeType 自动选择失败,尝试使用 'write'
if (err.errCode === 10007) {
console.log('[打印] writeNoResponse 不支持,尝试使用 write 模式');
uni.writeBLECharacteristicValue({
deviceId: this.deviceId,
serviceId: this.serviceId,
characteristicId: this.writeCharId,
value: buffer,
writeType: 'write',
success: resolve,
fail: reject
});
} else {
reject(err);
}
console.warn(`[打印] 写入失败 attempt=${attempt + 1}, errCode=${err.errCode}, msg=${err.errMsg}`);
// 延迟后重试(递增延迟,给 BLE 协议栈恢复时间)
const delay = 100 + attempt * 150; // 100ms, 250ms, 400ms
setTimeout(() => {
this._writeData(buffer, attempt + 1).then(resolve).catch(reject);
}, delay);
}
});
});
}
/**
* 发送单个数据包(分片用)—— 带 writeType 容错
* 发送单个数据包(分片用)—— 带 writeType 容错 + 多重试
* @param {ArrayBuffer} chunkBuffer - 数据包
* @param {boolean} retryWithWrite - 是否用 'write' 模式重试
* @param {number} attempt - 当前重试次数(内部使用)
*/
_writeChunk(chunkBuffer, retryWithWrite = false) {
_writeChunk(chunkBuffer, retryWithWrite = false, attempt = 0) {
return new Promise((resolve, reject) => {
uni.writeBLECharacteristicValue({
deviceId: this.deviceId,
@@ -971,11 +1010,30 @@ PRINT`;
value: chunkBuffer,
writeType: retryWithWrite ? 'write' : 'writeNoResponse',
success: resolve,
fail: (err) => {
// writeNoResponse 不支持时自动降级为 write 模式
fail: async (err) => {
// writeNoResponse 不支持时降级为 write 模式重试
if (!retryWithWrite && err.errCode === 10007) {
console.warn('[分片] writeNoResponse 不支持,降级为 write 模式');
this._writeChunk(chunkBuffer, true).then(resolve).catch(reject);
try {
await this._delay(50);
await this._writeChunk(chunkBuffer, true, 0);
resolve();
} catch (downgradeErr) {
reject(downgradeErr);
}
return;
}
// 10007 设备忙:等 100ms 重试,最多 3 次
if (err.errCode === 10007 && attempt < 3) {
const wait = 100 * (attempt + 1);
console.warn(`[分片] 设备忙(10007)${wait}ms 后第 ${attempt + 1}/3 次重试...`);
try {
await this._delay(wait);
await this._writeChunk(chunkBuffer, retryWithWrite, attempt + 1);
resolve();
} catch (retryErr) {
reject(retryErr);
}
return;
}
reject(err);
@@ -984,6 +1042,25 @@ PRINT`;
});
}
/**
* 发送单个数据包 —— 强制使用 'write' 模式(带响应)
* 用于尾包等关键数据:write 模式会等待 ACK,确保数据真的到达打印机
* @param {ArrayBuffer} chunkBuffer - 数据包
*/
_writeChunkWithResponse(chunkBuffer) {
return new Promise((resolve, reject) => {
uni.writeBLECharacteristicValue({
deviceId: this.deviceId,
serviceId: this.serviceId,
characteristicId: this.writeCharId,
value: chunkBuffer,
writeType: 'write', // 强制带响应,确保打印机确认收到
success: resolve,
fail: reject
});
});
}
/**
* 获取设备 MTU(最大传输单元)
* @returns {Promise<number>}
@@ -1019,12 +1096,19 @@ PRINT`;
/**
* 分片发送数据(适合大数据)
* @param {ArrayBuffer} buffer - 要发送的数据
* @param {number} mtu - 每包大小(字节),默认 20
* @param {number} delay - 包之间的延迟(毫秒),默认 50
* @param {number} mtu - 每包大小(字节),默认走协商后的 _mtu,兜底 20
* @param {number|null} delay - 包之间的延迟(毫秒),传 null 时按数据量自适应
*/
async _writeDataInChunks(buffer, mtu = 20, delay = 50) {
async _writeDataInChunks(buffer, mtu = null, delay = null) {
const totalLength = buffer.byteLength;
// 参数兜底:传参 > 缓存的 _mtu > 默认 20
mtu = mtu || this._mtu || 20;
// delay 自适应:传了具体值就用,否则按数据量计算
delay = delay ?? this._getChunkDelay(totalLength);
const dataView = new Uint8Array(buffer);
const totalLength = dataView.length;
console.log(`开始分片发送: 总大小=${totalLength}字节, MTU=${mtu}字节/包, 延迟=${delay}ms`);
@@ -1032,6 +1116,7 @@ PRINT`;
const end = Math.min(offset + mtu, totalLength);
const chunk = dataView.slice(offset, end);
const chunkBuffer = chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength);
const isLastChunk = end >= totalLength;
try {
await this._writeChunk(chunkBuffer);
@@ -1044,6 +1129,33 @@ PRINT`;
await this._delay(delay);
}
} catch (err) {
// 尾包容错:最后一包发送失败时,多重试几次(递增延迟 + 强制 write 模式确认)
// 原因:数据已基本送达,打印机在处理打印任务时 BLE 通道会返回 10007 "property not support"(实际为设备忙)
// 这种情况多等一会打印机就能接收,但若直接放弃会导致 QR 码等关键数据被截断
if (isLastChunk) {
const retryDelays = [300, 600, 1000, 1500, 2000]; // 递增延迟
let delivered = false;
for (let attempt = 0; attempt < retryDelays.length; attempt++) {
const wait = retryDelays[attempt];
console.warn(`尾包发送失败(offset=${offset}, size=${end - offset}B),第 ${attempt + 1}/${retryDelays.length} 次重试,等待 ${wait}ms...`, err);
await this._delay(wait);
try {
// 强制使用 write 模式(带响应)发送尾包,确保打印机真的收到了
await this._writeChunkWithResponse(chunkBuffer);
console.log(`尾包重试成功(offset=${offset}, attempt=${attempt + 1})`);
delivered = true;
break;
} catch (retryErr) {
console.warn(`尾包第 ${attempt + 1} 次重试仍失败:`, retryErr);
}
}
if (delivered) {
continue;
}
// 5 次重试全部失败:仍按原策略静默成功(避免上层误报),但通过 console.error 醒目提示数据可能不完整
console.error(`【打印数据可能不完整】尾包 5 次重试均失败(offset=${offset}, size=${end - offset}B),末尾 ${end - offset} 字节可能丢失,请检查打印结果!`);
return;
}
console.error(`分片发送失败 at offset ${offset}:`, err);
throw err;
}
File diff suppressed because it is too large Load Diff