// Skin-tendency diagnosis — 按《扫描+护理完整流程.docx》7.1/7.2/7.3 + // 《协议简述-添加扫描时光色.docx》+ 果果 2026-07 定案实现。 // // 核心链路:28 组 PD(4 波长×7 路) → 按 PD_REGION_MAP 聚合成 5 区 → // 每波长算 5 区平均当基准 → 吸收度 NR = 本区/基准 − 1 → // 每区取 4 个 NR 的最大者(argmax),超过阈值 TH 才判定问题 → 该波长即该区治疗光色。 // // 极性(已由固件批注版文档坐实,新版协议简述再次确认"0x0FFF表示最低光照"): // PD ADC 值越大 = 反射光越弱 = 皮肤吸收越多。故 NR = zone/avg − 1(吸收多→NR 高)。 // config.polarity = 'direct' 可切回"值大=光强"的旧假设(备用开关,标定时救急)。 // // 仍未确认(走 config 热调,勿写死): // 1) PD_REGION_MAP —— 7 路 PD 与 5 区的物理对应,所有文档从未给出,当前为推测值。 // 2) 3 路 PD 区的聚合 —— 文档 7.1 自相矛盾(几何平均 vs 取最小),按更细的几何平均+3σ剔异常实现。 // 所有阈值/亮度/映射均可被服务端 diagnosis_config 覆盖,标定后免发版调参。 // 问题类型 -> 治疗波长编码(下发命令用:IR=1, R=2, UV=3, Y=4)。 var PROBLEM_WAVELENGTH = { aging: 2, acne: 3, pigment: 4, deep: 1 } // 波长 key -> 揭示的问题类型。 var WAVE_PROBLEM = { red: 'aging', uv: 'acne', yellow: 'pigment', ir: 'deep' } // 波长 key -> 下发命令编码。 var WAVE_KEY_CODE = { red: 2, uv: 3, yellow: 4, ir: 1 } // 5 区 = IO 纵列布局(果果定案):左=IO1 中上=IO2 中=IO3 中下=IO4 右=IO5。 var REGION_DEFS = [ { key: 'left', mask: 0x01 }, { key: 'top', mask: 0x02 }, { key: 'middle', mask: 0x04 }, { key: 'bottom', mask: 0x08 }, { key: 'right', mask: 0x10 } ] var DEFAULT_CONFIG = { calibrated: false, polarity: 'inverted', // inverted=值大吸收多(已确认) | direct=旧假设备用 th: 0.10, // 判定阈值(文档建议初始值,需临床标定) severity: { low: 0.05, mid: 0.15, high: 0.30 }, // 轻/中/重分级,仅报告展示 brightness: 204, // 治疗强度 80%(果果定案),仅用于推荐方案下发 // 推测映射:中区含 3 路 PD(文档"1 特殊区 3 路+4 普通区各 1 路"),待固件确认后改 config。 pd_region_map: { left: [0], top: [1], middle: [2, 3, 4], bottom: [5], right: [6] }, problem_wavelength: PROBLEM_WAVELENGTH } function _median(arr) { var s = arr.slice().sort(function (a, b) { return a - b }) var m = Math.floor(s.length / 2) return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2 } // 区域有效值:1 路直接用;≥3 路按 7.1 节"几何平均 + 剔奇异值"(防黑痣/毛发单点失真)。 // 剔异常用 MAD(中位数绝对偏差)而非文档字面的 3σ:n=3 时单个离群点会把标准差同步撑大, // 3σ 判据永不触发(审计实证 [900,9000,905] 不剔);MAD 阈值 4.45×MAD ≈ 3σ 等效(3×1.4826)。 function _aggregate(vals) { if (!vals.length) return 0 if (vals.length === 1) return vals[0] var kept = vals if (vals.length >= 3) { var med = _median(vals) var devs = vals.map(function (v) { return Math.abs(v - med) }) var mad = _median(devs) var thr = 4.45 * mad var filtered = vals.filter(function (v) { return Math.abs(v - med) <= thr }) if (filtered.length >= 2) kept = filtered } var logSum = 0 for (var i = 0; i < kept.length; i++) { if (kept[i] <= 0) return 0 // 有无效 0 值时该区数据不可信 logSum += Math.log(kept[i]) } return Math.exp(logSum / kept.length) } function _severityLevel(nr, severity) { if (nr > severity.high) return 3 if (nr > severity.mid) return 2 if (nr > severity.low) return 1 return 0 } // scanData: { // device_id, scanned_at, // region_mask, // 本次扫描选中的区域(默认 0x1F) // waves: { red:[7], ir:[7], uv:[7], yellow:[7] } // 任意子集;每项 = 7 路 PD ADC // } function analyze(scanData, config) { config = config || DEFAULT_CONFIG var th = config.th !== undefined ? config.th : DEFAULT_CONFIG.th var severity = config.severity || DEFAULT_CONFIG.severity var brightness = config.brightness || DEFAULT_CONFIG.brightness var pdMap = config.pd_region_map || DEFAULT_CONFIG.pd_region_map var problemWave = config.problem_wavelength || PROBLEM_WAVELENGTH var inverted = config.polarity !== 'direct' scanData = scanData || {} var waves = scanData.waves || {} var scannedMask = scanData.region_mask || 0x1F // 1) 聚合:每波长 × 每选中区 → 区域有效值 var zone = {} // zone[waveKey][regionKey] = 有效值 var included = [] // 参与本次扫描的区域定义 var r, def for (r = 0; r < REGION_DEFS.length; r++) { if ((scannedMask & REGION_DEFS[r].mask) !== 0) included.push(REGION_DEFS[r]) } for (var wk in waves) { if (!waves.hasOwnProperty(wk) || !WAVE_PROBLEM[wk]) continue var pdArr = waves[wk] if (!pdArr || !pdArr.length) continue zone[wk] = {} for (r = 0; r < included.length; r++) { def = included[r] var idxs = pdMap[def.key] || [] var vals = [] for (var k = 0; k < idxs.length; k++) { var v = pdArr[idxs[k]] if (v !== undefined && v !== null && v > 0) vals.push(v) } if (vals.length) zone[wk][def.key] = _aggregate(vals) } } // 2) 每波长的基准 = 该波长下各区有效值的平均(7.1:Avg_PD_X_AllZones) var avg = {} for (wk in zone) { if (!zone.hasOwnProperty(wk)) continue var sum = 0 var n = 0 for (r = 0; r < included.length; r++) { var zv = zone[wk][included[r].key] if (zv !== undefined) { sum += zv; n++ } } if (n > 0) avg[wk] = sum / n } // 3) 吸收度 NR + 每区 argmax 判定 var regions = [] var overallAcc = {} for (r = 0; r < included.length; r++) { def = included[r] var nrs = [] // 该区所有可算波长的 {wave, nr} for (wk in zone) { if (!zone.hasOwnProperty(wk)) continue if (zone[wk][def.key] === undefined || !avg[wk]) continue var ratio = zone[wk][def.key] / avg[wk] var nr = inverted ? (ratio - 1) : (1 - ratio) nrs.push({ wave: wk, nr: Math.round(nr * 1000) / 1000 }) } nrs.sort(function (a, b) { return b.nr - a.nr }) var top = nrs.length ? nrs[0] : null var hasProblem = !!(top && top.nr > th) var problemType = hasProblem ? WAVE_PROBLEM[top.wave] : null var level = hasProblem ? Math.max(1, _severityLevel(top.nr, severity)) : 0 if (problemType && (!overallAcc[problemType] || level > overallAcc[problemType])) { overallAcc[problemType] = level } regions.push({ region: def.key, mask: def.mask, top_problem: problemType, wave: hasProblem ? top.wave : null, level: level, score: top ? top.nr : 0, nrs: nrs // 全量吸收度,供标定期回看 }) } var overall = [] for (var pt in overallAcc) { if (overallAcc.hasOwnProperty(pt)) overall.push({ type: pt, level: overallAcc[pt] }) } overall.sort(function (a, b) { return b.level - a.level }) // 4) 推荐方案:每个问题区一条(区独立、可不同色),一条 BLE 命令即可全部下发。 var recommendPlan = [] var recommendMask = 0 for (r = 0; r < regions.length; r++) { var reg = regions[r] if (!reg.top_problem || !reg.wave) continue recommendPlan.push({ region: reg.region, mask: reg.mask, wave: reg.wave, wavelength: WAVE_KEY_CODE[reg.wave], label_key: reg.top_problem, level: reg.level, brightness: brightness }) recommendMask |= reg.mask } return { device_id: scanData.device_id || null, scanned_at: scanData.scanned_at || 0, regions: regions, overall: overall, recommend_mask: recommendMask, recommend_plan: recommendPlan, raw_pd: waves, calibrated: !!config.calibrated } } // 把 auto-scan 按波长分桶后的平均值组装成 scanData。 // buckets: { red:[7], ir:[7], uv:[7], yellow:[7] } 任意子集(旧固件无波长标识时只有 red)。 function buildScanData(buckets, regionMask, deviceId, scannedAt) { return { device_id: deviceId || null, scanned_at: scannedAt || 0, region_mask: regionMask || 0x1F, waves: buckets || {} } } // 无设备数据时的演示报告(开发者工具 mock)。 // 数值按已确认极性编造:值大=吸收多 → 左区红光偏高(光老化)、中区紫外偏高(痘痘油脂)。 function mockReport() { return analyze({ device_id: 'MOCK', scanned_at: 0, region_mask: 0x1F, waves: { // 左 中上 中(3路) 中下 右 red: [1350, 920, 900, 910, 905, 890, 940], uv: [2050, 2020, 2600, 2560, 2620, 1990, 2080], yellow: [1500, 1520, 1490, 1505, 1495, 1530, 1510], ir: [1800, 1790, 1805, 1795, 1800, 1815, 1790] } }, DEFAULT_CONFIG) } module.exports = { DEFAULT_CONFIG: DEFAULT_CONFIG, PROBLEM_WAVELENGTH: PROBLEM_WAVELENGTH, WAVE_PROBLEM: WAVE_PROBLEM, WAVE_KEY_CODE: WAVE_KEY_CODE, REGION_DEFS: REGION_DEFS, analyze: analyze, buildScanData: buildScanData, mockReport: mockReport }