比较提交

...
12 次代码提交
作者 SHA1 备注 提交日期
Guoguo bf83adcd28 chore: bump version to v0.1.7 2026-06-24 05:21:19 -07:00
Guoguo ec5b8584ec refactor: remove 17-byte FFE4 and 33-byte FFE1 legacy compat code
- Remove parseVendorStatus 17-byte branch (FFE4 is now 19 bytes)
- Remove parseVendorStatus 33-byte branch (FFE1 is now 34 bytes)
- Remove buildVendorCommand33 deprecated alias
- Remove fitting/run_state !== -1 guards in connection.js
- Update commands.js to use buildVendorCommand directly
2026-06-24 05:13:36 -07:00
Guoguo 29975c6043 fix: restore fitting timeout to 10s for hardware detection delay 2026-06-24 04:12:50 -07:00
Guoguo ca1b9b4377 debug: log FFE1 command hex on every writeRawCommand 2026-06-24 04:10:58 -07:00
Guoguo a5dacef4a7 debug: log stopTreatment success/failure on stop button 2026-06-24 03:53:31 -07:00
Guoguo 229e5f4ed5 fix: default gain to 0 instead of brightness per protocol examples 2026-06-24 03:16:19 -07:00
Guoguo da307e32c8 fix: reduce fitting timeout from 10s to 3s (firmware not yet reporting fitting) 2026-06-24 03:11:32 -07:00
Guoguo c6d958d2ea debug: log FFE4 frame length and type on receive 2026-06-24 02:31:23 -07:00
Guoguo 4c674e5c23 fix: address audit findings — fitting trigger, race guards, state conflicts
wear-check:
- Send weak pulse light (brightness=26) to trigger FFE4 fitting report
- Timeout degrades to pass (backward compat with 17-byte firmware)
- Sync _resolved flag prevents double-resolve

treating:
- Only treat run_state=0x00 as completion after seeing run_state=0x02
  (prevents false completion on startup)
- Sync _finishing flag replaces async data.completed for race prevention
- onStatus preserves paused state during fitting loss (|| fittingLost)
- Remove dead _fittingTimer reference from onUnload

auto-scan:
- Sync _completed flag in _scanComplete prevents double-build
2026-06-22 06:28:25 -07:00
Guoguo c5a9e89bd9 feat: treating page monitors fitting/run_state and handles safety events
- Listen for 'fitting' event: detect skin detachment, pause on loss,
  resume on re-attach
- Listen for 'run_state' event: detect device fault (0xFF) and IDLE
  completion (0x00)
- Low battery warnings: toast at <=10%, force stop at <=5%
- Guard finishAsComplete against double-completion
- Proper cleanup of all new event listeners on unload
2026-06-22 06:21:59 -07:00
Guoguo f4e74e9460 feat: auto-scan receives 4 sequential ADC sets for multi-spectrum scan
Firmware does R→UV→Y→IR sequential scan autonomously. Miniprogram
now listens for 4 consecutive ADC notifications, storing each as a
separate spectrum group. Also listens for run_state=IDLE as scan
completion signal. Displays all 4 spectrum groups with PD1-PD7 bars.
Stores full scan data in globalData.lastScanData for diagnosis phase.
2026-06-22 06:20:53 -07:00
Guoguo b5c6038273 feat: wear-check uses FFE4 fitting status instead of hardcoded ok
Listen for 'fitting' event from FFE4 Byte17 (0=not fitted, 1=fitted).
10s timeout if no fitting signal received. Removes vendor_33 bypass
that always returned ok.
2026-06-22 06:19:23 -07:00
修改 10 个文件,包含 245 行新增134 行删除
+77 -39
查看文件
@@ -1,6 +1,7 @@
var ble = require('../../services/ble') var ble = require('../../services/ble')
var SCAN_BRIGHTNESS = 128 var SCAN_TIMEOUT = 15000
var SCAN_TIMEOUT = 12000 var SPECTRUM_NAMES = ['红光', '紫外光', '黄光', '红外光']
var SPECTRUM_COUNT = 4
Page({ Page({
data: { data: {
@@ -8,8 +9,8 @@ Page({
scanning: true, scanning: true,
scanProgress: 0, scanProgress: 0,
regions: 0x1F, regions: 0x1F,
pdValues: [], spectrumIndex: 0,
pdRaw: [], spectrumData: [],
vbat: 0, vbat: 0,
battery: 0, battery: 0,
error: '' error: ''
@@ -36,41 +37,66 @@ Page({
if (this._progressTimer) { clearInterval(this._progressTimer); this._progressTimer = null } if (this._progressTimer) { clearInterval(this._progressTimer); this._progressTimer = null }
if (this._timeoutTimer) { clearTimeout(this._timeoutTimer); this._timeoutTimer = null } if (this._timeoutTimer) { clearTimeout(this._timeoutTimer); this._timeoutTimer = null }
if (this._onAdc) { ble.off('adc', this._onAdc); this._onAdc = null } if (this._onAdc) { ble.off('adc', this._onAdc); this._onAdc = null }
if (this._onRunState) { ble.off('run_state', this._onRunState); this._onRunState = null }
}, },
startScan: function () { startScan: function () {
var self = this var self = this
self._scanResults = []
self._completed = false
if (!ble.isConnected()) { if (!ble.isConnected()) {
self.setData({ scanning: false, error: '设备未连接,请先连接设备' }) self.setData({ scanning: false, error: '设备未连接,请先连接设备' })
return return
} }
self.setData({ scanning: true, scanProgress: 0, error: '', pdValues: [] }) self.setData({ scanning: true, scanProgress: 0, error: '', spectrumData: [], spectrumIndex: 0 })
this._progressTimer = setInterval(function () { this._progressTimer = setInterval(function () {
var p = self.data.scanProgress + 1 var target = ((self._scanResults.length + 1) / SPECTRUM_COUNT) * 90
var p = self.data.scanProgress
if (p < target) p += 1
if (p > 90) p = 90 if (p > 90) p = 90
self.setData({ scanProgress: p }) self.setData({ scanProgress: p })
}, 80) }, 100)
this._onAdc = function (data) { this._onAdc = function (data) {
console.log('[SCAN] 收到ADC数据:', JSON.stringify(data)) if (!self.data.scanning) return
self._cleanup() var idx = self._scanResults.length
self.setData({ scanning: false, scanProgress: 100 }) if (idx >= SPECTRUM_COUNT) return
self._handleAdcData(data)
console.log('[SCAN] 收到第' + (idx + 1) + '组ADC (' + SPECTRUM_NAMES[idx] + '):', data.pd)
self._scanResults.push({
spectrum: SPECTRUM_NAMES[idx],
pd: data.pd.slice(0),
vbat: data.vbat,
battery: data.battery
})
self.setData({ spectrumIndex: idx + 1 })
if (self._scanResults.length >= SPECTRUM_COUNT) {
self._scanComplete()
}
} }
ble.on('adc', this._onAdc) ble.on('adc', this._onAdc)
console.log('[SCAN] 下发扫描指令: 全域亮度', SCAN_BRIGHTNESS) this._onRunState = function (data) {
if (data.run_state === 0x00 && self._scanResults.length > 0 && self.data.scanning) {
console.log('[SCAN] 设备进入IDLE,扫描结束')
self._scanComplete()
}
}
ble.on('run_state', this._onRunState)
console.log('[SCAN] 下发扫描指令,等待固件序贯扫描 R→UV→Y→IR')
ble.setParams({ ble.setParams({
region_mask: 0x1F, region_mask: self.data.regions,
wavelength: 2, wavelength: 2,
brightness: SCAN_BRIGHTNESS, brightness: 128,
hold_time: 10, hold_time: 10,
control: 0x01 control: 0x01
}).then(function () { }).then(function () {
console.log('[SCAN] 扫描指令发送成功,等待ADC回传...') console.log('[SCAN] 扫描指令发送成功')
}).catch(function (err) { }).catch(function (err) {
console.error('[SCAN] 扫描指令发送失败:', err) console.error('[SCAN] 扫描指令发送失败:', err)
self._cleanup() self._cleanup()
@@ -79,35 +105,47 @@ Page({
this._timeoutTimer = setTimeout(function () { this._timeoutTimer = setTimeout(function () {
if (!self.data.scanning) return if (!self.data.scanning) return
console.log('[SCAN] 等待ADC超时,使用默认值') console.log('[SCAN] 超时,已收到' + self._scanResults.length + '组数据')
self._cleanup() self._scanComplete()
self.setData({ scanning: false, scanProgress: 100 })
self._handleAdcData(null)
}, SCAN_TIMEOUT) }, SCAN_TIMEOUT)
}, },
_handleAdcData: function (data) { _scanComplete: function () {
var regionNames = ['右区(IO1)', '左区(IO2)', '上区(IO3)', '中区(IO4)', '下区(IO5)'] if (this._completed) return
var pdValues = [] this._completed = true
this._cleanup()
this.setData({ scanning: false, scanProgress: 100 })
this._buildDisplay()
},
if (data && data.pd && data.pd.length >= 7) { _buildDisplay: function () {
this.setData({ pdRaw: data.pd, vbat: data.vbat || 0, battery: data.battery || 0 }) var results = this._scanResults || []
for (var i = 0; i < 5; i++) { var spectrumData = []
var barW = Math.min(100, Math.round(data.pd[i] / 40.95))
pdValues.push({ region: regionNames[i], pd: data.pd[i], pdText: String(data.pd[i]), barWidth: barW })
}
pdValues.push({ region: 'PD6', pd: data.pd[5], pdText: String(data.pd[5]), barWidth: Math.min(100, Math.round(data.pd[5] / 40.95)) })
pdValues.push({ region: 'PD7', pd: data.pd[6], pdText: String(data.pd[6]), barWidth: Math.min(100, Math.round(data.pd[6] / 40.95)) })
getApp().globalData.lastScanPd = data.pd for (var s = 0; s < results.length; s++) {
getApp().globalData.lastScanVbat = data.vbat var r = results[s]
} else { var items = []
for (var j = 0; j < 5; j++) { for (var i = 0; i < 7; i++) {
pdValues.push({ region: regionNames[j], pd: 0, pdText: '未采集', barWidth: 0 }) var val = r.pd[i] || 0
items.push({
label: 'PD' + (i + 1),
value: val,
barWidth: Math.min(100, Math.round(val / 40.95))
})
} }
spectrumData.push({ name: r.spectrum, items: items })
} }
this.setData({ pdValues: pdValues }) if (results.length > 0) {
var last = results[results.length - 1]
this.setData({
vbat: last.vbat || 0,
battery: last.battery || 0
})
}
getApp().globalData.lastScanData = results
this.setData({ spectrumData: spectrumData })
}, },
onUnload: function () { onUnload: function () {
@@ -117,10 +155,10 @@ Page({
onNext: function () { onNext: function () {
var mask = this.data.regions || 0x1F var mask = this.data.regions || 0x1F
var pdRaw = this.data.pdRaw var scanData = this._scanResults || []
var pdParam = pdRaw.length > 0 ? '&pd=' + encodeURIComponent(JSON.stringify(pdRaw)) : '' var param = scanData.length > 0 ? '&scan=' + encodeURIComponent(JSON.stringify(scanData)) : ''
wx.redirectTo({ wx.redirectTo({
url: '/pages/treatment-setup/treatment-setup?regions=' + mask + pdParam url: '/pages/treatment-setup/treatment-setup?regions=' + mask + param
}) })
} }
}) })
+17 -11
查看文件
@@ -1,8 +1,8 @@
<view class="page"> <view class="page">
<view class="page-header page-header-pink" style="padding-top: {{statusBarHeight + 24}}px;"> <view class="page-header page-header-pink" style="padding-top: {{statusBarHeight + 24}}px;">
<view class="nav-back" bindtap="onBack"> 返回</view> <view class="nav-back" bindtap="onBack"> 返回</view>
<view class="page-header-title">区域扫描</view> <view class="page-header-title">多光谱扫描</view>
<view class="page-header-subtitle">{{scanning ? '正在采集数据...' : '扫描完成'}}</view> <view class="page-header-subtitle">{{scanning ? '正在采集数据 (' + spectrumIndex + '/4)...' : '扫描完成'}}</view>
</view> </view>
<view class="page-content"> <view class="page-content">
@@ -13,17 +13,19 @@
<view class="scan-progress-bar"> <view class="scan-progress-bar">
<view class="scan-progress-fill" style="width: {{scanProgress}}%"></view> <view class="scan-progress-fill" style="width: {{scanProgress}}%"></view>
</view> </view>
<view class="scan-status">全域LED已开启,亮度 128,等待PD数据回传...</view> <view class="scan-status">固件序贯扫描中:红光 → 紫外光 → 黄光 → 红外光</view>
</view> </view>
<view class="scan-result" wx:if="{{!scanning && pdValues.length > 0}}"> <view class="scan-result" wx:if="{{!scanning && spectrumData.length > 0}}">
<view class="result-title">PD 采集结果</view> <view class="spectrum-group" wx:for="{{spectrumData}}" wx:key="name">
<view class="pd-list"> <view class="spectrum-title">{{item.name}} (第{{index + 1}}组)</view>
<view class="pd-item" wx:for="{{pdValues}}" wx:key="region"> <view class="pd-list">
<view class="pd-region">{{item.region}}</view> <view class="pd-item" wx:for="{{item.items}}" wx:for-item="pd" wx:key="label">
<view class="pd-value {{item.pd > 0 ? '' : 'pd-empty'}}">{{item.pdText}}</view> <view class="pd-region">{{pd.label}}</view>
<view class="pd-bar-bg" wx:if="{{item.pd > 0}}"> <view class="pd-value">{{pd.value}}</view>
<view class="pd-bar-fill" style="width: {{item.barWidth}}%"></view> <view class="pd-bar-bg" wx:if="{{pd.value > 0}}">
<view class="pd-bar-fill" style="width: {{pd.barWidth}}%"></view>
</view>
</view> </view>
</view> </view>
</view> </view>
@@ -32,6 +34,10 @@
</view> </view>
</view> </view>
<view class="scan-result" wx:if="{{!scanning && spectrumData.length === 0 && !error}}">
<view class="empty-hint">未收到扫描数据</view>
</view>
<view class="scan-error" wx:if="{{!scanning && error}}">{{error}}</view> <view class="scan-error" wx:if="{{!scanning && error}}">{{error}}</view>
<button class="btn-primary mt-30" wx:if="{{!scanning}}" bindtap="onNext">下一步</button> <button class="btn-primary mt-30" wx:if="{{!scanning}}" bindtap="onNext">下一步</button>
@@ -121,6 +121,30 @@
text-align: center; text-align: center;
} }
.spectrum-group {
margin-bottom: 24rpx;
}
.spectrum-group:last-child {
margin-bottom: 0;
}
.spectrum-title {
font-size: 26rpx;
font-weight: 600;
color: #E6508C;
margin-bottom: 12rpx;
padding-bottom: 8rpx;
border-bottom: 2rpx solid #fdf2f8;
}
.empty-hint {
text-align: center;
color: #999;
font-size: 26rpx;
padding: 40rpx 0;
}
.scan-error { .scan-error {
text-align: center; text-align: center;
color: #ff4d4f; color: #ff4d4f;
+1 -1
查看文件
@@ -62,7 +62,7 @@
<view class="logout-btn" bindtap="onLogout">退出登录</view> <view class="logout-btn" bindtap="onLogout">退出登录</view>
<view class="version-text">v0.1.6</view> <view class="version-text">v0.1.7</view>
</view> </view>
<!-- Edit profile modal --> <!-- Edit profile modal -->
+75 -6
查看文件
@@ -17,6 +17,8 @@ Page({
completed: false, completed: false,
startedAt: 0, startedAt: 0,
regionText: '全区域', regionText: '全区域',
fittingLost: false,
deviceFault: false
}, },
_getRegionText: function (regionMask) { _getRegionText: function (regionMask) {
@@ -44,15 +46,18 @@ Page({
regionText: this._getRegionText(regions) regionText: this._getRegionText(regions)
}) })
this._sessionId = 'SESS_' + Date.now()
this._onStatus = this.onStatus.bind(this) this._onStatus = this.onStatus.bind(this)
this._onBattery = this.onBattery.bind(this)
this._onComplete = this.onComplete.bind(this) this._onComplete = this.onComplete.bind(this)
this._onException = this.onException.bind(this) this._onException = this.onException.bind(this)
this._onDisconnect = this.onDisconnect.bind(this) this._onDisconnect = this.onDisconnect.bind(this)
this._onReconnect = this.onReconnect.bind(this) this._onReconnect = this.onReconnect.bind(this)
this._onReconnectFailed = this.onReconnectFailed.bind(this) this._onReconnectFailed = this.onReconnectFailed.bind(this)
this._sessionId = 'SESS_' + Date.now() this._onFitting = this.onFitting.bind(this)
this._onRunState = this.onRunState.bind(this)
this._onBattery = this.onBattery.bind(this)
ble.on('status', this._onStatus) ble.on('status', this._onStatus)
ble.on('battery', this._onBattery) ble.on('battery', this._onBattery)
ble.on('treatment_complete', this._onComplete) ble.on('treatment_complete', this._onComplete)
@@ -60,6 +65,9 @@ Page({
ble.on('disconnected', this._onDisconnect) ble.on('disconnected', this._onDisconnect)
ble.on('reconnected', this._onReconnect) ble.on('reconnected', this._onReconnect)
ble.on('reconnect_failed', this._onReconnectFailed) ble.on('reconnect_failed', this._onReconnectFailed)
ble.on('fitting', this._onFitting)
ble.on('run_state', this._onRunState)
this.startLocalTimer() this.startLocalTimer()
this.syncCommands() this.syncCommands()
this.syncTreatmentStart() this.syncTreatmentStart()
@@ -73,9 +81,48 @@ Page({
if (this._onDisconnect) ble.off('disconnected', this._onDisconnect) if (this._onDisconnect) ble.off('disconnected', this._onDisconnect)
if (this._onReconnect) ble.off('reconnected', this._onReconnect) if (this._onReconnect) ble.off('reconnected', this._onReconnect)
if (this._onReconnectFailed) ble.off('reconnect_failed', this._onReconnectFailed) if (this._onReconnectFailed) ble.off('reconnect_failed', this._onReconnectFailed)
if (this._onFitting) ble.off('fitting', this._onFitting)
if (this._onRunState) ble.off('run_state', this._onRunState)
if (this._localTimer) clearInterval(this._localTimer) if (this._localTimer) clearInterval(this._localTimer)
}, },
onFitting: function (data) {
if (this.data.completed) return
if (data.fitting === 0 && !this.data.fittingLost) {
this.setData({ fittingLost: true, paused: true })
wx.showToast({ title: '检测到离肤,请重新佩戴设备', icon: 'none', duration: 3000 })
} else if (data.fitting === 1 && this.data.fittingLost) {
this.setData({ fittingLost: false, paused: false })
wx.showToast({ title: '已重新贴合,继续使用', icon: 'success', duration: 2000 })
}
},
onRunState: function (data) {
if (this._finishing) return
console.log('[TREAT] run_state:', data.run_state)
if (data.run_state === 0x02) {
this._seenTreating = true
}
if (data.run_state === 0xFF && !this._finishing) {
this._finishing = true
this.setData({ deviceFault: true })
var self = this
wx.showModal({
title: '设备故障',
content: '设备上报故障,已自动停止。',
showCancel: false,
complete: function () { self.finishAsComplete() }
})
return
}
if (data.run_state === 0x00 && this._seenTreating && !this.data.fittingLost) {
this.finishAsComplete()
}
},
startLocalTimer: function () { startLocalTimer: function () {
var self = this var self = this
var timer = setInterval(function () { var timer = setInterval(function () {
@@ -83,6 +130,23 @@ Page({
var elapsed = Date.now() - self.data.startedAt var elapsed = Date.now() - self.data.startedAt
var remaining = Math.max(0, self.data.duration - elapsed) var remaining = Math.max(0, self.data.duration - elapsed)
self.updateProgress(remaining) self.updateProgress(remaining)
if (self.data.battery > 0 && self.data.battery <= 5) {
clearInterval(timer)
wx.showModal({
title: '电量过低',
content: '设备电量不足5%,已自动停止。请及时充电。',
showCancel: false,
complete: function () { self.finishAsComplete() }
})
return
}
if (self.data.battery > 0 && self.data.battery <= 10 && !self._lowBatteryWarned) {
self._lowBatteryWarned = true
wx.showToast({ title: '电量低,请及时充电', icon: 'none', duration: 3000 })
}
if (remaining <= 0) { if (remaining <= 0) {
clearInterval(timer) clearInterval(timer)
self.finishAsComplete() self.finishAsComplete()
@@ -110,7 +174,7 @@ Page({
this.setData({ this.setData({
battery: status.battery || this.data.battery, battery: status.battery || this.data.battery,
temperature: status.temperature || this.data.temperature, temperature: status.temperature || this.data.temperature,
paused: status.mode_state === 0x03 paused: status.mode_state === 0x03 || this.data.fittingLost
}) })
this.syncCommands() this.syncCommands()
}, },
@@ -179,6 +243,8 @@ Page({
}, },
finishAsComplete: function () { finishAsComplete: function () {
if (this._finishing) return
this._finishing = true
var elapsed = Math.min(this.data.duration, Date.now() - this.data.startedAt) var elapsed = Math.min(this.data.duration, Date.now() - this.data.startedAt)
this.syncTreatmentEnd(elapsed) this.syncTreatmentEnd(elapsed)
this.onComplete({ this.onComplete({
@@ -248,12 +314,15 @@ Page({
content: '确定要提前结束本次使用吗?', content: '确定要提前结束本次使用吗?',
success: function (res) { success: function (res) {
if (res.confirm) { if (res.confirm) {
ble.stopTreatment().catch(function () {}).then(function () { ble.stopTreatment().then(function (res) {
console.log('[TREAT] 停止命令发送成功', res)
}).catch(function (err) {
console.error('[TREAT] 停止命令发送失败', err)
}).then(function () {
self.finishAsComplete() self.finishAsComplete()
}) })
} }
} }
}) })
}, }
}) })
+43 -22
查看文件
@@ -1,4 +1,6 @@
var ble = require('../../services/ble') var ble = require('../../services/ble')
var FITTING_TIMEOUT = 10000
Page({ Page({
data: { data: {
statusBarHeight: 44, statusBarHeight: 44,
@@ -31,8 +33,16 @@ Page({
this.checkWearing() this.checkWearing()
}, },
_cleanup: function () {
if (this._onFitting) { ble.off('fitting', this._onFitting); this._onFitting = null }
if (this._onBattery) { ble.off('battery', this._onBattery); this._onBattery = null }
if (this._fittingTimer) { clearTimeout(this._fittingTimer); this._fittingTimer = null }
},
checkWearing: function () { checkWearing: function () {
var self = this var self = this
self._cleanup()
self._resolved = false
self.setData({ checking: true, result: null, error: '' }) self.setData({ checking: true, result: null, error: '' })
if (!ble.isConnected()) { if (!ble.isConnected()) {
@@ -40,33 +50,45 @@ Page({
return return
} }
var protocol = require('../../services/ble/protocol') self.setData({ connected: true })
if (protocol.PROTOCOL_MODE === 'vendor_33') {
self.setData({ checking: false, result: 'ok', connected: true })
return
}
if (this._onStatus) ble.off('status', this._onStatus) self._onBattery = function (info) {
this._onStatus = function (status) { self.setData({ battery: info.battery })
self.setData({ checking: false }) }
if (status.battery !== undefined) { ble.on('battery', self._onBattery)
self.setData({ battery: status.battery, connected: true })
} self._onFitting = function (data) {
if (status.bind_status === 1) { if (self._resolved) return
self.setData({ result: 'ok' }) console.log('[WEAR] 贴合状态:', data.fitting)
} else { if (data.fitting === 1) {
self.setData({ result: 'fail', error: '请确认设备已正确佩戴' }) self._resolved = true
self._cleanup()
self.setData({ checking: false, result: 'ok' })
} }
} }
ble.on('status', this._onStatus) ble.on('fitting', self._onFitting)
ble.queryStatus().catch(function (err) { ble.setParams({
self.setData({ checking: false, result: 'fail', error: err.error_msg || '查询失败' }) region_mask: 0x1F,
}) wavelength: 2,
brightness: 26,
hold_time: 0,
control: 0x01
}).then(function () {
console.log('[WEAR] 微弱光贴合检测指令已发送')
}).catch(function () {})
self._fittingTimer = setTimeout(function () {
if (self._resolved) return
self._resolved = true
self._cleanup()
console.log('[WEAR] 超时未收到 fitting 事件,降级为通过(兼容旧固件)')
self.setData({ checking: false, result: 'ok' })
}, FITTING_TIMEOUT)
}, },
onUnload: function () { onUnload: function () {
if (this._onStatus) ble.off('status', this._onStatus) this._cleanup()
clearTimeout(this._reconnectTimer) clearTimeout(this._reconnectTimer)
ble.stopScan() ble.stopScan()
}, },
@@ -103,6 +125,5 @@ Page({
self.setData({ checking: false, result: 'fail', error: '搜索超时,请确认设备已开机' }) self.setData({ checking: false, result: 'fail', error: '搜索超时,请确认设备已开机' })
}, 15000) }, 15000)
}, 500) }, 500)
}, }
}) })
+4 -2
查看文件
@@ -72,6 +72,7 @@ function writeRawCommand(bytes) {
reject({ error_code: 0xFF, error_msg: 'command characteristic not found' }) reject({ error_code: 0xFF, error_msg: 'command characteristic not found' })
return return
} }
console.log('[BLE] FFE1 发送', bytes.length + '字节', '| hex:', protocol.bytesToHex(bytes))
wx.writeBLECharacteristicValue({ wx.writeBLECharacteristicValue({
deviceId: connection.getDeviceId(), deviceId: connection.getDeviceId(),
serviceId: chars.command.serviceId, serviceId: chars.command.serviceId,
@@ -80,7 +81,8 @@ function writeRawCommand(bytes) {
success: function () { success: function () {
resolve({ message: 'success', bytes: bytes }) resolve({ message: 'success', bytes: bytes })
}, },
fail: function () { fail: function (err) {
console.error('[BLE] FFE1 发送失败', err)
reject({ error_code: 0x0C, error_msg: 'ERR_BLE_DISCONNECTED' }) reject({ error_code: 0x0C, error_msg: 'ERR_BLE_DISCONNECTED' })
} }
}) })
@@ -149,7 +151,7 @@ function readDeviceInfo() {
function setParams(options) { function setParams(options) {
if (protocol.PROTOCOL_MODE === 'vendor_33') { if (protocol.PROTOCOL_MODE === 'vendor_33') {
return writeRawCommand(protocol.buildVendorCommand33(options || {})) return writeRawCommand(protocol.buildVendorCommand(options || {}))
} }
var regionMask = options.region_mask || protocol.REGION.FULL_FACE var regionMask = options.region_mask || protocol.REGION.FULL_FACE
+3 -6
查看文件
@@ -84,6 +84,7 @@ function handleNotification(frame) {
function handleValueChange(res) { function handleValueChange(res) {
if (protocol.PROTOCOL_MODE === 'vendor_33') { if (protocol.PROTOCOL_MODE === 'vendor_33') {
var parsed = protocol.parseVendorStatus(res.value) var parsed = protocol.parseVendorStatus(res.value)
console.log('[BLE] FFE4 收到', protocol.bufferToBytes(res.value).length + '字节', parsed.type, '| raw:', parsed.raw_hex)
emit('vendor_raw', parsed) emit('vendor_raw', parsed)
if (parsed.type === 'heartbeat') { if (parsed.type === 'heartbeat') {
@@ -100,12 +101,8 @@ function handleValueChange(res) {
} }
emit('adc', { pd: parsed.pd, vbat: vbatMv, battery: batteryPct, fitting: parsed.fitting, run_state: parsed.run_state }) emit('adc', { pd: parsed.pd, vbat: vbatMv, battery: batteryPct, fitting: parsed.fitting, run_state: parsed.run_state })
emit('battery', { battery: batteryPct, vbat: vbatMv }) emit('battery', { battery: batteryPct, vbat: vbatMv })
if (parsed.fitting !== -1) { emit('fitting', { fitting: parsed.fitting })
emit('fitting', { fitting: parsed.fitting }) emit('run_state', { run_state: parsed.run_state })
}
if (parsed.run_state !== -1) {
emit('run_state', { run_state: parsed.run_state })
}
return return
} }
-1
查看文件
@@ -43,7 +43,6 @@ module.exports = {
buildFrame: protocol.buildFrame, buildFrame: protocol.buildFrame,
parseFrame: protocol.parseFrame, parseFrame: protocol.parseFrame,
buildVendorCommand: protocol.buildVendorCommand, buildVendorCommand: protocol.buildVendorCommand,
buildVendorCommand33: protocol.buildVendorCommand33,
parseVendorStatus: protocol.parseVendorStatus, parseVendorStatus: protocol.parseVendorStatus,
parseStatusReport: protocol.parseStatusReport, parseStatusReport: protocol.parseStatusReport,
parseAck: protocol.parseAck, parseAck: protocol.parseAck,
+1 -46
查看文件
@@ -172,7 +172,7 @@ function buildVendorCommand(options) {
var regionMask = options.region_mask || REGION.FULL_FACE var regionMask = options.region_mask || REGION.FULL_FACE
var wavelength = options.wavelength || WAVELENGTH.R var wavelength = options.wavelength || WAVELENGTH.R
var brightness = clampByte(options.brightness === undefined ? 200 : options.brightness) var brightness = clampByte(options.brightness === undefined ? 200 : options.brightness)
var gain = clampUInt16(options.current_gain === undefined ? brightness : options.current_gain) var gain = clampUInt16(options.current_gain === undefined ? 0 : options.current_gain)
var durationMs = options.duration_ms || 600000 var durationMs = options.duration_ms || 600000
var holdSeconds = clampUInt16(options.hold_time === undefined ? Math.round(durationMs / 1000) : options.hold_time) var holdSeconds = clampUInt16(options.hold_time === undefined ? Math.round(durationMs / 1000) : options.hold_time)
var control = options.control !== undefined ? options.control : CONTROL.TREAT var control = options.control !== undefined ? options.control : CONTROL.TREAT
@@ -206,11 +206,6 @@ function buildVendorCommand(options) {
return bytes return bytes
} }
// deprecated: alias kept for backward compat, actually outputs 34 bytes
function buildVendorCommand33(options) {
return buildVendorCommand(options)
}
function parseVendorStatus(buffer) { function parseVendorStatus(buffer) {
var bytes = bufferToBytes(buffer) var bytes = bufferToBytes(buffer)
if (bytes.length === 1) { if (bytes.length === 1) {
@@ -233,23 +228,6 @@ function parseVendorStatus(buffer) {
raw_hex: bytesToHex(bytes) raw_hex: bytesToHex(bytes)
} }
} }
if (bytes.length === 17) {
var checksum = xorChecksum(bytes.slice(0, 16))
var pds = []
for (var p = 0; p < 7; p++) {
pds.push(bytes[p * 2] | (bytes[p * 2 + 1] << 8))
}
return {
type: 'adc',
is_heartbeat: false,
pd: pds,
vbat: bytes[14] | (bytes[15] << 8),
fitting: -1,
run_state: -1,
checksum_ok: checksum === bytes[16],
raw_hex: bytesToHex(bytes)
}
}
if (bytes.length === 34) { if (bytes.length === 34) {
var ios34 = [] var ios34 = []
for (var j = 0; j < 5; j++) { for (var j = 0; j < 5; j++) {
@@ -272,28 +250,6 @@ function parseVendorStatus(buffer) {
raw_hex: bytesToHex(bytes) raw_hex: bytesToHex(bytes)
} }
} }
if (bytes.length === 33) {
var ios = []
for (var i = 0; i < 5; i++) {
var off = i * 6
ios.push({
red: bytes[off], infrared: bytes[off + 1],
uv: bytes[off + 2], warm_yellow: bytes[off + 3],
gain: (bytes[off + 4] << 8) | bytes[off + 5]
})
}
var expected33 = xorChecksum(bytes.slice(0, 32))
return {
type: 'params',
is_heartbeat: false,
ios: ios,
hold_time: (bytes[30] << 8) | bytes[31],
control: -1,
checksum: bytes[32],
checksum_ok: expected33 === bytes[32],
raw_hex: bytesToHex(bytes)
}
}
return { type: 'unknown', is_heartbeat: false, raw_bytes: bytes, raw_hex: bytesToHex(bytes) } return { type: 'unknown', is_heartbeat: false, raw_bytes: bytes, raw_hex: bytesToHex(bytes) }
} }
@@ -417,7 +373,6 @@ module.exports = {
hexToBytes: hexToBytes, hexToBytes: hexToBytes,
bytesToHex: bytesToHex, bytesToHex: bytesToHex,
buildVendorCommand: buildVendorCommand, buildVendorCommand: buildVendorCommand,
buildVendorCommand33: buildVendorCommand33,
buildFrame: buildFrame, buildFrame: buildFrame,
parseFrame: parseFrame, parseFrame: parseFrame,