比较提交

...
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 SCAN_TIMEOUT = 15000
var SPECTRUM_NAMES = ['红光', '紫外光', '黄光', '红外光']
var SPECTRUM_COUNT = 4
var SCAN_TIMEOUT = 180000
Page({
data: {
statusBarHeight: 44,
scanning: true,
scanProgress: 0,
regions: 0x1F,
spectrumIndex: 0,
spectrumData: [],
phase: 'ready',
frameCount: 0,
pdAvg: [],
vbat: 0,
battery: 0,
error: ''
@@ -30,83 +27,114 @@ Page({
statusBarHeight: app.globalData.statusBarHeight,
regions: parseInt(options.regions) || 0x1F
})
this.startScan()
},
_cleanup: function () {
if (this._progressTimer) { clearInterval(this._progressTimer); this._progressTimer = null }
if (this._timeoutTimer) { clearTimeout(this._timeoutTimer); this._timeoutTimer = 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 }
},
startScan: function () {
onStartScan: function () {
var self = this
self._scanResults = []
self._completed = false
if (!ble.isConnected()) {
self.setData({ scanning: false, error: '设备未连接,请先连接设备' })
self.setData({ error: '设备未连接' })
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 () {
var target = ((self._scanResults.length + 1) / SPECTRUM_COUNT) * 90
var p = self.data.scanProgress
if (p < target) p += 1
if (p > 90) p = 90
self.setData({ scanProgress: p })
}, 100)
self._onFitting = function (data) {
if (self._completed) return
if (data.fitting !== self._lastFitting) {
console.log('[SCAN] fitting:', data.fitting)
self._lastFitting = data.fitting
}
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) {
if (!self.data.scanning) return
var idx = self._scanResults.length
if (idx >= SPECTRUM_COUNT) return
self._onRunState = function (data) {
if (self._completed) return
if (data.run_state !== self._lastRunState) {
console.log('[SCAN] run_state:', data.run_state)
self._lastRunState = data.run_state
}
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
if (self._lastFitting === 1 && data.run_state === 1 && !self._collecting) {
console.log('[SCAN] 贴合确认,开始采集PD数据')
self._collecting = true
self.setData({ phase: 'collecting' })
}
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) {
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')
console.log('[SCAN] 发送检测命令 control=0x01')
ble.setParams({
region_mask: self.data.regions,
wavelength: 2,
brightness: 128,
hold_time: 10,
hold_time: 0,
control: 0x01
}).then(function () {
console.log('[SCAN] 扫描指令发送成功')
console.log('[SCAN] 检测命令发送成功,等待贴合...')
}).catch(function (err) {
console.error('[SCAN] 扫描指令发送失败:', err)
console.error('[SCAN] 检测命令发送失败:', err)
self._cleanup()
self.setData({ scanning: false, scanProgress: 0, error: '扫描指令发送失败' })
self.setData({ phase: 'ready', error: '检测命令发送失败' })
})
this._timeoutTimer = setTimeout(function () {
if (!self.data.scanning) return
console.log('[SCAN] 超时,已收到' + self._scanResults.length + '组数据')
self._scanComplete()
self._timeoutTimer = setTimeout(function () {
if (self._completed) return
console.log('[SCAN] 超时3分钟),已收到', self._frameCount, '帧')
if (self._frameCount > 0) {
self._scanComplete()
} else {
self._completed = true
self._cleanup()
self.setData({ phase: 'ready', error: '检测超时,未收到数据,请重试' })
}
}, SCAN_TIMEOUT)
},
@@ -114,51 +142,56 @@ Page({
if (this._completed) return
this._completed = true
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 () {
var results = this._scanResults || []
var spectrumData = []
_startTreatment: function () {
var self = this
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++) {
var r = results[s]
var items = []
for (var i = 0; i < 7; i++) {
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 })
}
if (results.length > 0) {
var last = results[results.length - 1]
this.setData({
vbat: last.vbat || 0,
battery: last.battery || 0
console.log('[SCAN] 自动发送治疗命令:', JSON.stringify(params))
ble.setParams(params).then(function () {
return ble.startTreatment(mask)
}).then(function () {
wx.redirectTo({
url: '/pages/treating/treating?regions=' + mask +
'&wavelength=2&duration=600000&mode=1'
})
}
}).catch(function (err) {
self.setData({ error: err.error_msg || '启动治疗失败' })
})
},
getApp().globalData.lastScanData = results
this.setData({ spectrumData: spectrumData })
_calculateTreatParams: function (mask, pdAvg) {
// TODO: 接入完整诊断算法(NR 归一化吸收率 → 匹配光谱+强度)
// 需要 PD→区域映射确认后实现
return {
region_mask: mask,
wavelength: 2,
brightness: 200,
duration_ms: 600000,
mode: 1,
control: 0x02
}
},
onUnload: function () {
if (!this._completed && (this.data.phase === 'waiting_fit' || this.data.phase === 'collecting')) {
ble.stopTreatment().catch(function () {})
}
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-header page-header-pink" style="padding-top: {{statusBarHeight + 24}}px;">
<view class="nav-back" bindtap="onBack"> 返回</view>
<view class="page-header-title">多光谱扫描</view>
<view class="page-header-subtitle">{{scanning ? '正在采集数据 (' + spectrumIndex + '/4)...' : '扫描完成'}}</view>
<view class="page-header-title">智能检测</view>
<view class="page-header-subtitle">{{phase === 'ready' ? '准备就绪' : phase === 'waiting_fit' ? '等待佩戴贴合...' : phase === 'collecting' ? '采集数据 (' + frameCount + ')...' : '检测完成,正在启动...'}}</view>
</view>
<view class="page-content">
<view class="scan-container" wx:if="{{scanning}}">
<view class="face-outline">
<view class="scan-line"></view>
</view>
<view class="scan-progress-bar">
<view class="scan-progress-fill" style="width: {{scanProgress}}%"></view>
</view>
<view class="scan-status">固件序贯扫描中:红光 → 紫外光 → 黄光 → 红外光</view>
<!-- 待开始 -->
<view wx:if="{{phase === 'ready'}}">
<view class="scan-hint">设备已佩戴,点击下方按钮开始智能检测</view>
<view class="scan-hint sub-hint">检测过程约需 2 分钟,请保持设备贴合</view>
<button class="btn-primary mt-30" bindtap="onStartScan">开始检测</button>
</view>
<view class="scan-result" wx:if="{{!scanning && spectrumData.length > 0}}">
<view class="spectrum-group" wx:for="{{spectrumData}}" wx:key="name">
<view class="spectrum-title">{{item.name}} (第{{index + 1}}组)</view>
<view class="pd-list">
<view class="pd-item" wx:for="{{item.items}}" wx:for-item="pd" wx:key="label">
<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 wx:if="{{phase === 'waiting_fit'}}">
<view class="scan-container">
<view class="face-outline">
<view class="scan-line"></view>
</view>
</view>
<view class="vbat-info" wx:if="{{vbat > 0}}">
电池电压: {{vbat}}mV ({{battery}}%)
<view class="scan-status">正在检测面膜贴合状态,请确保设备正确佩戴...</view>
</view>
<!-- 采集中 -->
<view wx:if="{{phase === 'collecting'}}">
<view class="scan-container">
<view class="face-outline">
<view class="scan-line"></view>
</view>
</view>
<view class="scan-status">已贴合,正在采集光谱数据... 已收到 {{frameCount}} 帧</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 class="scan-error" wx:if="{{!scanning && error}}">{{error}}</view>
<button class="btn-primary mt-30" wx:if="{{!scanning}}" bindtap="onNext">下一步</button>
<view class="scan-error" wx:if="{{error}}">{{error}}</view>
</view>
</view>
+21 -80
查看文件
@@ -1,12 +1,8 @@
var ble = require('../../services/ble')
var FITTING_TIMEOUT = 10000
Page({
data: {
statusBarHeight: 44,
checking: false,
result: null,
error: '',
battery: 0,
connected: false
},
@@ -21,109 +17,54 @@ Page({
onLoad: function () {
var app = getApp()
this.setData({ statusBarHeight: app.globalData.statusBarHeight })
var device = app.globalData.currentDevice || {}
this.setData({
battery: device.battery || 0,
statusBarHeight: app.globalData.statusBarHeight,
connected: ble.isConnected()
})
},
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()
var device = app.globalData.currentDevice || {}
this.setData({ battery: device.battery || 0 })
},
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 () {
var self = this
if (self.data.checking) return
if (ble.isConnected()) {
self.checkWearing()
self.setData({ connected: true })
return
}
self.setData({ checking: true, result: null, error: '', connected: false })
clearTimeout(self._reconnectTimer)
self.setData({ connected: false })
ble.disconnect()
setTimeout(function () {
ble.startScan({
onFound: function () {},
onConnected: function () {
clearTimeout(self._reconnectTimer)
self.setData({ checking: false, connected: true })
self.checkWearing()
self.setData({ connected: true })
wx.showToast({ title: '连接成功', icon: 'success' })
},
onError: function (err) {
clearTimeout(self._reconnectTimer)
self.setData({ checking: false, result: 'fail', error: err.msg || '连接失败' })
wx.showToast({ title: err.msg || '连接失败', icon: 'none' })
}
})
self._reconnectTimer = setTimeout(function () {
ble.stopScan()
self.setData({ checking: false, result: 'fail', error: '搜索超时,请确认设备已开机' })
wx.showToast({ title: '搜索超时', icon: 'none' })
}, 15000)
}, 500)
},
onUnload: function () {
clearTimeout(this._reconnectTimer)
ble.stopScan()
}
})
+2 -13
查看文件
@@ -28,18 +28,7 @@
</view>
</view>
<button class="btn-primary" bindtap="checkWearing" disabled="{{checking}}" loading="{{checking}}">
确认已佩戴
</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>
<button class="btn-primary" wx:if="{{connected}}" bindtap="onNext">确认已佩戴,开始检测</button>
<button class="btn-secondary" wx:if="{{!connected}}" bindtap="onRetry">重新连接</button>
</view>
</view>