news 2026/9/15 16:57:18

微信小程序睡眠检测实战:从加速度计到状态机的完整拆解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
微信小程序睡眠检测实战:从加速度计到状态机的完整拆解

简介:压缩包内是一款面向普通用户与轻度失眠人群的睡眠检测微信小程序,基于微信小程序原生框架开发,通过睡眠时长、深度、翻身次数等数据完成智能分析与可视化展示。资源共110个文件,主要包括53个png图标与界面素材、14个js逻辑脚本、13个json配置与数据文件、11个wxss样式、10个wxml页面结构,另有doc文档与mp4演示视频等,包体约22.59MB,结构清晰适合直接学习或二次开发。目前已有88人浏览学习。资源中除完整小程序源码外,还附带《睡眠助手》设计与实现文档、开题报告、效果展示gif及echarts图表组件,能够帮助开发者快速理解小程序项目从需求分析、界面设计到数据可视化的完整流程。对于关注睡眠健康的用户,可直接在微信中运行体验;对于小程序初学者,则可将其作为实战参考,学习页面布局、数据绑定、图表渲染等关键技能。

1. 一个能跑的睡眠检测小程序,拆开 Zip 后你会看到什么

拿到一个落地的“睡眠检测小程序.zip”,比看十篇睡眠 App 的界面评测更有价值。它不是一个 PPT 原型,也不是几张高保真图为了一等奖打磨出的静态稿,而是一份可交付、可运行、可二次改写的微信小程序源码包。你解压后会看到pages目录里的sleepreportsetting三组页面,utils里大概率躺着传感器数据处理模块,app.json里还配好了加速度计的权限申请。这些文件真正要解决的问题只有一个:怎么用手机内置的传感器和微信的 API,在不对用户造成打扰的前提下,推算出晚上躺下、入睡、深睡与醒来的时间段。

这套方案对三类人最有用:做微信小程序毕业设计的在校生,需要一份能现场演示、不至于答辩时白屏的完整代码;初级前端开发,想看看小程序里除了“商城 + 购物车”之外还能切什么赛道;以及正在做健康类产品、想低成本验证睡眠检测需求的独立开发者。本文不分析某个我看不到的源码内部注释,而是以一个一线工程师顺着标题讲方案的思路,把这类小程序从解开压缩包到真机调通的完整路径铺开。

2. 解压 Zip 包与小程序基础结构:先让项目在开发者工具里跑起来

2.1 一份标准微信小程序源码包里必须有哪些文件

不管这个压缩包叫“睡眠检测小程序”还是别的名字,只要是基于微信小程序原生语法写的,解开后必然能看到下图这套固定骨架。微信开发者工具识别一个项目的唯一依据,是根目录下的project.config.json,而不是app.json。前者描述的是“这个目录是给微信开发者工具用的工程”,后者才是小程序真正运行时的全局逻辑入口。

sleep-miniapp/ ├── app.js # 小程序逻辑入口,注册 App() 实例 ├── app.json # 全局配置:页面路由、窗口样式、权限申请 ├── app.wxss # 全局样式表 ├── project.config.json # 开发者工具工程配置,含 appid 与 libVersion ├── sitemap.json # 搜索引擎收录配置,默认即可 ├── pages/ │ ├── index/ # 首页 / 睡眠总览 │ ├── sleep/ # 监测页:启动/停止监测,传感器数据显示 │ └── report/ # 报告页:查看历史睡眠质量 ├── utils/ │ ├── sensor.js # 加速度计数据处理 │ ├── calc.js # 睡眠质量计算(时长、深度、觉醒次数) │ └── format.js # 时间格式化工具 └── images/ # tabBar 图标等静态资源

在终端里解压后,第一件事是打开project.config.json,确认appid字段。如果压缩包作者用的是测试号,appid会是一个touristappid,你导入自己的开发者工具时得替换成自己小程序的 AppID,否则真机预览时会被微信拦在门外。libVersion字段建议升级到2.30.0以上,低版本基础库对wx.onAccelerometerChange这类传感器 API 的支持有兼容缺口。

{ "appid": "wx你的真实appid替换这里", "compileType": "miniprogram", "libVersion": "2.30.0", "projectname": "sleep-miniapp", "setting": { "es6": true, "postcss": true, "minified": true } }

es6设为true是关键,压缩包里如果用了async/awaitPromise而这里没开,开发者工具会在编译时报一堆“语法错误”提示。postcss负责自动补全样式前缀,minified决定上传代码时是否压缩体积,本地调试阶段关掉也无妨,但正式发布建议打开。

2.2 全局配置app.json:权限、页面路由和导航栏动态标题

睡眠检测绕不开加速度计,而微信小程序在申请这类传感器权限时,不能像网页那样通过navigator.permissions直接询问,而是要依赖基础库的隐私接口。app.json里的permission字段是写在全局配置里的,配合sitemapLocation一起声明:

{ "pages": [ "pages/index/index", "pages/sleep/sleep", "pages/report/report" ], "permission": { "scope.userLocation": { "desc": "用于校准睡眠时的设备位置" } }, "requiredBackgroundModes": ["audio"], "window": { "navigationBarTitleText": "睡眠检测", "navigationBarBackgroundColor": "#1f1f2e", "navigationBarTextStyle": "white" } }

requiredBackgroundModes声明音频后台运行,是为了万一睡眠检测的增强版本用到录音分析鼾声,App 切换到后台时还能继续采音。如果你只用加速度计方案,这个字段可以去掉,省得微信审核时问你要后台运行的具体说明。navigationBarBackgroundColor用深色,是为了配合睡眠检测场景的暗色 UI,减少夜间看手机时的刺眼感。

导航栏标题在运行时会动态变。比如用户在睡眠监测页启动检测后,标题从“睡眠检测”改成“监测中 03:24”,这个动作在页面 JS 里用wx.setNavigationBarTitle实现,不是改app.json,改app.json是全局静态的。压缩包里如果没实现这个交互,你拿到后第一件事就应该在onShow生命周期里补上动态标题逻辑,增强“实时感”。

2.3 从 Zip 到可调试状态的 3 条命令行与导入细节

在终端里解压时,macOS 和 Windows 有细节差异。macOS 上用unzip直接解到当前目录,Windows 上如果没有安装解压工具,可以打开 PowerShell 运行下面这条命令,不需要额外装第三方软件。

# macOS / Linux unzip 睡眠检测小程序.zip -d ./sleep-miniapp
# Windows PowerShell Expand-Archive -Path "睡眠检测小程序.zip" -DestinationPath ".\sleep-miniapp"

解压后如果发现文件夹里多了一层__MACOSX,那是 macOS 的隐藏元数据目录,直接删掉即可,不影响小程序运行。导入微信开发者工具时,选择“导入项目”,目录指向解压出来的sleep-miniapp,AppID 按前面说的替换成自己的。如果开发者工具提示“文件过多”或“项目目录不是空的”,检查一下是不是把整个用户主目录选中了,而不是选中包含project.config.json的那个子目录。

导入成功的标志,是模拟器上出现首页“睡眠总览”的界面。如果页面白屏或报app.json解析错误,最可能的原因是压缩包在传输过程中被截断,导致 JSON 文件缺了一个}。这时用 Node 跑一条校验命令,能快速定位问题文件:

node -e "JSON.parse(require('fs').readFileSync('app.json','utf8')); console.log('app.json ok')"

同理检查project.config.json和每个page下的.json文件。大多数“解压了却跑不起来”的纠纷,源头都是这里。源码包作者在打 zip 时可能误用了非 UTF-8 编码,导致中文注释变成乱码,但 JSON 解析只看括号不看注释,如果连解析都报错,那必然是文件本身损坏,这和“zip 压缩包密码破解工具”没有关系,不需要去绕什么加密,正常解压后排查才是正道。

3. 睡眠检测核心逻辑:从加速度计原始数据到睡眠状态机

3.1 体动检测模型的原理与误差边界

睡眠检测小程序里最容易被忽视、却最决定体验的,是数据来源。市面上大多数纯软件睡眠监测 App 采用两种方案:加速度计体动检测和麦克风鼾声识别。前者省电、稳定、不涉及录音隐私,适合做成微信小程序,但精度受“手机放哪儿”影响极大;后者能分析 REM 期,却需要持续录音,微信审核会要求额外声明隐私用途。

标题里锁定了“睡眠检测”而非“睡眠监测医疗”,所以用加速度计方案是取舍后最可靠的做法。它的物理逻辑很直接:人在清醒或浅睡期,身体翻转、手臂移动、抓起手机看时间等动作频繁,传感器感受到的三轴合加速度变化剧烈;进入深睡后,体动明显减少,三轴加速度曲线接近一条平线。

算法实现上不需要做傅里叶变换,常见的做法是取一个时间窗口的加速度标准差。小于某个阈值判定为“稳定期”,连续稳定超过 20 分钟进入“深睡状态”;窗口内有几次剧烈波动则标记为“觉醒事件”。这个四状态模型足够满足毕业设计和初版产品:

IDLE -> FALLING_ASLEEP -> ASLEEP -> WAKE_UP

FALLING_ASLEEP是一个缓冲态,用来过滤“躺下玩手机”和“睡着”之间的模糊地带。如果你刚从床上坐起来拿水杯,加速度计会立刻探测到剧烈变化,但你不能因为一次坐起就把整段睡眠打断。缓冲态就是用来吞掉这种短时扰动的。设置缓冲时长为 5 分钟,意思是体动平息后 5 分钟内没有大幅波动,才真正从“躺下”转为“睡着”。

3.2 用wx.onAccelerometerChange收集数据的完整代码

微信小程序原生提供了wx.onAccelerometerChange这个 API,它会在每次传感器数据变化时回调一个包含xyz三轴加速度的对象,单位是m/s²。注意它和我们高中物理里的重力加速度g不是一回事,手机静止平放时,z轴读数约为9.8。使用前要先通过wx.startAccelerometer设置监听频率,这个interval参数的取值直接决定了电池消耗和精度:

interval 取值回调频率适用场景单小时耗电估算
game约 20ms体感游戏,夜间检测没必要
ui约 60ms页面滚动加速计特效
normal约 200ms睡眠检测推荐值,兼顾精度与续航

normal就够了。睡眠体动不是一个高频信号,200ms 的采样率意味着每秒 5 个数据点,一晚上 8 小时就是 14.4 万个点,这个体量在数组里做滑动窗口计算完全没有性能压力。下面是收集模块的完整代码,放在utils/sensor.js里:

// 传感器数据管理器:缓冲原始三轴数据,并对外提供窗口特征 const sensor = { dataBuffer: [], // 存放最近 30 秒的加速度模长 windowMaxCount: 150, // 30秒 * 5条/秒 = 150条 timer: null, start(interval = 'normal') { wx.startAccelerometer({ interval }) wx.onAccelerometerChange(({ x, y, z }) => { // 算三轴合加速度,消除手机摆放朝向的影响 const magnitude = Math.sqrt(x * x + y * y + z * z) // 重力加速度在9.8附近,这里减去9.8取波动残差 this.dataBuffer.push(Math.abs(magnitude - 9.8)) if (this.dataBuffer.length > this.windowMaxCount) { this.dataBuffer.shift() } }) }, // 返回最近30秒内的体动指数:标准差越大,说明动作越剧烈 getActivityIndex() { if (this.dataBuffer.length < 50) return 0 const mean = this.dataBuffer.reduce((a, b) => a + b, 0) / this.dataBuffer.length const variance = this.dataBuffer.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / this.dataBuffer.length return Math.sqrt(variance) }, stop() { if (this.timer) clearInterval(this.timer) wx.stopAccelerometer() } } module.exports = sensor

代码里的关键点在合加速度逻辑上:手机侧放、竖放、平放,三轴的分配完全不同,如果单看某一轴,同一个翻身动作在不同摆放方式下读到的数值会天差地别。三轴合加速度把方向信息抹平了,只剩“整体运动的剧烈程度”这一个标量,这对睡眠检测来说是好事,因为判断睡眠只需要强度,不需要方向。注释里把每个变量的单位标清楚,这在自己回看或者答辩演示时非常有帮助。

dataBuffershift()维持一个长度为 150 的滑动窗口,窗口越短,对“翻身”这类瞬时动作越敏感,但噪声也越大。30 秒窗口是经验值——人在浅睡期的一次翻身大概持续 2 到 3 秒,如果窗口只有 3 秒,一次翻身就会让标准差飙升,导致整个晚上频繁被误判为“清醒”。窗口拉长到 30 秒,单次翻身的贡献就被稀释了。

3.3 睡眠状态机的状态转移与参数阈值

有了getActivityIndex()返回的体动指数,下一步是将这批连续的浮点数翻译成可读的睡眠阶段。状态机的好处是:判定不是孤立地看当前时刻,而是结合前一状态做转移,能显著减少抖动。下面是状态机核心代码,放在utils/calc.js中:

const SleepState = { IDLE: 'idle', FALLING_ASLEEP: 'falling_asleep', ASLEEP: 'asleep', WAKE: 'wake' } // 状态机配置参数 const config = { fallAsleepSeconds: 300, // 从躺下到入睡的缓冲期,默认5分钟 deepSleepSeconds: 1200, // 连续稳定20分钟判定为深睡 activityWake: 1.2, // 体动指数超过1.2视为一次觉醒 activityCalm: 0.4 // 体动指数低于0.4视为稳定 } const stateMachine = { state: SleepState.IDLE, calmAccumulator: 0, wakeAccumulator: 0, sleepStartTime: null, feed(activityIndex, timestamp) { const isCalm = activityIndex < config.activityCalm const isActive = activityIndex > config.activityWake switch (this.state) { case SleepState.IDLE: if (isCalm) { this.calmAccumulator += 1 if (this.calmAccumulator * 30 >= config.fallAsleepSeconds) { this.state = SleepState.FALLING_ASLEEP this.sleepStartTime = timestamp } } break case SleepState.FALLING_ASLEEP: if (isActive) { // 缓冲期内大幅动作,重新计时 this.calmAccumulator = 0 this.state = SleepState.IDLE } else { this.state = SleepState.ASLEEP } break case SleepState.ASLEEP: if (isActive) { this.wakeAccumulator += 1 if (this.wakeAccumulator * 30 >= 180) { // 连续3分钟体动剧烈,认为醒了 this.state = SleepState.WAKE } } else { this.wakeAccumulator = 0 } break case SleepState.WAKE: if (isCalm) { this.state = SleepState.ASLEEP } break } } } module.exports = { stateMachine, SleepState, config }

这里的activityWake: 1.2activityCalm: 0.4两个阈值是最需要根据真机实测调整的参数。压缩包自带的阈值可能在作者的测试机上表现良好,换一台手机就完全失灵。原因在于不同型号手机的加速度计噪声基底不一样,中低端安卓机在静止状态下标准差能到 0.6,iPhone 则能压到 0.2 以下。如果你的测试机静止时getActivityIndex()返回 0.6,而阈值是 0.4,那机器会一直以为你在翻身,整晚状态机都在 IDLE 和 FALLING_ASLEEP 之间跳。

参数要能调,不能写死在代码里。把这份config对象存到wx.getStorageSync('sleep_config')里,页面加载时读取,用户可以在设置页修改。体动指数的计算间隔是 30 秒,calmAccumulator每次加 1 代表 30 秒稳定,累计 10 次就是 5 分钟。asleep状态里连续 6 次指数超过 1.2 才判觉醒,这个 180 秒的过滤条件非常关键,否则半夜一次的挠头动作就会把整段深睡打断。

3.4 睡眠报告数据结构的建模与统计口径

状态机每 30 秒产生一个状态标签,把一整晚的标签序列汇总成报告。这里要特别提一个容易算错的统计口径:睡眠时长到底是“从 FALLING_ASLEEP 到最终 WAKE 的时间”,还是“ASLEEP 状态累计的时间”?

大多数用户能直观理解的答案是后者。你 23:00 躺下,玩手机到 23:40 才真正入睡,早上 7:00 醒来。那么“总睡眠时长”是 6 小时 20 分钟,而不是 8 小时。如果把躺下就开始计时,用户会吐槽“我明明玩了半小时手机,为什么算我睡了半小时”,这个体验损失很大。建设报告模型时把这两者分开记录:

const reportTemplate = { date: '2025-06-01', bedTime: '23:00', // 用户点“开始监测”的时间 sleepStart: '23:40', // 状态机首次进入 ASLEEP 的时间 wakeTime: '07:00', // 状态机进入 WAKE 且后续未回睡的时间 totalSleepMinutes: 380, // 23:40到7:00之间的ASLEEP累计时长 awakeCount: 2, // 夜间体动导致的觉醒次数 deepSleepMinutes: 75, lightSleepMinutes: 305, qualityScore: 82 }

压缩包里如果实现的是一整晚结束后一次性生成报告,建议改成“每 5 分钟自动保存一次当前时序到本地缓存”。否则用户睡着了,小程序被微信后台回收,等你醒来打开时数据已经丢了。睡眠小程序的数据丢失是最致命的,毕竟没有人愿意为了再次监测重新睡一觉。

4. 数据持久化与睡眠报告页面:用本地存储撑起整个历史记录

4.1 用wx.setStorageSync按天存储睡眠数据

微信小程序的本地缓存容量上限是 10MB,对于纯文本的睡眠记录来说绰绰有余。一天的睡眠报告加上状态时序,撑死了也就 20KB,存一年 365 天不过 7.3MB。所以本地存储方案完全可行,不需要一上来就上云开发。按天分 key 存储,key 直接用日期字符串,例如sleep_report_20250601,查找逻辑简单,也方便做日历视图。

// pages/sleep/sleep.js 中保存报告的核心逻辑 function saveDailyReport(report) { const key = `sleep_report_${report.date.replace(/-/g, '')}` try { wx.setStorageSync(key, report) // 同时维护一个“最近30天报告索引”,供日历视图读取 const indexKey = 'sleep_report_index' const index = wx.getStorageSync(indexKey) || [] if (!index.includes(key)) { index.push(key) // 只保留最近60天的索引,防止缓存无限膨胀 wx.setStorageSync(indexKey, index.slice(-60)) } console.log('睡眠报告已保存:', key) } catch (e) { // 常见于存储已满或隐私模式下写入被拒 console.error('保存失败:', e) } }

存储策略要把“写入频率”和“写入粒度”都说清楚。睡眠过程中的中间态报告每 5 分钟覆盖写一次同一个 key,避免碎片化写操作。当天睡眠结束后,用最终状态覆盖完整报告。这样即使小程序被杀,至少用户丢的数据不超过 5 分钟。index.slice(-60)这行的边界处理值得借鉴:数组长度超过 60 时从头截断,保证缓存目录永恒可控。

4.2 报告页面的加载与动态标题切换

报告页pages/report/report.jsonLoad接收从首页传过来的date参数,读出对应存储并渲染。微信小程序的页面传参只能通过 URL 字符串,所以日期格式要么用20250601这种纯数字,要么对2025-06-01encodeURIComponent,否则中间那个减号在 URL 解析时会出现意外截断。

// pages/report/report.js Page({ data: { report: null, scoreText: '--', scoreColor: '#ccc' }, onLoad(query) { const dateKey = query.date || '20250601' const report = wx.getStorageSync(`sleep_report_${dateKey}`) if (report) { // 动态设置页面标题,显示具体日期 wx.setNavigationBarTitle({ title: `${report.date} 睡眠报告` }) this.setData({ report }) } else { wx.setNavigationBarTitle({ title: '无报告' }) } } })

wx.setNavigationBarTitle必须在页面onLoad里调用,不能在onLaunch全局设置。刷新的标题会覆盖app.json中配置的静态标题,这就是“微信小程序动态设置标题”的正确姿势。报告页里的scoreColor按分数变化,好分数用暖黄色,差分数用灰色,不要用红绿两色,色弱用户可能会看不清。

5. 真机调试、阈值校准与 3 个可以长期用的小技巧

5.1 为什么开发者工具里的加速度计数据是假的

微信开发者工具模拟器没有物理加速度计,wx.onAccelerometerChange在模拟器里会返回一组预先写死的模拟数据,通常是完美平放的{ x: 0, y: 0, z: 9.8 }。这意味着模拟器上体动指数永远是 0,状态机永远判“稳定”,看起来一切正常,到了真机上就原形毕露。排查睡眠检测小程序的问题,第一步就是拿真实手机扫码预览。

真机调试时打开vConsole,在sensor.jsgetActivityIndex里加一行console.log('activity:', this.getActivityIndex())。然后把手机放桌上静止 30 秒,记录下静止基线值;再拿在手里走路 30 秒,记录活动峰值。这两个数字之差决定了你的activityCalmactivityWake应该怎么设。如果静止基线是 0.5,活动峰值是 0.8,说明这台手机传感器噪声高,你可以考虑提高采样频率到ui级别,让数据更密集后再做滑动平均。

5.2 参数实测校准流程:拍平手机与侧放手机的基线对照

校准流程建议做成一个隐藏页面,别让用户自己找。常见做法是在设置页长按“监测灵敏度”标题 3 秒弹出校准面板,运行下面这段流程:

  1. 手机平放桌面,保持 60 秒,计算平均体动指数,记为baseline_flat
  2. 手机侧放(立起来靠在枕头边),保持 60 秒,计算平均体动指数,记为baseline_side
  3. 手拿手机从平放到立起快速翻转 5 次,记录最大体动指数,记为peak_active
  4. 最终activityCalm = Math.max(baseline_flat, baseline_side) * 1.5
  5. 最终activityWake = peak_active * 0.4

这套流程录完后把结果写回存储里的sleep_config,状态机每次启动前读取。有了真机基线,不同手机间的体验就能保持大体一致。这一步做完,睡眠检测的准确率从“图一乐”提升到“能看出昨晚睡得好不好”的日常可用程度,体感差异巨大。

5.3 把校准参数做成可分享二维码,二次分发你的配置

睡眠检测小程序的调试往往需要多人参与,不同手机混测时“我这台阈值不对啊”就成了高频反馈。与其把参数发来发去,不如把sleep_config的 JSON 按encodeURIComponent编码后塞进一个小程序码。用户扫码后自动解析参数并写入本地,这就是给“睡眠检测小程序.zip”这份代码附加的分发技巧。不过要注意微信小程序的二维码生成接口需要后端调用,独立开发者没有服务器的话,可以用wx.scanCode让已经配好的手机直接扫码,通过临时会话转发方式同步配置。

// 分享配置:把阈值编码进分享参数 function shareConfig() { const config = wx.getStorageSync('sleep_config') const encoded = encodeURIComponent(JSON.stringify(config)) wx.shareAppMessage({ title: '我的睡眠监测校准参数', path: `/pages/sleep/sleep?config=${encoded}` }) }

接收方在onLoad里检测到query.config时,执行JSON.parse(decodeURIComponent(query.config))后写回存储。这样一位测试者校准完参数,其余人扫码即可复用,省去了反复讲解activityCalmactivityWake含义的时间。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 16:57:15

DL/T 645与DL/T 698.45对比:电力抄表协议选型与现场排障指南

先说句大实话&#xff1a;做电力抄表好些年的人&#xff0c;听到 DL/T 645 就像看到老邻居&#xff0c;听到 DL/T 698.45 则是一副“听说过但没深交”的表情。这两套协议说到底都是电表和数据采集系统之间的“对话规则”&#xff0c;但一个是串口时代的“账本式”抄表&#xff…

作者头像 李华
网站建设 2026/9/15 16:51:52

PyTorch+OpenCV实现车牌识别:从两阶段检测到字符分类完整实战

简介&#xff1a;面向高校计算机相关专业学生的初级车牌识别完整项目&#xff0c;基于PyTorch与OpenCV实现&#xff0c;可作为期末大作业、课程设计和毕业设计的参考&#xff0c;也适合初次接触深度学习视觉任务的开发者动手练手。整个zip包共7个文件、25.38MB&#xff0c;其中…

作者头像 李华