比较提交

...
8 次代码提交
作者 SHA1 备注 提交日期
Guoguo 515153f134 feat: auto-scan 3-phase state machine with proper PD collection timing
- Phase 1 (waiting_fit): after sending control=0x01, wait for fitting=1
- Phase 2 (collecting): once fitting=1 AND run_state=1, start PD accumulation
- Phase 3 (done): run_state 1→0, calculate averages, auto-send treatment
- Check start-collecting in BOTH fitting and run_state handlers
- fitting=0 + run_state 1→0 = fitting failure, prompt retry
- Timeout 3 minutes (was 30s), auto-start treatment on completion
2026-06-29 05:36:12 -07:00
Guoguo b7b76a675d chore: remove dead regions param from wear-check 2026-06-29 05:29:51 -07:00
Guoguo f85925ecd1 fix: correct page flow — wear-check before mode select, smart mode to auto-scan
- wear-check onNext → treatment-setup (was auto-scan)
- treatment-setup smart mode → auto-scan (was wear-check)
Flow: index → wear-check → treatment-setup → normal: treat / smart: auto-scan → treat
2026-06-29 05:28:05 -07:00
Guoguo 74591d4da2 fix: audit fixes — dedupe run_state logs, stop device on scan exit
- Only log run_state when value changes (avoid flooding console)
- Send stopTreatment on unload if scan is in progress (stop device LEDs)
2026-06-29 04:02:16 -07:00
Guoguo f5cd882986 feat: redesign auto-scan as detection page with manual trigger and PD averaging
- Add "start detection" button (not auto-start on page load)
- Send control=0x01 only when user clicks button
- Accumulate PD values across all ADC frames during scan
- Monitor run_state: wait for 0x01→0x00 transition as completion signal
- Calculate PD averages on completion
- Send treatment command (control=0x02) on "next" with calculated params
- TODO: plug in full NR diagnosis algorithm when PD→region mapping confirmed
2026-06-29 03:58:40 -07:00
Guoguo 2995b0ee02 fix: smart mode goes to wear-check first instead of directly to auto-scan 2026-06-29 03:57:31 -07:00
Guoguo f9141b25d0 refactor: wear-check is now pure manual confirm, no BLE commands
Remove all fitting detection logic (setParams, fitting event listener,
timeout). User manually confirms wearing, then proceeds to detection
page. No BLE commands sent on this page.
2026-06-29 03:57:05 -07:00
Guoguo 52f3ef971b fix: auto-scan onNext goes directly to treating instead of looping back to setup 2026-06-24 19:38:16 -07:00
修改 4 个文件,包含 173 行新增212 行删除
+124 -91
查看文件
@@ -1,16 +1,13 @@
var ble = require('../../services/ble') var ble = require('../../services/ble')
var SCAN_TIMEOUT = 15000 var SCAN_TIMEOUT = 180000
var SPECTRUM_NAMES = ['红光', '紫外光', '黄光', '红外光']
var SPECTRUM_COUNT = 4
Page({ Page({
data: { data: {
statusBarHeight: 44, statusBarHeight: 44,
scanning: true,
scanProgress: 0,
regions: 0x1F, regions: 0x1F,
spectrumIndex: 0, phase: 'ready',
spectrumData: [], frameCount: 0,
pdAvg: [],
vbat: 0, vbat: 0,
battery: 0, battery: 0,
error: '' error: ''
@@ -30,83 +27,114 @@ Page({
statusBarHeight: app.globalData.statusBarHeight, statusBarHeight: app.globalData.statusBarHeight,
regions: parseInt(options.regions) || 0x1F regions: parseInt(options.regions) || 0x1F
}) })
this.startScan()
}, },
_cleanup: function () { _cleanup: function () {
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._onFitting) { ble.off('fitting', this._onFitting); this._onFitting = null }
if (this._onRunState) { ble.off('run_state', this._onRunState); this._onRunState = null } if (this._onRunState) { ble.off('run_state', this._onRunState); this._onRunState = null }
}, },
startScan: function () { onStartScan: 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({ error: '设备未连接' })
return return
} }
self.setData({ scanning: true, scanProgress: 0, error: '', spectrumData: [], spectrumIndex: 0 }) self._pdSum = [0, 0, 0, 0, 0, 0, 0]
self._frameCount = 0
self._completed = false
self._collecting = false
self._lastFitting = -1
self._lastRunState = -1
self.setData({ phase: 'waiting_fit', frameCount: 0, pdAvg: [], error: '' })
this._progressTimer = setInterval(function () { self._onFitting = function (data) {
var target = ((self._scanResults.length + 1) / SPECTRUM_COUNT) * 90 if (self._completed) return
var p = self.data.scanProgress if (data.fitting !== self._lastFitting) {
if (p < target) p += 1 console.log('[SCAN] fitting:', data.fitting)
if (p > 90) p = 90 self._lastFitting = data.fitting
self.setData({ scanProgress: p }) }
}, 100) if (data.fitting === 1 && self._lastRunState === 1 && !self._collecting) {
console.log('[SCAN] 贴合确认,开始采集PD数据')
self._collecting = true
self.setData({ phase: 'collecting' })
}
}
ble.on('fitting', self._onFitting)
this._onAdc = function (data) { self._onRunState = function (data) {
if (!self.data.scanning) return if (self._completed) return
var idx = self._scanResults.length if (data.run_state !== self._lastRunState) {
if (idx >= SPECTRUM_COUNT) return console.log('[SCAN] run_state:', data.run_state)
self._lastRunState = data.run_state
}
console.log('[SCAN] 收到第' + (idx + 1) + '组ADC (' + SPECTRUM_NAMES[idx] + '):', data.pd) if (self._lastFitting === 1 && data.run_state === 1 && !self._collecting) {
self._scanResults.push({ console.log('[SCAN] 贴合确认,开始采集PD数据')
spectrum: SPECTRUM_NAMES[idx], self._collecting = true
pd: data.pd.slice(0), self.setData({ phase: 'collecting' })
vbat: data.vbat, }
battery: data.battery
if (self._lastFitting === 0 && data.run_state === 0 && self.data.phase === 'waiting_fit') {
console.log('[SCAN] 未贴合,检测失败')
self._completed = true
self._cleanup()
self.setData({ phase: 'ready', error: '面膜未佩戴好,请重新佩戴后再试' })
return
}
if (data.run_state === 0 && self._collecting) {
console.log('[SCAN] 扫描完成,共', self._frameCount, '帧')
self._scanComplete()
}
}
ble.on('run_state', self._onRunState)
self._onAdc = function (data) {
if (self._completed || !self._collecting) return
if (!data || !data.pd || data.pd.length < 7) return
self._frameCount++
for (var i = 0; i < 7; i++) {
self._pdSum[i] += data.pd[i]
}
self.setData({
frameCount: self._frameCount,
vbat: data.vbat || self.data.vbat,
battery: data.battery || self.data.battery
}) })
self.setData({ spectrumIndex: idx + 1 })
if (self._scanResults.length >= SPECTRUM_COUNT) {
self._scanComplete()
}
} }
ble.on('adc', this._onAdc) ble.on('adc', self._onAdc)
this._onRunState = function (data) { console.log('[SCAN] 发送检测命令 control=0x01')
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: self.data.regions, region_mask: self.data.regions,
wavelength: 2, wavelength: 2,
brightness: 128, brightness: 128,
hold_time: 10, hold_time: 0,
control: 0x01 control: 0x01
}).then(function () { }).then(function () {
console.log('[SCAN] 扫描指令发送成功') console.log('[SCAN] 检测命令发送成功,等待贴合...')
}).catch(function (err) { }).catch(function (err) {
console.error('[SCAN] 扫描指令发送失败:', err) console.error('[SCAN] 检测命令发送失败:', err)
self._cleanup() self._cleanup()
self.setData({ scanning: false, scanProgress: 0, error: '扫描指令发送失败' }) self.setData({ phase: 'ready', error: '检测命令发送失败' })
}) })
this._timeoutTimer = setTimeout(function () { self._timeoutTimer = setTimeout(function () {
if (!self.data.scanning) return if (self._completed) return
console.log('[SCAN] 超时,已收到' + self._scanResults.length + '组数据') console.log('[SCAN] 超时3分钟),已收到', self._frameCount, '帧')
self._scanComplete() if (self._frameCount > 0) {
self._scanComplete()
} else {
self._completed = true
self._cleanup()
self.setData({ phase: 'ready', error: '检测超时,未收到数据,请重试' })
}
}, SCAN_TIMEOUT) }, SCAN_TIMEOUT)
}, },
@@ -114,51 +142,56 @@ Page({
if (this._completed) return if (this._completed) return
this._completed = true this._completed = true
this._cleanup() this._cleanup()
this.setData({ scanning: false, scanProgress: 100 })
this._buildDisplay() var avg = []
var count = this._frameCount || 1
for (var i = 0; i < 7; i++) {
avg.push(Math.round(this._pdSum[i] / count))
}
this.setData({ phase: 'done', pdAvg: avg, frameCount: count })
getApp().globalData.lastScanPdAvg = avg
console.log('[SCAN] PD平均值:', avg, '帧数:', count)
this._startTreatment()
}, },
_buildDisplay: function () { _startTreatment: function () {
var results = this._scanResults || [] var self = this
var spectrumData = [] var mask = self.data.regions || 0x1F
var avg = self.data.pdAvg
var params = self._calculateTreatParams(mask, avg)
for (var s = 0; s < results.length; s++) { console.log('[SCAN] 自动发送治疗命令:', JSON.stringify(params))
var r = results[s] ble.setParams(params).then(function () {
var items = [] return ble.startTreatment(mask)
for (var i = 0; i < 7; i++) { }).then(function () {
var val = r.pd[i] || 0 wx.redirectTo({
items.push({ url: '/pages/treating/treating?regions=' + mask +
label: 'PD' + (i + 1), '&wavelength=2&duration=600000&mode=1'
value: val,
barWidth: Math.min(100, Math.round(val / 40.95))
})
}
spectrumData.push({ name: r.spectrum, items: items })
}
if (results.length > 0) {
var last = results[results.length - 1]
this.setData({
vbat: last.vbat || 0,
battery: last.battery || 0
}) })
} }).catch(function (err) {
self.setData({ error: err.error_msg || '启动治疗失败' })
})
},
getApp().globalData.lastScanData = results _calculateTreatParams: function (mask, pdAvg) {
this.setData({ spectrumData: spectrumData }) // TODO: 接入完整诊断算法(NR 归一化吸收率 → 匹配光谱+强度)
// 需要 PD→区域映射确认后实现
return {
region_mask: mask,
wavelength: 2,
brightness: 200,
duration_ms: 600000,
mode: 1,
control: 0x02
}
}, },
onUnload: function () { onUnload: function () {
if (!this._completed && (this.data.phase === 'waiting_fit' || this.data.phase === 'collecting')) {
ble.stopTreatment().catch(function () {})
}
this._cleanup() this._cleanup()
ble.stopTreatment().catch(function () {})
},
onNext: function () {
var mask = this.data.regions || 0x1F
var scanData = this._scanResults || []
var param = scanData.length > 0 ? '&scan=' + encodeURIComponent(JSON.stringify(scanData)) : ''
wx.redirectTo({
url: '/pages/treatment-setup/treatment-setup?regions=' + mask + param
})
} }
}) })
+26 -28
查看文件
@@ -1,45 +1,43 @@
<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 ? '正在采集数据 (' + spectrumIndex + '/4)...' : '扫描完成'}}</view> <view class="page-header-subtitle">{{phase === 'ready' ? '准备就绪' : phase === 'waiting_fit' ? '等待佩戴贴合...' : phase === 'collecting' ? '采集数据 (' + frameCount + ')...' : '检测完成,正在启动...'}}</view>
</view> </view>
<view class="page-content"> <view class="page-content">
<view class="scan-container" wx:if="{{scanning}}"> <!-- 待开始 -->
<view class="face-outline"> <view wx:if="{{phase === 'ready'}}">
<view class="scan-line"></view> <view class="scan-hint">设备已佩戴,点击下方按钮开始智能检测</view>
</view> <view class="scan-hint sub-hint">检测过程约需 2 分钟,请保持设备贴合</view>
<view class="scan-progress-bar"> <button class="btn-primary mt-30" bindtap="onStartScan">开始检测</button>
<view class="scan-progress-fill" style="width: {{scanProgress}}%"></view>
</view>
<view class="scan-status">固件序贯扫描中:红光 → 紫外光 → 黄光 → 红外光</view>
</view> </view>
<view class="scan-result" wx:if="{{!scanning && spectrumData.length > 0}}"> <!-- 等待贴合 -->
<view class="spectrum-group" wx:for="{{spectrumData}}" wx:key="name"> <view wx:if="{{phase === 'waiting_fit'}}">
<view class="spectrum-title">{{item.name}} (第{{index + 1}}组)</view> <view class="scan-container">
<view class="pd-list"> <view class="face-outline">
<view class="pd-item" wx:for="{{item.items}}" wx:for-item="pd" wx:key="label"> <view class="scan-line"></view>
<view class="pd-region">{{pd.label}}</view>
<view class="pd-value">{{pd.value}}</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 class="vbat-info" wx:if="{{vbat > 0}}"> <view class="scan-status">正在检测面膜贴合状态,请确保设备正确佩戴...</view>
电池电压: {{vbat}}mV ({{battery}}%) </view>
<!-- 采集中 -->
<view wx:if="{{phase === 'collecting'}}">
<view class="scan-container">
<view class="face-outline">
<view class="scan-line"></view>
</view>
</view> </view>
<view class="scan-status">已贴合,正在采集光谱数据... 已收到 {{frameCount}} 帧</view>
</view> </view>
<view class="scan-result" wx:if="{{!scanning && spectrumData.length === 0 && !error}}"> <!-- 完成,自动跳转 -->
<view class="empty-hint">未收到扫描数据</view> <view wx:if="{{phase === 'done'}}">
<view class="scan-status">检测完成(共 {{frameCount}} 帧),正在启动治疗...</view>
</view> </view>
<view class="scan-error" wx:if="{{!scanning && error}}">{{error}}</view> <view class="scan-error" wx:if="{{error}}">{{error}}</view>
<button class="btn-primary mt-30" wx:if="{{!scanning}}" bindtap="onNext">下一步</button>
</view> </view>
</view> </view>
+21 -80
查看文件
@@ -1,12 +1,8 @@
var ble = require('../../services/ble') var ble = require('../../services/ble')
var FITTING_TIMEOUT = 10000
Page({ Page({
data: { data: {
statusBarHeight: 44, statusBarHeight: 44,
checking: false,
result: null,
error: '',
battery: 0, battery: 0,
connected: false connected: false
}, },
@@ -21,109 +17,54 @@ Page({
onLoad: function () { onLoad: function () {
var app = getApp() var app = getApp()
this.setData({ statusBarHeight: app.globalData.statusBarHeight })
var device = app.globalData.currentDevice || {}
this.setData({ this.setData({
battery: device.battery || 0, statusBarHeight: app.globalData.statusBarHeight,
connected: ble.isConnected() connected: ble.isConnected()
}) })
}, var device = app.globalData.currentDevice || {}
this.setData({ battery: device.battery || 0 })
onShow: function () {
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 () {
var self = this
self._cleanup()
self._resolved = false
self.setData({ checking: true, result: null, error: '' })
if (!ble.isConnected()) {
self.setData({ checking: false, result: 'fail', error: '设备未连接,请点击重新连接' })
return
}
self.setData({ connected: true })
self._onBattery = function (info) {
self.setData({ battery: info.battery })
}
ble.on('battery', self._onBattery)
self._onFitting = function (data) {
if (self._resolved) return
console.log('[WEAR] 贴合状态:', data.fitting)
if (data.fitting === 1) {
self._resolved = true
self._cleanup()
self.setData({ checking: false, result: 'ok' })
}
}
ble.on('fitting', self._onFitting)
ble.setParams({
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 () {
this._cleanup()
clearTimeout(this._reconnectTimer)
ble.stopScan()
}, },
onNext: function () { onNext: function () {
wx.navigateTo({ url: '/pages/treatment-setup/treatment-setup' }) if (!ble.isConnected()) {
wx.showToast({ title: '设备未连接', icon: 'none' })
return
}
wx.redirectTo({
url: '/pages/treatment-setup/treatment-setup'
})
}, },
onRetry: function () { onRetry: function () {
var self = this var self = this
if (self.data.checking) return
if (ble.isConnected()) { if (ble.isConnected()) {
self.checkWearing() self.setData({ connected: true })
return return
} }
self.setData({ checking: true, result: null, error: '', connected: false }) self.setData({ connected: false })
clearTimeout(self._reconnectTimer)
ble.disconnect() ble.disconnect()
setTimeout(function () { setTimeout(function () {
ble.startScan({ ble.startScan({
onFound: function () {}, onFound: function () {},
onConnected: function () { onConnected: function () {
clearTimeout(self._reconnectTimer) clearTimeout(self._reconnectTimer)
self.setData({ checking: false, connected: true }) self.setData({ connected: true })
self.checkWearing() wx.showToast({ title: '连接成功', icon: 'success' })
}, },
onError: function (err) { onError: function (err) {
clearTimeout(self._reconnectTimer) clearTimeout(self._reconnectTimer)
self.setData({ checking: false, result: 'fail', error: err.msg || '连接失败' }) wx.showToast({ title: err.msg || '连接失败', icon: 'none' })
} }
}) })
self._reconnectTimer = setTimeout(function () { self._reconnectTimer = setTimeout(function () {
ble.stopScan() ble.stopScan()
self.setData({ checking: false, result: 'fail', error: '搜索超时,请确认设备已开机' }) wx.showToast({ title: '搜索超时', icon: 'none' })
}, 15000) }, 15000)
}, 500) }, 500)
},
onUnload: function () {
clearTimeout(this._reconnectTimer)
ble.stopScan()
} }
}) })
+2 -13
查看文件
@@ -28,18 +28,7 @@
</view> </view>
</view> </view>
<button class="btn-primary" bindtap="checkWearing" disabled="{{checking}}" loading="{{checking}}"> <button class="btn-primary" wx:if="{{connected}}" bindtap="onNext">确认已佩戴,开始检测</button>
确认已佩戴 <button class="btn-secondary" wx:if="{{!connected}}" bindtap="onRetry">重新连接</button>
</button>
<button class="btn-secondary" bindtap="onRetry">重新连接</button>
<view class="check-result" wx:if="{{result === 'ok'}}">
<text class="check-ok">✓ 佩戴确认成功</text>
</view>
<view class="check-result" wx:if="{{result === 'fail'}}">
<text class="check-fail">{{error}}</text>
</view>
<button wx:if="{{result === 'ok'}}" class="btn-primary mt-30" bindtap="onNext">设置参数</button>
</view> </view>
</view> </view>