Posted on

DIY睡眠监测器,从此抛弃小米手环

对于睡眠的监测,我使用的第一款产品是小米手环,每晚佩戴手环睡觉,第二天打开APP可以同步出前一晚的睡眠数据,看到深睡和浅睡时长等数据。

就原理而言,小米手环使用了三轴加速度计,在实现计步的情况下也可以完成睡眠的监测,所以从原理上讲,睡眠的监测和计步是一样的,可能从某种程度上来说睡眠比计步更加简单。

先看一下完成的睡眠数据效果图:

睡眠详情

需要提醒,本文只是介绍一种思路,提供一种方法,供创客玩自己DIY,传感器和硬件的选择并不代表产品的实际情况!

需要提醒,本文只是介绍一种思路,提供一种方法,供创客玩自己DIY,传感器和硬件的选择并不代表产品的实际情况!

需要提醒,本文只是介绍一种思路,提供一种方法,供创客玩自己DIY,传感器和硬件的选择并不代表产品的实际情况!

传感器的选择

要选择传感器,就需要明确一下睡眠的场景。首先,我们并不喜欢佩戴手环或者其他设备睡觉,所以现在市面上的睡眠监测器都不是佩戴的,以最近米加出的睡眠监测器来讲,它是允许用户将设备放在床头或压在枕头下。值得说,这种使用方式确实不错,不用佩戴,当然会睡的舒服。

提取一下使用场景:放在床头,或压在枕下。

放在床头,可以使用三轴陀螺仪,也就是加速度计进行传感,当我们躺下的时候,势必会对床产生压力,压力在产生形变,是床凹下去,这时放在床头的传感器就可以检测到一个以Z轴为主的数据变化,这个变化也就一定程度上说明的睡眠情况。三轴值变化越大,说明床的形变变化越剧烈,也就是人处于活动状态。所以通过三轴加速度计变可以近似的得到深睡、浅睡和活动。

压在枕下,可以使用压力传感器,原理和使用三轴加速度计同理。当人处于熟睡状态时,头部的压力是均匀的,波动是最小的,这时可以判断为深睡。浅睡状态下,人的头部对传感器的压力会存在一定范围内波动,进而可以判断浅睡。

整理方案

因为博主手头没有压力传感器,也没有三轴陀螺仪,所以我使用了三轴磁场传感器,也就是常说的电子指南针,通过对该传感器的读值,可以获取传感器当前姿态对磁场x, y, z面的夹角。因为地磁面是确定的,所以三轴的夹角波动很小的时候就可以判断为是深睡,波动在一定范围内则判断为浅睡,剧烈的波动则视为活动。

这个传感器方案或许不是最好的方案,但是一定程度上可以帮助我们了解如何对睡眠进行测量,同时,本文主要用于介绍思路,如果后续有了压力传感器和三轴加速度计,我也会毫不犹豫的用它们。

硬件选择

HMC5883L三轴磁场传感器 + nodemcu开发板

HMC5883L三轴磁场传感器 + nodemcu开发板

之所以选择nodemcu,除了我对它比较熟悉外,更重要的是它自带WiFi,支持arduino模式开发,并且有良好的开源支持。

编译固件

编写代码之前,需要先明确一下设备需要具备的功能。博主设计了它具备的功能如下:

  • 支持微信Airkiss配网
  • 支持MQTT消息收发
  • 支持发送HTTP请求
  • 支持HMC5883L数据的读取
  • 支持充电,且能实现电量的监控

以上是博主设计的需要具备的基本功能,基于这些功能,开始写代码。首先,需要编译以上用到的功能到NodeMcu的固件,关于固件的编译,请看《NodeMcu固件编译方法》

app/include/user_model.h中选择以上需要的功能去除注释,电量监测需要去掉ADC的注释,这里列出所有需要开启的模块,未列出的默认为关闭:

#define LUA_USE_MODULES_CJSON
#define LUA_USE_MODULES_FILE
#define LUA_USE_MODULES_GPIO
#define LUA_USE_MODULES_HMC5883L
#define LUA_USE_MODULES_HTTP
#define LUA_USE_MODULES_MQTT
#define LUA_USE_MODULES_NODE
#define LUA_USE_MODULES_TMR
#define LUA_USE_MODULES_WIFI

以上为需要开启的模块,其他模块可关闭以减少后期编译出的固件体积。另外,请不要编译为浮点型,因为NodeMcu读取的HMC5883L数据精确到1°,所以可以直接编译成整型版本,这样也可以减少11k的固件体积。编译整形,请打开app/include/user_config.h,并找到#define LUA_NUMBER_INTEGRAL将其取消注释。

// 编译为整形,取消注释
#define LUA_NUMBER_INTEGRAL

修改完成后,就可以使用make进行编译了,编译完成以后,会在bin目录下生成两个.bin文件,分别为:0x00000.bin0x10000.bin

烧写固件

将这两个固件烧写到NodeMcu开发板就可以正式开始编写lua脚本了,如果使用Windows系统下的NodeMCU Flasher进行烧写,请如图进行配置:

Windows下固件烧写配置方法

如果使用esptool进行烧写,请使用如下命令:

PATH_TO_ESPTOOL/esptool.py --port YOUR_USB_PORT write_flash -fm dio -fs 32m -ff 40m 0x00000 0x00000.bin 0x10000 0x10000

其中PATH_TO_ESPTOOL/esptool.py是esptool的执行目录,YOUR_USB_PORT是端口号或端口地址,另外请着重注意后面四个值,要分别指定烧写的地址和烧写的文件

硬件端代码

以下代码使用lua语言开发。

WIFI的连接以及Airkiss设置

首先,配置NodeMcu联网,可以写一个WIFI_CONFIG的配置文件,NodeMcu上电初始化完成以后则从WIFI_CONFIG中读取配置,读取到配置以后使用配置好的SSIDPASS进行联网,如果存在配置并且可以连接上,则直接连接,否则开启Airkiss配网,这时可以使用微信的Arikiss功能进行联网设置,Airkiss连接成功以后,将新的WIFI配置项写入文件,方便下次直接读取。流程整理OK,现在开始写代码。

function app_init()
-- 连接WiFi并处理WiFi配置
print("Linking to wifi...")
-- WiFi配置
if (file.open("wifi_config", "r")) then
    local config = file.readline()
    file.close()
    -- print(config)
    -- 检查json文件的有效性
    local ok, WIFI_CONF = pcall(cjson.decode, config)
    -- 检查是否存在WiFi的目标配置项
    if ok then
        -- 不存在则airkiss获取
        if (not WIFI_CONF.WIFI_SSID or not WIFI_CONF.WIFI_PASS) then
            set_airkiss_config()
        else
            -- 存在WiFi配置项,则连接WiFi
            wifi.setmode(wifi.STATION)
            wifi.sta.config(WIFI_CONF.WIFI_SSID, WIFI_CONF.WIFI_PASS)
            wifi.sta.connect()
            -- 连接记次
            local connect_count = 0
            tmr.alarm(led_wifi_get_ip_timer, 1000, 1, function()
                if wifi.sta.getip() == nil then
                    WIFI_STATE = "Connecting..."
                    print("Connecting...")
                    -- 20次尝试失败后,重新开启airkiss模式进行WiFi配置
                    connect_count = connect_count + 1
                    if (connect_count > 20) then
                        set_airkiss_config()
                    end
                else
                    print(wifi.sta.getip())
                    -- 开始业务
                    --
                                            -- 业务部分的函数可以在这里进行调用
                                            --
                end
                -- 
            end)
            -- 
        end
    -- 如果不存在WiFi的相应配置项,则开启airkiss配置模式
    else
        set_airkiss_config()
    end
    -- 
else
    -- airkiss
    set_airkiss_config()
end
end

-- 通过airkiss设置WiFi信息
function set_airkiss_config()
AIR_KISS = 1
print("waite for airkiss to link wifi...")
-- 开启WiFi配置
wifi.setmode(wifi.STATION)
-- 开启airkiss模式
wifi.startsmart(1, function (ssid, password)
    -- 删除文件
    ok = pcall(file.remove, 'wifi_config')
    if ok then
        -- 新建配置文件
        file.open("wifi_config", "w+")
        file.writeline('{"WIFI_SSID":"' .. ssid .. '","WIFI_PASS":"' .. password .. '"}')
        file.close()
        print(string.format("Success. SSID:%s ; PASSWORD:%s", ssid, password))
        wifi.stopsmart()
        node.restart()
    else
        print('remove old config error')
    end
    -- 
end)
-- 
end

需要注意,以上函数是写在init.lua文件中的,因为NodeMcu上电以后默认加载init.lua。写完以后,可以将代码上传到NodeMcu,上传代码的方法很多,这里推荐使用esplorer,它的功能强大,使用方便,因为esplorer是java编写的,所以使用这个工具之前,请确认自己的电脑上已经安装好了java环境。

esplorer软件界面

没有这个可以点击这里进行下载Download ESPlorer.zip (v 0.2.0-rc6),elporer的GitHub地址在这里ESPlorer

上传以后,重启NodeMcu就可以看到从串口打印出来的运行信息,如果不出意外,Airkiss或WiFi都会顺利连接。有时候我们可能不想盯着屏幕,希望通过一个LED指示出现在的状态,其实也很简单,写两个函数来操作LED,当WiFi联网的时候慢闪,Airkiss进行配网的时候快闪,这样在脱离电脑的情况下,可以通过LED的闪烁情况来判断目前的状态,两个函数如下:

-- airkiss模式LED快闪
function set_airkiss_led_model()
-- 关闭所有定时器
clear_timer_interval()
-- 设置airkiss模式的LED闪烁定时器
tmr.alarm(airkiss_config_led_timer, 50, 1, function ()
    -- 闪烁LED
    if (LED_ON == 0) then
        gpio.write(led_pin, gpio.LOW)
        LED_ON = 1
    else
        gpio.write(led_pin, gpio.HIGH)
        LED_ON = 0
    end
end)
end

-- WiFi连接过程中模式下,LED慢闪
function set_wifi_connect_led_model()
-- 关闭所有定时器
clear_timer_interval()
-- 设置WiFi连接模式的定时器
tmr.alarm(wifi_connect_timer, 100, 1, function ()
    -- 闪烁LED
    if (LED_ON == 0) then
        gpio.write(led_pin, gpio.LOW)
        LED_ON = 1
    else
        gpio.write(led_pin, gpio.HIGH)
        LED_ON = 0
    end
end)
end

有了上面两个函数,就可以在相应的工作状态下设置LED的闪烁模式,这才是比较符合人类的思维模式。上面的代码中有一个clear_timer_interval()方法,主要是用来清除定时器,类似于JavaScript里面的clearIntervalairkiss_config_led_timerwifi_connect_timer是两个定时器ID,在NodeMcu中,允许设定7个独立的定时器,ID号从0~6,所以有时会出现定时器复用的情况,在这种情况下,请先保证清除了定时器,然后再赋予它新的功能。

读取三轴磁场传感器的值

在官方最新的文档中,已经集成对了HMC5883L的支持,所以读取传感器的值非常方便:

-- 初始化传感器
hmc5883l.init(1, 2)
-- 读取三轴的角度值
local x, y, z = hmc5883l.read()

引脚关联请注意,hmc5883l.init(1, 2)的原型为hmc5883l.init(sda, scl),这里的sdascl引脚可以自己设定,这里使用了1,2两个数字引脚。

xyz分别读到三轴的值,是与磁场平面的三轴夹角值。

体动数与体动指数

体动数在运动监测相关的开发中是一个专有名字,通过体动数可以检测到人体的运动情况,通常不同的体动数对应的不同的运动状态。这里我们不使用体动数,而是给它启一个别名,叫体动指数

为什么是体动指数而不是体动数?主要是因为,传感器读到的是一个角度值,从角度值计算体动的误差比较大,而且本文所编译的固件是整型的,又会放大这个误差,所以使用体动指数这个杜撰的名字。

怎么计算体动指数?其实也很简单,首先,确定传感器在静止状态下的的三轴读数,通过大量采集可以得到一个静止状态下的波动值。在我的传感器中,静止状态下三轴的波动值的和不大于3,也就是说,静止状态下,每一个轴的角度变化小于1°。

下来需要设计一下采集的模式,我这里直接设定好了,每200毫秒采集一次,每次采集都将三轴波动的和进行累加,每一分钟再打一个点,打完点以后将累加的和清零。代码如下:

-- 获取传感器的波动和
function get_body_sleep()
    READ_INDEX = 0
    -- 求和
    SUM_X = 0
    SUM_Y = 0
    SUM_Z = 0
    SUM = 0
    -- 上次抖动
    local x, y, z = hmc5883l.read()
    PX = x
    PY = y
    PZ = z
    MOVE = 0
    -- 定时获取
    tmr.alarm(6, 200, 1, function ()
            local x, y, z = hmc5883l.read()
            READ_INDEX = READ_INDEX + 1
            -- 累计
            SUM_X = SUM_X + math.abs(x - PX)
            SUM_Y = SUM_Y + math.abs(y - PY)
            SUM_Z = SUM_Z + math.abs(z - PZ)
            -- 本次采集的累计体动和
            SUM = SUM_X + SUM_Y + SUM_Z
            -- 重置前一次读数
            PX = x
            PY = y
            PZ = z
            -- 一分钟为一组
            if (READ_INDEX % 300 == 0) then
                    MOVE = SUM
                    print(MOVE)
                    if POST_MOVE_STATE == 1 then
                        -- 上报体动数
                            post_move(MOVE)
                    end
                    -- 清零计数
                    SUM_X = 0
                    SUM_Y = 0
                    SUM_Z = 0
                    SUM = 0
                    READ_INDEX = 0
            end
            -- 
    end)
end

这样可以每分钟读到一个整形值,根据在静止状态下的测试,可以大致得到结论,这个值的范围应该是大于200的。没错,我读到的值确实是大于两百的,静止状态下基本集中的300上下。这么做有什么好处?这么做可以把体动数放大百倍,通过模式判断的时候我们判断的是百位数,而不是个位数,而且后期进行校准的时候,放大的数值更有利于校准。

上报体动指数

细心的同学一定发现了上面的函数中有一个post_move()方法,没错,这个就是上报数值的方法。使用这个方法前,首先来封装两个适用于Smartline接口的方法,一个用于POST,一个用于GET

-- Smartline的GET请求
function http_get(api_address, param_json, callback)
    -- 添加请求参数
    if param_json then
            api_address = api_address .. "?"
            for key, value in pairs(param_json) do
                    -- 组装请求参数
                    api_address = api_address .. key .. "=" .. value .. "&"
            end
            -- 删除最后一个&符号
            api_address = string.sub(api_address, 0, string.len(api_address) - 1)
    end
    -- 发送请求
    http.get(api_address, http_get_headers, function(code, data)
            if (code ~= 200) then
                    print(code, data, "HTTP request failed")
            elseif code == 200 and callback then
                    callback(code, data)
            end
            -- 
    end)
    -- 
end

-- Smartline的POST请求
function http_post(api_address, param_json, callback)
    -- 添加请求参数
    local param_str = ""
    if param_json then
            for key, value in pairs(param_json) do
                    -- 组装请求参数字符串
                    param_str = param_str .. key .. "=" .. value .. "&"
            end
            -- 删除最后一个&符号
            param_str = string.sub(param_str, 0, string.len(param_str) - 1) 
    end
    -- 发送请求
    http.post(api_address, http_post_headers, param_str, function(code, data)
            if (code ~= 200) then
                    print("HTTP request failed")
            elseif code == 200 and callback then
                    callback(code, data)
            end
    end)
end

上面两个方法可以实现Smartline的网络请求,其中http_post_headers就是请求头的设置,具体的参数请参考Smartline数据接口文档callback是请求的回调函数,在请求成功以后,会通过callback来指定接下来的操作。

有了以上两个方法,现在来编写post_move()函数。

-- 上报传感器的值
function post_move(move_times)
    local params = {}
    params['sn'] = SN
    params['data_id'] = BODY_MOVE_ID
    params['value_json'] = '{"value":'..move_times..'}'
    -- 上报
    http_post(API_LIST['uploadDataPoint']['api_address'], params, function (code, res)
            print(code, res)
            -- 
    end)
end

其中的SN是你所绑定的设备唯一标识,BODY_MOVE_ID是此类设备的数据点标记位,即 这个数据要上报到哪个传感器模型上面value_json是组装好的数据字符串,有关这部分数据上报,请看Smartline数据接口文档中有关上报传感器数据的接口描述。API_LIST['uploadDataPoint']['api_address']是配置的接口名对应的接口地址,存放在api_config.lua中,形如:

-- 设备的接口列表
API_LIST = {}
API_HOST = "http://api.smartline.cc"

-- 上报数据点
API_LIST['uploadDataPoint'] = {}
API_LIST['uploadDataPoint']['api_address'] = API_HOST .. "/sensor/upload/point"
API_LIST['uploadDataPoint']['method'] = "POST"
API_LIST['uploadDataPoint']['params_needed'] = {"data_id", "sn", "value_json"}
API_LIST['uploadDataPoint']['params_extra'] = {}

-- 获取系统更新
API_LIST['getFirmwareUpdate'] = {}
API_LIST['getFirmwareUpdate']['api_address'] = API_HOST .. "/device/firmware/getlast"
API_LIST['getFirmwareUpdate']['method'] = "GET"
API_LIST['getFirmwareUpdate']['params_needed'] = {"sn"}
API_LIST['getFirmwareUpdate']['params_extra'] = {}

-- 设备保活
API_LIST['heartbeat'] = {}
API_LIST['heartbeat']['api_address'] = API_HOST .. "/application/heart/pong"
API_LIST['heartbeat']['method'] = "POST"
API_LIST['heartbeat']['params_needed'] = {"sn"}
API_LIST['heartbeat']['params_extra'] = {}

这个文件不是必须的,你也可以直接使用api地址代替。

心跳及设备保活

设备保活的意义在于,通过前端的界面可以看到设备目前是否在线。设备保活接口有两种,一种是通过HTTP接口去触发保活,另外一种,如果连接了MQTT服务器,MQTT会自动监测设备是否在线,实现心跳保活。

这里我们以HTTP触发心跳保活为例,代码很简单:

-- 开始保持心跳
function heartbeat_start()
    tmr.alarm(heartbeat_timer_id, 58 * 1000, 1, function ()
        post_pong()
    end)
end

-- 发送心跳
function post_pong()
    local params = {}
    params['sn'] = SN
    http_post(API_LIST['heartbeat']['api_address'], params, function (code, res)
        print(code, res)
    end)
end

可以看到,心跳保活其实是一个POST请求,由定时器控制每58秒触发一次,因为设备在云端设置的保活时间为60秒,如果60后没有收到保活的数据,则视为设备离线。

** 注意!这里的保活只是表意上的保活,是为了提供的界面端的显示,并不代码设备是不是真的在线!**
** 注意!这里的保活只是表意上的保活,是为了提供的界面端的显示,并不代码设备是不是真的在线!**
** 注意!这里的保活只是表意上的保活,是为了提供的界面端的显示,并不代码设备是不是真的在线!**

如果使用MQTT连接服务器,可以省去POST保活的代码。

MQTT实现指令接收

在国内最早做物联网平台的是Yeelink,那时他们只接收HTTP的接口,要同步一个在云端的开关状态,需要不停的使用HTTP轮询,后来Yeelink限制了每两个HTTP请求直接的时间间隔为2s,没两次数据上报的时间间隔为12秒,于是如果想通过轮询获取云端的开关状态,至少需要2秒的延迟。

很明显,HTTP轮询的方式很不友好,很多请求是无用的,并且也带来了服务器的压力,更重要的,是它没有办法及时的推送状态信息。

MQTT的出现简直是曙光,MQTT的socket连接只使用了很小的协议包,更适合嵌入式硬件进行通信。

这里需要介绍一些Smartline的接口构成。按照统一接口的架构,搜索上行的数据全部使用HTTP接口,这样可以大范围兼容Web端,移动APP,嵌入式硬件和服务端脚本。下行的数据使用MQTT进行推送,主要是为了兼容嵌入式硬件,除此之外,还支持Websocket协议的下行数据,这样可以解决在Web端的消息实时推送问题。

以上了解清楚,开始写代码:

-- 连接MQTT服务器
function mqtt_init()
    -- 连接
    mq = mqtt.Client(SN, 120, MQTT_NAME, MQTT_PASS)
    device_topic = "device/" .. SN

    mq:connect(MQTT_HOST, MQTT_PORT, 0, function(con)

            -- 关闭重连定时器
            tmr.stop(reconnect_mqtt_timer)

            -- 订阅消息,qos = 0
            mq:subscribe(device_topic, 0, function(con)
                    print("[mqtt] subscribe success")
            end)

            -- mqtt连接事件
            mq:on("connect", function(con)
                    print("[mqtt] connecting")
            end)

            -- mqtt 接收到消息事件
            mq:on("message", function(con, topic, data) 
                    print(topic .. ":" )
                    if data ~= nil then
                            print(data)
                            deal_mqtt_message(data)
                    end
            end)

            -- mqtt 断开连接事件
            mq:on("offline", function(con)
                    print ("mqtt reconnecting")
                    -- 重新连接
                    local rec_timer_count = 0
                    -- 五秒钟后重新连接mqtt服务
                    tmr.alarm(reconnect_mqtt_timer, 5000, 1, function ()
                            print('.')
                            -- 重连
                            mqtt_init()
                    end)
                    -- 
            end)
            -- 
    end)
    -- 
end

因为NodeMcu的很多模块是基于事件的,所以这里很容易判断出连接上或已断开。Smartline的MQTT是需要鉴权的,默认的用户名为设备的SN,密码又服务器端自动分配生成,通过Smartline的接口可以获取到服务器端分配的SN和MQTT密码。

MQTT信息示例

上图便是一个设备实例的信息,里面有MQTT的连接信息。此界面只是基于Smartline接口开发的私人后台,如需获取MQTT或设备实例信息,请参照Smartline的接口文档进行操作。

至此,硬件端的框架已经基本完成,主要代码也已经编写OK,没有补充的代码,请自行补充。

还是要说一句,本文提供的是思路和方法,不提供完整代码,但是文中所写的代码其实已经包含了80%以上,如果真的理解了文中所讲,那么剩下的20%代码你肯定可以自己完成

收集睡眠数据

补充完代码并验证无误以后,便可以在晚上讲设备压在枕下,或防止在离你不远的枕边,确保你的活动可以影响到传感器的读值就行,不需要很大,只要有变化,就可以监测到,博主是直接放到枕头下的。

第二天醒来以后,可以关闭设备,然后来处理获取到的数据,下面我将讲解如何处理获取到的数据。

Web 端代码

下面的代码将使用JavaScript进行编写。

睡眠数据的处理

添加一条提示,很重要!

睡眠数据的处理与分析实则是业务层的东西,与数据接口不在一个层面,为了尽快实现效果,这里的业务层用JavaScript在Web端完成了,如果是商用或者为了正规,请在服务器端编写业务层的程序

装逼完了,开始写代码。首先,使用Smartline的JSSDK来初始化业务以便获取可以使用的接口列表:

var params = {
    "appSecret"     :   "b43fa70cc5f**********************",
    "apiKey"        :   "ed71d70cc5f**********************",
    "developerId"   :   245325
}

// 获取接口列表
smartline(params, $, function (smart) { // $指的是传入JQuery对象;Dom7则表示传入的为Framework7对象
    // 接口初始化完成,将smartline对象赋值到全局
    smartObj = smart;
    // 获取绑定的设备列表
    getUserBindDevices(user_id);
});

然后是HTML结果,这里博主使用了Framework7框架来进行界面搭建。

<div class="pages navbar-through toolbar-through">
<div data-page="device-sleep" class="page toolbar-fixed device-sleep">
    <div class="navbar device-sleep-navbar">
        <div class="navbar-inner">
            <div class="left"><a href="#" data-picker=".picker-color" class="back link icon-only close-picker"><i class="ti-arrow-left"></i></a></div>
            <div id="device-name" class="center"></div>
            <div class="right">
                <a href="#" onclick="show_sleep_menu()" class="link icon-only"><i class="icon icon-bars"></i></a>
            </div>
        </div>
    </div>
    <div class="sleep-menu">
        <div><a href="#" class="sleep-state-text" onclick="set_sleep_state(this)"></a></div>
        <div class="line"><a href="#" onclick="get_sleep_info()">同步数据</a></div>
    </div>
    <!-- 睡眠记录图表 -->
    <div class="device-sleep-chart" onclick="hidden_sleep_menu()">
        <!-- 睡眠情况加载在这里 -->
    </div>
    <!-- 波动背景 -->
    <div class="device-sleep-ge">
        <img src="img/bg.png" />
    </div>
    <div class="device-sleep-info" onclick="hidden_sleep_menu()">
        <div class="sleep-item">
            <img src="img/sleep/sleeptime.png">
            <h3 class="device-sleep-time">--</h3>
            <p>入睡时间</p>
        </div>
        <div class="sleep-item">
            <img src="img/sleep/wake_up_time.png">
            <h3 class="device-wakeup-time">--</h3>
            <p>醒来时间</p>
        </div>
        <div class="sleep-item">
            <img src="img/sleep/time.png">
            <h3 class="device-time">--</h3>
            <p>睡眠时长</p>
        </div>
        <div class="clear"></div>
        <div class="sleep-item">
            <img src="img/sleep/deep_sleep.png">
            <h3 class="device-deep-sleep-time">--</h3>
            <p>深睡时长</p>
        </div>
        <div class="sleep-item">
            <img src="img/sleep/light_sleep.png">
            <h3 class="device-shallow-sleep-time">--</h3>
            <p>浅睡时长</p>
        </div>
        <div class="sleep-item">
            <img src="img/sleep/fine.png">
            <h3 class="device-sleep-result">--</h3>
            <p>睡眠质量</p>
        </div>
        <div class="clear"></div>
    </div>
</div>
</div>

下来获取睡眠数据。

// 拉取睡眠数据
function get_sleep_data (start_time) {
    // 参数
    var params = {
        sn: currentDevice.sn,
        data_id: 25448,
        timestamp: start_time
    };
    // 请求接口
    smart_qurey("getHistoryAfter", params, function (res) {
        // console.log(res);
        if (res.code == 0 && res.data.length > 0) {
            var sleepData = res.data;
            // 处理睡眠情况
            deal_sleep_data(sleepData);
        }
        else return false;
    });
}

其中的data_id: 25448是睡眠监测器这个设备的数据上报点,所以要从这里获取,currentDevice.sn是当前设备实例的唯一SN,timestamp: start_time表明从哪个时间点开始获取,因为是昨晚的睡眠,所以从昨晚的某个时间戳开始获取。deal_sleep_data(sleepData)用于处理睡眠数据。下来来看下睡眠情况的判断:

// 处理睡眠数据
function deal_sleep_data (data) {
    var dispArray = [];
    // 深度数组
    var deepArray = new Array(), shallowArray = new Array(), wakeupArray = new Array();
    // 临时
    var deepSleep = [], shallowSleep = [], wakeup = [], startSleep = [];
    var endTime = 0;
    // 计数
    var deepIndex = 0, shallowIndex = 0, wakeupIndex = 0;
    // 遍历睡眠数据获取深度睡眠片段
    data.forEach( function(item, index) {
        var value = item.value_json.value;
        // 深睡
        if (value <= 400) {
            deepSleep.push({
                start_time : item.create_time,
                value: value,
                index: index
            });
            deepIndex += 1;
        }
        else{
            // 连续10分钟以上满足,则计入深睡
            if (deepIndex >= 10) {
                dispArray.push({
                    target: "deepSleep",
                    startIndex: deepSleep[0].index,
                    endIndex: deepSleep[deepSleep.length - 1].index,
                    length: deepSleep.length,
                    start: deepSleep[0].start_time,
                    end: deepSleep[deepSleep.length - 1].start_time
                });
            }
            // 计数清零
            deepIndex = 0;
            // 清空深睡记录
            deepSleep.splice(0, deepSleep.length);
        }

        // 活动
        if (value > 500) {
            // 浅睡记录
            wakeup.push({
                start_time : item.create_time,
                value: value,
                index: index
            });
            wakeupIndex += 1;
        }
        // 醒来
        else {
            // 连续3分钟高体动数,记为活动
            if (wakeupIndex >= 3) {
                dispArray.push({
                    target: "wakeup",
                    startIndex: wakeup[0].index,
                    endIndex: wakeup[wakeup.length - 1].index,
                    length: wakeup.length,
                    start: wakeup[0].start_time,
                    end: wakeup[wakeup.length - 1].start_time
                });
            }
            // 计数清零
            wakeupIndex = 0;
            // 清空深睡记录
            wakeup.splice(0, wakeup.length);
        }
    });
}

这里的睡眠判断原理很简单,默认全部为浅睡,然后从拉取的睡眠数据中获取深睡的片段,然后再获取活动的片段,剩下的记为浅睡片段。下面就将切好的数据片段绘制成柱形图:

// 显示睡觉信息
function display_sleep_data (dispArray, dataLength, timeDuration) {
    if (dispArray.length == 0) return false;
    // 绘图
    var innerHtml = "";
    var deepLong = 0;
    var shallowLong = 0;
    var wakeupLong = 0;
    // 遍历
    dispArray.forEach( function(item, index) {
        var left = 0;
        var width = 0;
        // 计算宽
        width = item.length / dataLength * WIDTH;
        left = item.startIndex / dataLength * WIDTH;
        // 深睡
        if (item.target == "deepSleep") {
            innerHtml += '<div class="animated fadeIn device-sleep-pice ' + "device-sleep-chart-deep" + '" style="width:' + width + 'px;left:' + left + 'px;"></div>'; 
            deepLong += item.length;
        }
        // 活动
        if (item.target == "wakeup") {
            innerHtml += '<div class="animated fadeIn device-sleep-pice ' + "device-sleep-chart-wakeup" + '" style="width:' + width + 'px;left:' + left + 'px;"></div>'; 
            wakeupLong += item.length;
        }
    });
    // 添加到视图
    $('.device-sleep-chart').html(innerHtml);
    // 深睡时长
    $('.device-deep-sleep-time').html(format_time_long(deepLong * 60));
    // 浅睡时长
    $('.device-shallow-sleep-time').html(format_time_long(timeDuration - deepLong * 60 - wakeupLong * 60));
}

柱形图是使用了device-sleep-pice的div,所以需要设置一下CSS样式:

.device-sleep-pice {
    position: absolute;
    height: 60%;
    top: 0;
}

这样就可以通过处理好的数据来计算device-sleep-pice的位置,以及宽度,而高度则使用60%,保持与浅睡同高。UI界面也进行了一番设计,使其看起来更加美观,最终的效果如下:

睡眠监测最终效果图

还是那句话,本文只提供思路和方法,具体的硬件和传感器选择请根据使用场景确定,希望本文对你有所帮助。

发表评论

电子邮件地址不会被公开。 必填项已用*标注