// Skin-tendency diagnosis — FRAMEWORK / placeholder implementation. // // STATUS: the diagnosis 依据 is NOT yet calibrated (客户也认为"还只是概念"). // This module defines the full data pipeline so the report UI + storage work end // to end today, but the thresholds and the PD->region mapping are PLACEHOLDERS to // be replaced after real-device calibration. It degrades gracefully when only // partial scan data (currently single-wavelength red) is available. // // Two things here need hardware/calibration confirmation before this is "real": // 1) PD_REGION_MAP — which of the 7 PDs feed which facial region (docs disagree). // 2) levels thresholds — normalized-absorption cutoffs per severity level. // Both can be overridden at runtime by the server-side diagnosis_config, so tuning // after calibration does NOT require an app release. // Problem type -> recommended care wavelength (IR=1, R=2, UV=3, Y=4). var PROBLEM_WAVELENGTH = { aging: 2, acne: 3, pigment: 4, deep: 1 } // Scanned wavelength code -> the problem it reveals (inverse of the above). var WAVELENGTH_PROBLEM = { 1: 'deep', 2: 'aging', 3: 'acne', 4: 'pigment' } // Dictionary wave-key -> wavelength code. var WAVE_KEY_CODE = { red: 2, uv: 3, yellow: 4, ir: 1 } // 5 regions in the app's neutral convention (right/left/top/middle/bottom), // matching treatment-setup.js masks. var REGION_DEFS = [ { key: 'right', mask: 0x01 }, { key: 'left', mask: 0x02 }, { key: 'top', mask: 0x04 }, { key: 'middle', mask: 0x08 }, { key: 'bottom', mask: 0x10 } ] // PLACEHOLDER PD->region map. Flow doc: 7 PDs = 1 special region (3 PDs) + 4 regions // (1 PD each). The exact assignment is UNCONFIRMED — replace after hardware confirms // the PD physical layout. Each region lists the PD index(es) that feed its signal. var PD_REGION_MAP = { right: [0], left: [1], top: [2], middle: [3, 5, 6], bottom: [4] } var MAX_ADC = 4095 var DEFAULT_CONFIG = { calibrated: false, levels: { low: 0.15, mid: 0.35, high: 0.55 }, problem_wavelength: PROBLEM_WAVELENGTH } function _avg(arr) { if (!arr || !arr.length) return 0 var s = 0 for (var i = 0; i < arr.length; i++) s += arr[i] return s / arr.length } // Normalized absorption in [0,1]: the more the light is absorbed (lower reflected PD // relative to the底噪 baseline), the stronger the tendency signal. function _absorption(pdValue, baseline) { if (!baseline || baseline <= 0) return 0 var a = (baseline - pdValue) / baseline if (a < 0) a = 0 if (a > 1) a = 1 return a } function _levelOf(score, levels) { if (score >= levels.high) return 3 if (score >= levels.mid) return 2 if (score >= levels.low) return 1 return 0 } // scanData: { // device_id, scanned_at, // region_mask, // which regions were scanned (default 0x1F) // noise: [7], // per-PD 底噪 baseline (optional; defaults MAX_ADC) // waves: { red:[7], uv:[7], yellow:[7], ir:[7] } // any subset; each = 7 PD values // } function analyze(scanData, config) { config = config || DEFAULT_CONFIG var levels = config.levels || DEFAULT_CONFIG.levels var problemWave = config.problem_wavelength || PROBLEM_WAVELENGTH scanData = scanData || {} var waves = scanData.waves || {} var noise = scanData.noise var scannedMask = scanData.region_mask || 0x1F var regions = [] var overallAcc = {} for (var r = 0; r < REGION_DEFS.length; r++) { var def = REGION_DEFS[r] if ((scannedMask & def.mask) === 0) continue var pdIdx = PD_REGION_MAP[def.key] || [] var problems = [] for (var wk in waves) { if (!waves.hasOwnProperty(wk)) continue var code = WAVE_KEY_CODE[wk] var problemType = WAVELENGTH_PROBLEM[code] if (!problemType) continue var pdVals = [] var baseVals = [] for (var k = 0; k < pdIdx.length; k++) { var idx = pdIdx[k] if (waves[wk][idx] !== undefined) pdVals.push(waves[wk][idx]) baseVals.push(noise && noise[idx] !== undefined ? noise[idx] : MAX_ADC) } if (!pdVals.length) continue var score = _absorption(_avg(pdVals), _avg(baseVals)) var level = _levelOf(score, levels) if (level > 0) { problems.push({ type: problemType, level: level, score: Math.round(score * 100) / 100 }) if (!overallAcc[problemType] || level > overallAcc[problemType]) { overallAcc[problemType] = level } } } problems.sort(function (a, b) { return (b.level - a.level) || (b.score - a.score) }) var top = problems.length ? problems[0] : null regions.push({ region: def.key, mask: def.mask, top_problem: top ? top.type : null, level: top ? top.level : 0, problems: problems }) } 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 }) // Recommendation: care each region-with-a-top-problem using that problem's wavelength. // One BLE command carries one wavelength, so plan entries are grouped by wavelength. var planByWave = {} var recommendMask = 0 for (var i = 0; i < regions.length; i++) { var reg = regions[i] if (!reg.top_problem) continue var wl = problemWave[reg.top_problem] if (!wl) continue planByWave[wl] = (planByWave[wl] || 0) | reg.mask recommendMask |= reg.mask } var recommendPlan = [] for (var w in planByWave) { if (!planByWave.hasOwnProperty(w)) continue var code2 = parseInt(w, 10) recommendPlan.push({ wavelength: code2, mask: planByWave[w], label_key: WAVELENGTH_PROBLEM[code2] }) } 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 } } // Adapt the current (single-wavelength red) auto-scan result into scanData, so the // pipeline works today. pdAvg = 7 averaged PD values under red light. function fromRedScan(pdAvg, regionMask, deviceId, scannedAt, noise) { return { device_id: deviceId || null, scanned_at: scannedAt || 0, region_mask: regionMask || 0x1F, noise: noise || null, waves: { red: (pdAvg || []).slice(0, 7) } } } // Deterministic mock report for UI development / demo when there is no device data. function mockReport() { return analyze({ device_id: 'MOCK', scanned_at: 0, region_mask: 0x1F, noise: [4000, 4000, 4000, 4000, 4000, 4000, 4000], waves: { red: [3500, 3600, 1900, 3400, 3450, 3500, 3500], uv: [3700, 3750, 3600, 1700, 3700, 1800, 1900], yellow: [3500, 3550, 3400, 3450, 2300, 3500, 3520], ir: [3600, 3600, 3550, 3580, 3560, 3600, 3600] } }, DEFAULT_CONFIG) } module.exports = { DEFAULT_CONFIG: DEFAULT_CONFIG, PROBLEM_WAVELENGTH: PROBLEM_WAVELENGTH, WAVELENGTH_PROBLEM: WAVELENGTH_PROBLEM, REGION_DEFS: REGION_DEFS, analyze: analyze, fromRedScan: fromRedScan, mockReport: mockReport }