1.0.6
This commit is contained in:
+2
-2
@@ -2,8 +2,8 @@
|
||||
"name" : "危废数据终端",
|
||||
"appid" : "__UNI__F18DD0A",
|
||||
"description" : "",
|
||||
"versionName" : "1.0.4",
|
||||
"versionCode" : 104,
|
||||
"versionName" : "1.0.6",
|
||||
"versionCode" : 106,
|
||||
"transformPx" : false,
|
||||
/* 5+App特有相关 */
|
||||
"app-plus" : {
|
||||
|
||||
@@ -139,6 +139,7 @@ export default {
|
||||
},
|
||||
onLoad() {
|
||||
this.$store.dispatch('scan/initScanMode')
|
||||
this.$store.dispatch('scale/initScaleConfig') // 初始化称重配置
|
||||
},
|
||||
methods: {
|
||||
handlePrinter() {
|
||||
|
||||
@@ -423,6 +423,7 @@ export default {
|
||||
uni.showToast({ title: '打印成功', icon: 'none' })
|
||||
},
|
||||
fail: (err) => {
|
||||
console.log(err)
|
||||
uni.showToast({ title: '发送失败', icon: 'error' })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -51,6 +51,9 @@
|
||||
|
||||
<!-- 断开连接 -->
|
||||
<view class="section" v-if="connected">
|
||||
<button class="btn btn-primary" :loading="isReading" @click="handleReadWeight">
|
||||
{{ isReading ? '读取中...' : '读取重量(R指令)' }}
|
||||
</button>
|
||||
<button class="btn btn-outline" @click="handleDisconnect">断开连接</button>
|
||||
</view>
|
||||
|
||||
@@ -79,7 +82,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import scaleService from '@/utils/BluetoothScaleService.js';
|
||||
import scaleService from '@/utils/BluetoothScaleService.new.js';
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
|
||||
export default {
|
||||
@@ -87,6 +90,7 @@ export default {
|
||||
return {
|
||||
isConnecting: false,
|
||||
isTaring: false,
|
||||
isReading: false,
|
||||
isCalibrating: false,
|
||||
connected: false,
|
||||
connectingText: '正在检查配置...',
|
||||
@@ -99,7 +103,7 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('scale', ['formattedWeight']),
|
||||
...mapGetters('scale', ['formattedWeight', 'scaleDeviceId', 'scaleServiceId', 'scaleCharacteristicId', 'scaleWriteCharId', 'scaleDeviceName']),
|
||||
displayWeight() {
|
||||
if (!this.currentWeight || this.currentWeight.value === null) {
|
||||
return '0.00';
|
||||
@@ -108,6 +112,7 @@ export default {
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.$store.dispatch('scale/initScaleConfig'); // 先初始化,从 localStorage 恢复
|
||||
this.loadScaleConfig();
|
||||
this.checkAndConnect();
|
||||
},
|
||||
@@ -139,12 +144,11 @@ export default {
|
||||
scaleService.onError(this.handleError);
|
||||
},
|
||||
methods: {
|
||||
// ...mapActions('scale', ['setWeightValue']),
|
||||
...mapActions('scale', ['setWeightValue']),
|
||||
|
||||
handleWeightChange(weight) {
|
||||
this.currentWeight = weight;
|
||||
// 同步到store
|
||||
// this.setWeightValue(weight);
|
||||
this.setWeightValue(weight);
|
||||
},
|
||||
|
||||
handleConnectionChange(connected) {
|
||||
@@ -173,15 +177,12 @@ export default {
|
||||
},
|
||||
|
||||
loadScaleConfig() {
|
||||
const storeState = uni.getStorageSync('scaleConfig') || {};
|
||||
const serviceConfig = scaleService.getConfig();
|
||||
|
||||
this.scaleConfig = {
|
||||
deviceId: serviceConfig.deviceId || storeState.deviceId || '',
|
||||
serviceId: serviceConfig.serviceId || storeState.serviceId || '',
|
||||
notifyCharId: serviceConfig.notifyCharId || storeState.characteristicId || '',
|
||||
writeCharId: serviceConfig.writeCharId || storeState.writeCharacteristicId || '',
|
||||
deviceName: serviceConfig.deviceName || storeState.deviceName || ''
|
||||
deviceId: this.scaleDeviceId || '',
|
||||
serviceId: this.scaleServiceId || '',
|
||||
notifyCharId: this.scaleCharacteristicId || '',
|
||||
writeCharId: this.scaleWriteCharId || '',
|
||||
deviceName: this.scaleDeviceName || ''
|
||||
};
|
||||
},
|
||||
|
||||
@@ -191,9 +192,20 @@ export default {
|
||||
async checkAndConnect() {
|
||||
this.loadScaleConfig();
|
||||
|
||||
// console.log('【调试】Vuex getter 值:', {
|
||||
// scaleDeviceId: this.scaleDeviceId,
|
||||
// scaleServiceId: this.scaleServiceId,
|
||||
// scaleCharacteristicId: this.scaleCharacteristicId,
|
||||
// scaleWriteCharId: this.scaleWriteCharId,
|
||||
// scaleDeviceName: this.scaleDeviceName
|
||||
// });
|
||||
|
||||
// console.log('【调试】loadScaleConfig 后:', this.scaleConfig);
|
||||
|
||||
// 检查是否有保存的配置
|
||||
if (!this.scaleConfig.deviceId) {
|
||||
this.connectingText = '未配置称重设备,请先配置设备';
|
||||
this.addLog('未配置称重设备,请先配置设备', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -243,6 +255,8 @@ export default {
|
||||
|
||||
this.loadScaleConfig();
|
||||
|
||||
// console.log('【调试】handleConnect 配置:', this.scaleConfig);
|
||||
|
||||
if (!this.scaleConfig.deviceId) {
|
||||
uni.showToast({ title: '请先配置称重设备', icon: 'none' });
|
||||
this.connectingText = '未配置称重设备';
|
||||
@@ -320,6 +334,38 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 读取重量(主动发送R指令)
|
||||
*/
|
||||
async handleReadWeight() {
|
||||
if (this.isReading) return;
|
||||
|
||||
this.isReading = true;
|
||||
try {
|
||||
const result = await scaleService.readWeight();
|
||||
|
||||
// 控制台打印完整数据(调试用)
|
||||
// console.log('========== 蓝牙秤原始数据 ==========');
|
||||
// console.log('HEX原始数据:', result.raw);
|
||||
// console.log('ASCII原始数据:', result.rawAscii);
|
||||
// console.log('解析后重量值:', result.value);
|
||||
// console.log('单位:', result.unit);
|
||||
// console.log('是否稳定:', result.stable);
|
||||
// console.log('=====================================');
|
||||
|
||||
// 显示在页面上
|
||||
this.currentWeight = result;
|
||||
this.addLog(`读取成功: ${result.value} ${result.unit}`);
|
||||
uni.showToast({ title: `重量: ${result.value} ${result.unit}`, icon: 'none' });
|
||||
} catch (err) {
|
||||
console.error('【称重】读取失败:', err);
|
||||
this.addLog(`读取失败: ${err.message}`, 'error');
|
||||
uni.showToast({ title: '读取失败', icon: 'none' });
|
||||
} finally {
|
||||
this.isReading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 显示校准弹窗
|
||||
*/
|
||||
|
||||
@@ -171,8 +171,9 @@ export default {
|
||||
content: '确定要退出登录吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.clearStorageSync()
|
||||
uni.reLaunch({ url: '/pages/login/login' })
|
||||
// uni.clearStorageSync()
|
||||
// uni.reLaunch({ url: '/pages/login/login' })
|
||||
this.$store.dispatch('user/logout')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
import { api } from '@/api/index'
|
||||
import scanMixin from '@/utils/barcodeMixin.js'
|
||||
// #ifdef APP-PLUS
|
||||
import printerService from '@/utils/BluetoothPrinterService.js'
|
||||
import printerService from '@/utils/BluetoothPrinterService.new.js'
|
||||
// #endif
|
||||
|
||||
export default {
|
||||
@@ -96,7 +96,7 @@ export default {
|
||||
|
||||
try {
|
||||
const res = await api.labelPrint({ code: this.form.code })
|
||||
|
||||
console.log(res)
|
||||
if (res.data) {
|
||||
this.previewData = res.data
|
||||
uni.showToast({ title: '查询成功', icon: 'none' })
|
||||
@@ -115,6 +115,7 @@ export default {
|
||||
}
|
||||
// #ifdef APP-PLUS
|
||||
const printData = this.printData()
|
||||
console.log(printData)
|
||||
uni.showToast({ title: '打印中...', icon: 'none' })
|
||||
try {
|
||||
await printerService.init()
|
||||
@@ -152,10 +153,10 @@ export default {
|
||||
'zhuyi': this.previewData.zysx || '-', // 注意事项
|
||||
'shibiema': this.previewData.szsbm || '', // 数字识别码
|
||||
'danwei': this.previewData.dwmc || '', // 单位名称
|
||||
'lianxi': this.previewData.lxr || '', // 联系人
|
||||
'lianxi': (this.previewData.lxr || '') + ' ' + (this.previewData.lxrsj || ''), // 联系人
|
||||
'riqi': this.previewData.cssj || '', // 产生时间
|
||||
'zhongliang': (this.previewData.csl || '') + ' ' + (this.previewData.jldw || ''), // 重量
|
||||
'beizhu': '危险特性: ' + (this.previewData.wxtx || '-'), // 备注
|
||||
'beizhu': this.previewData.wxtx || '-', // 备注 危险特征
|
||||
'biaoshi': '', // 标识
|
||||
'qrcode': this.previewData.wxfwbqewm || '', // 二维码
|
||||
'lianxidianhua': this.previewData.lxrsj || '', // 联系电话
|
||||
|
||||
+35
-34
@@ -69,10 +69,10 @@
|
||||
<script>
|
||||
import * as iconv from 'iconv-lite'
|
||||
import { api } from '@/api'
|
||||
import { getDateTime } from '@/utils/common.js'
|
||||
import printerService from '@/utils/BluetoothPrinterService.js';
|
||||
// 【蓝牙秤】引入蓝牙称重服务,用于从电子秤读取重量
|
||||
import scaleService from '@/utils/BluetoothScaleService.js';
|
||||
import { getDateTime, getDate } from '@/utils/common.js'
|
||||
import { mapGetters } from 'vuex';
|
||||
import printerService from '@/utils/BluetoothPrinterService.new.js';
|
||||
import scaleService from '@/utils/BluetoothScaleService.new.js';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
@@ -107,14 +107,20 @@ export default {
|
||||
},
|
||||
currentPrintData: null,
|
||||
// 【蓝牙秤】蓝牙秤连接状态标记,避免重复连接/断开
|
||||
scaleConnected: false
|
||||
scaleConnected: false,
|
||||
// 【打印机】蓝牙打印机连接状态标记(新增)
|
||||
printerConnected: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('scale', ['scaleDeviceId', 'scaleServiceId', 'scaleCharacteristicId'])
|
||||
},
|
||||
/**
|
||||
* 页面加载生命周期
|
||||
* 初始化产生日期为当前时间,并默认选中危废类型、经办人、去向的第一项
|
||||
*/
|
||||
onLoad() {
|
||||
this.$store.dispatch('scale/initScaleConfig'); // 初始化称重配置
|
||||
this.form.productionDate = getDateTime()
|
||||
this.initData()
|
||||
},
|
||||
@@ -191,13 +197,7 @@ export default {
|
||||
* 4. 解析返回数据填入 form.weight
|
||||
*/
|
||||
async getWeight() {
|
||||
// ============================================================
|
||||
// Step 1: 检查是否已配置称重设备
|
||||
// 配置存储在 LocalStorage 的 'scaleConfig' 中
|
||||
// 如果没有配置,提示用户去「设置 > 设备管理 > 称重设备」页面配置
|
||||
// ============================================================
|
||||
const scaleConfig = uni.getStorageSync('scaleConfig');
|
||||
if (!scaleConfig || !scaleConfig.deviceId) {
|
||||
if (!this.scaleDeviceId || !this.scaleServiceId || !this.scaleCharacteristicId) {
|
||||
uni.showModal({
|
||||
title: '未配置称重设备',
|
||||
content: '请先在设置中配置蓝牙称重设备,是否前往设置?',
|
||||
@@ -213,11 +213,6 @@ export default {
|
||||
uni.showToast({ title: '正在获取重量...', icon: 'none' });
|
||||
|
||||
try {
|
||||
// ============================================================
|
||||
// Step 2: 检查连接状态,未连接则自动连接
|
||||
// autoConnect() 会从 LocalStorage 读取 deviceId/serviceId 等配置
|
||||
// 然后调用底层 BluetoothManager 建立 BLE 连接
|
||||
// ============================================================
|
||||
if (!scaleService.isConnected || !this.scaleConnected) {
|
||||
await scaleService.autoConnect();
|
||||
this.scaleConnected = true;
|
||||
@@ -253,17 +248,22 @@ export default {
|
||||
} catch (error) {
|
||||
console.error('【蓝牙秤】获取重量失败:', error);
|
||||
|
||||
// 提取错误信息(兼容 Error 对象和 uni-app 错误对象)
|
||||
const errMsg = error.message || error.errMsg || JSON.stringify(error);
|
||||
|
||||
// 根据错误类型给出不同提示
|
||||
let errMsg = '获取重量失败';
|
||||
if (error.message.includes('未配置')) {
|
||||
errMsg = '请先配置称重设备';
|
||||
} else if (error.message.includes('未连接') || error.message.includes('连接失败')) {
|
||||
errMsg = '蓝牙连接失败,请检查设备是否开机';
|
||||
} else if (error.message.includes('超时')) {
|
||||
errMsg = '读取超时,设备无响应';
|
||||
let toastMsg = '获取重量失败';
|
||||
if (errMsg.includes('未配置')) {
|
||||
toastMsg = '请先配置称重设备';
|
||||
} else if (errMsg.includes('未连接') || errMsg.includes('连接失败') || errMsg.includes('already connect')) {
|
||||
toastMsg = '蓝牙连接失败,请检查设备是否开机';
|
||||
} else if (errMsg.includes('超时') || errMsg.includes('time out')) {
|
||||
toastMsg = '连接超时,请检查蓝牙设备';
|
||||
} else if (errMsg.includes('invalid data')) {
|
||||
toastMsg = '数据格式错误';
|
||||
}
|
||||
|
||||
uni.showToast({ title: errMsg, icon: 'none' });
|
||||
uni.showToast({ title: toastMsg, icon: 'none', duration: 2000 });
|
||||
}
|
||||
},
|
||||
/**
|
||||
@@ -339,17 +339,17 @@ export default {
|
||||
const printData = {
|
||||
'mingcheng': hwInfo.waste_name, // 危废名称
|
||||
'leibie': hwInfo.waste_category, // 危废类别
|
||||
'daima': hwInfo.waste_code, // 危废代码
|
||||
'daima': hwInfo.waste_category_code, // 危废代码
|
||||
'xingtai': hwInfo.waste_form || '-', // 废物形态
|
||||
'zhuyaochengfen': hwInfo.main_component || '-', // 主要成分
|
||||
'zhuyaochengfen': hwInfo.main_components || '-', // 主要成分
|
||||
'youhaichengfen': hwInfo.hazardous_components || '-', // 有害成分
|
||||
'zhuyi': '-', // 注意事项
|
||||
'zhuyi': hwInfo.main_properties || '-', // 注意事项
|
||||
'shibiema': result.digital_id, // 数字识别码
|
||||
'danwei': hwInfo.unit, // 单位
|
||||
'lianxi': '', // 联系人
|
||||
'riqi': getDate(submitData.time), // 日期
|
||||
'riqi': getDate(), // 日期
|
||||
'zhongliang': submitData.weight, // 重量
|
||||
'beizhu': '-', // 备注 台账编号,包装类型
|
||||
'beizhu': result.produce_ledger_no || '-', // 备注 台账编号,包装类型
|
||||
'biaoshi': '', // 标识
|
||||
'qrcode': result.label_qrcode // 二维码
|
||||
}
|
||||
@@ -388,7 +388,8 @@ export default {
|
||||
try {
|
||||
await printerService.init();
|
||||
await printerService.autoConnect();
|
||||
await printerService.printWasteLabel(currentPrintData);
|
||||
this.printerConnected = true
|
||||
await printerService.printWasteLabel(this.currentPrintData);
|
||||
await this.handleDisconnect()
|
||||
uni.showToast({ title: '打印成功', icon: 'success' })
|
||||
this.currentPrintData = null
|
||||
@@ -413,13 +414,13 @@ export default {
|
||||
}
|
||||
|
||||
// 断开蓝牙打印机
|
||||
if (this.connected) {
|
||||
if (this.printerConnected) { // ✅ 修改:this.connected → this.printerConnected
|
||||
try {
|
||||
await printerService.disconnect()
|
||||
this.connected = false
|
||||
this.printerConnected = false // ✅ 修改:this.connected → this.printerConnected
|
||||
} catch (err) {
|
||||
console.warn('【蓝牙打印】断开连接失败:', err)
|
||||
this.connected = false
|
||||
this.printerConnected = false // ✅ 修改:this.connected → this.printerConnected
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,13 +94,11 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
// 断开称重设备
|
||||
// 断开称重设备(仅更新连接状态,保留配置)
|
||||
async disconnectScale({ commit, dispatch }) {
|
||||
try {
|
||||
commit('SET_SCALE_CONFIG', {
|
||||
connected: false,
|
||||
deviceId: '',
|
||||
deviceName: ''
|
||||
connected: false
|
||||
})
|
||||
await dispatch('saveScaleConfig')
|
||||
return true
|
||||
|
||||
@@ -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') {
|
||||
// 连接失败,回退到自动发现
|
||||
console.warn('使用已保存配置连接失败,回退到自动发现:', err);
|
||||
await this._disconnectDevice(deviceId);
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
// 否则继续自动发现
|
||||
console.log('回退到自动发现服务...');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user