feat(debug): temporary BLE debug page for on-device verification
- entry in profile menu, visible only while a device is connected - live FFE4 readout: 7x PD, VBAT, fitting, scan wave marker, run state - tap a face-map region to fire a 2s single-region treat command (verifies region-to-IO mapping on the physical mask) - wavelength selector (red/ir/uv/yellow) to verify color byte order - scan command / full-face 2s / stop buttons, hex log of every write - intentionally not i18n'd; remove page before release
这个提交包含在:
+2
-1
@@ -20,7 +20,8 @@
|
|||||||
"pages/contact/contact",
|
"pages/contact/contact",
|
||||||
"pages/light-info/light-info",
|
"pages/light-info/light-info",
|
||||||
"pages/manual-treatment/manual-treatment",
|
"pages/manual-treatment/manual-treatment",
|
||||||
"pages/scan-report/scan-report"
|
"pages/scan-report/scan-report",
|
||||||
|
"pages/ble-debug/ble-debug"
|
||||||
],
|
],
|
||||||
"window": {
|
"window": {
|
||||||
"navigationBarBackgroundColor": "#ffffff",
|
"navigationBarBackgroundColor": "#ffffff",
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
// TEMPORARY debug page for on-device verification (region↔IO mapping, color byte
|
||||||
|
// order, scan wave bucketing, PD polarity). Guoguo-only tooling — intentionally
|
||||||
|
// NOT i18n'd; remove this page before any release build.
|
||||||
|
var ble = require('../../services/ble')
|
||||||
|
var protocol = require('../../services/ble/protocol')
|
||||||
|
|
||||||
|
var WAVES = [
|
||||||
|
{ key: 'red', code: 2, label: '红光' },
|
||||||
|
{ key: 'ir', code: 1, label: '红外' },
|
||||||
|
{ key: 'uv', code: 3, label: '紫外' },
|
||||||
|
{ key: 'yellow', code: 4, label: '暖黄' }
|
||||||
|
]
|
||||||
|
var REGION_LABELS = { 1: '左区/IO1', 2: '中上/IO2', 4: '中区/IO3', 8: '中下/IO4', 16: '右区/IO5' }
|
||||||
|
var RUN_STATES = { 0: 'IDLE', 1: '检测', 2: '治疗', 255: '故障(0xFF)' }
|
||||||
|
|
||||||
|
function hex(bytes) {
|
||||||
|
return bytes.map(function (b) { return ('0' + b.toString(16)).slice(-2).toUpperCase() }).join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
Page({
|
||||||
|
data: {
|
||||||
|
statusBarHeight: 44,
|
||||||
|
connected: false,
|
||||||
|
waves: WAVES,
|
||||||
|
selectedWave: 'red',
|
||||||
|
// live FFE4 status
|
||||||
|
pd: [], vbat: 0, battery: 0, fitting: -1, scanWave: '-', runState: '-', frames: 0,
|
||||||
|
logs: []
|
||||||
|
},
|
||||||
|
|
||||||
|
onLoad: function () {
|
||||||
|
this.setData({ statusBarHeight: getApp().globalData.statusBarHeight })
|
||||||
|
},
|
||||||
|
|
||||||
|
onShow: function () {
|
||||||
|
var self = this
|
||||||
|
self.setData({ connected: ble.isConnected() })
|
||||||
|
self._onAdc = function (d) {
|
||||||
|
self.setData({
|
||||||
|
pd: d.pd || [],
|
||||||
|
vbat: d.vbat || 0,
|
||||||
|
battery: d.battery || 0,
|
||||||
|
fitting: d.fitting,
|
||||||
|
scanWave: d.scan_wave || '-',
|
||||||
|
runState: RUN_STATES[d.run_state] !== undefined ? RUN_STATES[d.run_state] : String(d.run_state),
|
||||||
|
frames: self.data.frames + 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ble.on('adc', self._onAdc)
|
||||||
|
},
|
||||||
|
|
||||||
|
onHide: function () { this._cleanup() },
|
||||||
|
onUnload: function () { this._cleanup() },
|
||||||
|
_cleanup: function () {
|
||||||
|
if (this._onAdc) { ble.off('adc', this._onAdc); this._onAdc = null }
|
||||||
|
},
|
||||||
|
|
||||||
|
onBack: function () {
|
||||||
|
wx.navigateBack({ fail: function () { wx.switchTab({ url: '/pages/profile/profile' }) } })
|
||||||
|
},
|
||||||
|
|
||||||
|
_log: function (msg) {
|
||||||
|
var logs = this.data.logs
|
||||||
|
logs.unshift(new Date().toTimeString().slice(0, 8) + ' ' + msg)
|
||||||
|
if (logs.length > 20) logs.pop()
|
||||||
|
this.setData({ logs: logs })
|
||||||
|
},
|
||||||
|
|
||||||
|
_send: function (label, options) {
|
||||||
|
var self = this
|
||||||
|
if (!ble.isConnected()) {
|
||||||
|
self.setData({ connected: false })
|
||||||
|
wx.showToast({ title: '设备未连接', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var bytes = protocol.buildVendorCommand(options)
|
||||||
|
self._log(label)
|
||||||
|
self._log('→ ' + hex(bytes))
|
||||||
|
ble.setParams(options).then(function () {
|
||||||
|
self._log('✓ 写入成功')
|
||||||
|
}).catch(function (err) {
|
||||||
|
self._log('✗ 写入失败: ' + ((err && err.msg) || JSON.stringify(err)))
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
onSelectWave: function (e) {
|
||||||
|
this.setData({ selectedWave: e.currentTarget.dataset.key })
|
||||||
|
},
|
||||||
|
|
||||||
|
_waveCode: function () {
|
||||||
|
var key = this.data.selectedWave
|
||||||
|
for (var i = 0; i < WAVES.length; i++) if (WAVES[i].key === key) return WAVES[i].code
|
||||||
|
return 2
|
||||||
|
},
|
||||||
|
|
||||||
|
// 点脸型图分区:该区 2 秒治疗(验证物理位置对不对)
|
||||||
|
onTapRegion: function (e) {
|
||||||
|
var mask = parseInt(e.currentTarget.dataset.mask)
|
||||||
|
this._send('治疗 2s ' + REGION_LABELS[mask] + ' ' + this.data.selectedWave, {
|
||||||
|
region_mask: mask, wavelength: this._waveCode(), brightness: 204,
|
||||||
|
hold_time: 2, control: 0x02
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 检测指令(与 auto-scan 同参:红光 128 全脸,设备自跑序贯扫描)
|
||||||
|
onScanCmd: function () {
|
||||||
|
this._send('检测指令 control=0x01', {
|
||||||
|
region_mask: 0x1F, wavelength: 2, brightness: 128,
|
||||||
|
hold_time: 0, control: 0x01
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 全脸当前波长 2 秒
|
||||||
|
onTreatAll: function () {
|
||||||
|
this._send('治疗 2s 全脸 ' + this.data.selectedWave, {
|
||||||
|
region_mask: 0x1F, wavelength: this._waveCode(), brightness: 204,
|
||||||
|
hold_time: 2, control: 0x02
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 停止 / IDLE
|
||||||
|
onStop: function () {
|
||||||
|
var self = this
|
||||||
|
self._log('停止 → IDLE 全零帧')
|
||||||
|
ble.stopTreatment().then(function () { self._log('✓ 已停止') })
|
||||||
|
.catch(function (err) { self._log('✗ 停止失败: ' + ((err && err.msg) || '')) })
|
||||||
|
},
|
||||||
|
|
||||||
|
onClearLog: function () { this.setData({ logs: [], frames: 0 }) }
|
||||||
|
})
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "调试工具"
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<!-- 临时调试页:真机联调用(区域/波长/极性/分桶验证),上线前移除,不走 i18n -->
|
||||||
|
<view class="page">
|
||||||
|
<view class="page-header page-header-gradient" style="padding-top: {{statusBarHeight + 8}}px;">
|
||||||
|
<view class="nav-back nav-back-light" bindtap="onBack">‹ 返回</view>
|
||||||
|
<view class="page-header-title">调试工具(临时)</view>
|
||||||
|
<view class="page-header-subtitle">{{connected ? '设备已连接' : '设备未连接'}}</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="page-content">
|
||||||
|
<!-- 实时状态 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="card-title">FFE4 实时状态 <text class="frames">({{frames}} 帧)</text></view>
|
||||||
|
<view class="pd-row">
|
||||||
|
<view class="pd-cell" wx:for="{{pd}}" wx:key="*this">
|
||||||
|
<view class="pd-idx">PD{{index + 1}}</view>
|
||||||
|
<view class="pd-val">{{item}}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="status-row">
|
||||||
|
<text>VBAT {{vbat}}mV ({{battery}}%)</text>
|
||||||
|
<text>贴合 {{fitting}}</text>
|
||||||
|
<text>光色 {{scanWave}}</text>
|
||||||
|
<text>状态 {{runState}}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 波长选择 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="card-title">波长(点分区/全脸时使用)</view>
|
||||||
|
<view class="wave-row">
|
||||||
|
<view class="wave-chip {{selectedWave === item.key ? 'active' : ''}}" wx:for="{{waves}}" wx:key="key"
|
||||||
|
bindtap="onSelectWave" data-key="{{item.key}}">{{item.label}}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 脸型图:点区发 2 秒治疗 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="card-title">点分区 → 该区 2 秒治疗(对物理位置)</view>
|
||||||
|
<view class="face-map">
|
||||||
|
<view class="face-region forehead" bindtap="onTapRegion" data-mask="2">中上\nIO2</view>
|
||||||
|
<view class="face-region left-cheek" bindtap="onTapRegion" data-mask="1">左\nIO1</view>
|
||||||
|
<view class="face-region right-cheek" bindtap="onTapRegion" data-mask="16">右\nIO5</view>
|
||||||
|
<view class="face-region nose" bindtap="onTapRegion" data-mask="4">中\nIO3</view>
|
||||||
|
<view class="face-region chin" bindtap="onTapRegion" data-mask="8">中下\nIO4</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 指令按钮 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="card-title">指令</view>
|
||||||
|
<view class="btn-row">
|
||||||
|
<button class="dbg-btn" bindtap="onScanCmd">检测指令</button>
|
||||||
|
<button class="dbg-btn" bindtap="onTreatAll">全脸 2s</button>
|
||||||
|
<button class="dbg-btn dbg-btn-stop" bindtap="onStop">停止</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 日志 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="card-title">发送日志 <text class="log-clear" bindtap="onClearLog">清空</text></view>
|
||||||
|
<view class="log-line" wx:for="{{logs}}" wx:key="*this">{{item}}</view>
|
||||||
|
<view class="log-empty" wx:if="{{logs.length === 0}}">暂无</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/* 临时调试页样式(上线前随页面一并移除) */
|
||||||
|
.page { min-height: 100vh; background: #f5f6f8; }
|
||||||
|
.page-content { padding: 24rpx; }
|
||||||
|
|
||||||
|
.card { background: #fff; border-radius: 24rpx; padding: 24rpx; margin-bottom: 20rpx; }
|
||||||
|
.card-title { font-size: 28rpx; font-weight: 600; color: #333; margin-bottom: 16rpx; }
|
||||||
|
.frames { font-size: 22rpx; color: #999; font-weight: 400; }
|
||||||
|
|
||||||
|
.pd-row { display: flex; flex-wrap: wrap; gap: 12rpx; }
|
||||||
|
.pd-cell { flex: 0 0 calc((100% - 72rpx) / 7); text-align: center; background: #FAFAFA; border-radius: 12rpx; padding: 10rpx 0; }
|
||||||
|
.pd-idx { font-size: 20rpx; color: #999; }
|
||||||
|
.pd-val { font-size: 24rpx; color: #333; font-weight: 600; }
|
||||||
|
.status-row { display: flex; flex-wrap: wrap; gap: 20rpx; margin-top: 16rpx; font-size: 24rpx; color: #666; }
|
||||||
|
|
||||||
|
.wave-row { display: flex; gap: 16rpx; }
|
||||||
|
.wave-chip { flex: 1; text-align: center; padding: 18rpx 0; border: 4rpx solid #e5e5e5; border-radius: 20rpx; font-size: 26rpx; color: #999; }
|
||||||
|
.wave-chip.active { border-color: #E6508C; background: rgba(230, 80, 140, 0.12); color: #E6508C; }
|
||||||
|
|
||||||
|
.face-map { width: 280rpx; height: 340rpx; margin: 0 auto; position: relative; }
|
||||||
|
.face-region {
|
||||||
|
position: absolute; border: 4rpx solid #d5d5d5; border-radius: 50%;
|
||||||
|
display: flex; align-items: center; justify-content: center; text-align: center;
|
||||||
|
font-size: 22rpx; color: #666; background: #FAFAFA; white-space: pre-line; line-height: 1.3;
|
||||||
|
}
|
||||||
|
.face-region:active { background: rgba(230, 80, 140, 0.25); border-color: #E6508C; color: #E6508C; }
|
||||||
|
.face-region.forehead { top: 5%; left: 25%; width: 50%; height: 15%; border-radius: 40rpx 40rpx 50% 50%; }
|
||||||
|
.face-region.left-cheek { top: 25%; left: 5%; width: 30%; height: 25%; }
|
||||||
|
.face-region.right-cheek { top: 25%; right: 5%; width: 30%; height: 25%; }
|
||||||
|
.face-region.nose { top: 45%; left: 35%; width: 30%; height: 18%; border-radius: 40%; }
|
||||||
|
.face-region.chin { top: 68%; left: 25%; width: 50%; height: 20%; border-radius: 0 0 50% 50%; }
|
||||||
|
|
||||||
|
.btn-row { display: flex; gap: 16rpx; }
|
||||||
|
.dbg-btn { flex: 1; font-size: 26rpx; padding: 0; line-height: 76rpx; border-radius: 20rpx; background: #4A90D9; color: #fff; }
|
||||||
|
.dbg-btn::after { border: none; }
|
||||||
|
.dbg-btn-stop { background: #E8503A; }
|
||||||
|
|
||||||
|
.log-clear { float: right; font-size: 22rpx; color: #4A90D9; font-weight: 400; }
|
||||||
|
.log-line { font-family: monospace; font-size: 20rpx; color: #555; padding: 4rpx 0; border-bottom: 1rpx solid #f2f2f2; word-break: break-all; }
|
||||||
|
.log-empty { font-size: 22rpx; color: #bbb; }
|
||||||
@@ -19,6 +19,13 @@ Page({
|
|||||||
onShow: function () {
|
onShow: function () {
|
||||||
i18n.bind(this)
|
i18n.bind(this)
|
||||||
this.loadProfile()
|
this.loadProfile()
|
||||||
|
// 临时调试入口:仅设备已连接时显示(上线前随 ble-debug 页一并移除)
|
||||||
|
var ble = require('../../services/ble')
|
||||||
|
this.setData({ debugVisible: ble.isConnected() })
|
||||||
|
},
|
||||||
|
|
||||||
|
onOpenDebug: function () {
|
||||||
|
wx.navigateTo({ url: '/pages/ble-debug/ble-debug' })
|
||||||
},
|
},
|
||||||
|
|
||||||
onLightInfo: function () {
|
onLightInfo: function () {
|
||||||
|
|||||||
@@ -50,6 +50,12 @@
|
|||||||
<view class="menu-text">{{i18n.profile.menuHistory}}</view>
|
<view class="menu-text">{{i18n.profile.menuHistory}}</view>
|
||||||
<text class="menu-arrow">›</text>
|
<text class="menu-arrow">›</text>
|
||||||
</view>
|
</view>
|
||||||
|
<!-- 临时调试入口:仅设备已连接时显示,上线前移除 -->
|
||||||
|
<view class="menu-item" wx:if="{{debugVisible}}" bindtap="onOpenDebug">
|
||||||
|
<view class="menu-icon">🔧</view>
|
||||||
|
<view class="menu-text">调试工具(临时)</view>
|
||||||
|
<text class="menu-arrow">›</text>
|
||||||
|
</view>
|
||||||
<view class="menu-item" bindtap="onViewSubscription" wx:if="{{!subscription || subscription.plan !== 'free'}}">
|
<view class="menu-item" bindtap="onViewSubscription" wx:if="{{!subscription || subscription.plan !== 'free'}}">
|
||||||
<view class="menu-icon">💳</view>
|
<view class="menu-icon">💳</view>
|
||||||
<view class="menu-text">{{i18n.profile.menuSubscription}}</view>
|
<view class="menu-text">{{i18n.profile.menuSubscription}}</view>
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户