webOS 프로젝트

webOS를 활용한 HomeIoT : 수면패턴분석3 - 서비스

하이하정 2021. 11. 27. 13:42

  • 전체 시스템 소개
  • 조명 제어
  • 스마트 가습기
  • 수면 패턴 분석
  • 시스템 연동

 

서비스 개요


1. 서비스 기능

수면 패턴 분석은 사용자의 수면 중 움직임 패턴을 분석해 어플리케이션의 화면에 그래프로 표시하는 기능을 수행합니다.

 

그에 따라 필요한 서비스의 기능은 아래와 같습니다.

1. 서버를 켠다.

2. 켜놓은 서버를 시간이 지나도 꺼지지 않도록 유지한다.

3. 서버에 연결된 클라이언트로부터 사용자의 수면 시작 시간과 특정 시간(2초)마다의 움직임 값을 받아온다.

4. 받아온 움직임 값을 토대로 적절한 계산을 실행해 특정 시간(한 시간)마다의 결과값을 저장한다.

5. 저장된 결과값을 어플리케이션에서 호출할 수 있도록 한다.

 

 

2. 서비스 함수

handleSleepData 받아온 사용자의 움직임 데이터를 처리합니다. 
handleMessage 디바이스 측에서 받아온 센서 값을 출력합니다.
startServer 서버를 여는 서비스입니다.
heartbeat 서버의 열린 상태를 유지하기 위해 구동합니다.
getSleepData 디바이스로부터 센서 값을 받아옵니다.
createToast 화면에 사용자의 상태정보를 토스트 메세지로 표시합니다.

 

서비스 개발


1. handleSleepData

const handleSleepData = (deviceID, value) => {
    let sleepData = sleepDatas[deviceID];
    if(sleepData.isSleeping) {
        if(value <= SLEEP_THRESHOLD) {
            sleepData.isSleeping = false;
            console.log('잠에서 깼습니다.', sleepData);
            createToast(deviceID + " : 잠에서 깼습니다.");
        }
        else {
            sleepData.accumluateCount += 1;
            sleepData.accumulatedValue += Math.abs(value - sleepData.previousValue);
            sleepData.previousValue = value;
            
            if(sleepData.accumluateCount == 5) {
                sleepData.values.push(sleepData.accumulatedValue);
                sleepData.accumluateCount = 0;
                sleepData.accumulatedValue = 0;
                if(sleepData.times.length == 0){
                    let currentTime = (new Date()).getHours();
                    sleepData.times.push(currentTime);
                }
                else {
                    let previousTime = sleepData.times[sleepData.times.length -1];
                    sleepData.times.push(previousTime + 1);
                }
            }
            console.log('데이터 모으는 중입니다.', sleepData);
        }
        return;
    }

    else if(!(sleepData.isSleeping) && value > SLEEP_THRESHOLD) {
        sleepData.isSleeping = true;
        sleepData.accumluateCount = 0;
        sleepData.accumulatedValue = 0;
        sleepData.previousValue = value;
        sleepData.times = [];
        sleepData.values = [];

        console.log('취침모드로 들어갑니다.', sleepData);
        createToast(deviceID + " : 취침모드로 들어갑니다.");
        return;
    } 
};

수면 패턴 분석의 기능을 구현하는 handleSleepData 서비스 함수입니다.

기능 설정은 다음과 같습니다.

 

먼저 사용자의 현재 수면 상태를 확인해 수면 중일 경우 움직임 값이 일정 기준치를 넘으면 잠에서 깬 것으로 판단, 현재 수면 상태를 false로 바꾸고 사용자의 화면에 토스트로 deviceID와 함께 '잠에서 깼습니다' 메세지를 출력합니다.

기준치를 넘지 않은 경우 계속 수면 중인 것으로 판단해 accumulateCount 값에 1을 더하고, accumulatedValue 값에 현재 값에서 이전 값을 뺀 값의 절대값을 축적합니다.

 

accumulateCount은 특정 시간(여기서의 경우 10초)이 지났을 때 그동안 축적된 데이터값을 어플리케이션에서 출력할 수 있도록 저장하기 위해 사용됩니다. 클라이언트(압력 센서) 측에서 2초마다 한 번씩 데이터를 받아오는 것으로 설정되어 있기 때문에 accumulateCount의 값이 5가 되었을 때, 즉 10초가 지났을 때마다 축적된 데이터를 values 리스트에 추가합니다. 한 번 push를 하면 다음 index에 새로운 데이터를 저장하기 위해 accumulateCount값과 accumulatedValue값을 0으로 초기화합니다.

 

또한 어플리케이션으로 보낼 데이터에 움직임값뿐만 아니라 그래프 x축의 시간 데이터도 필요하기 때문에 times 리스트에 값을 저장해야 합니다.

만약 times에 아무 값도 저장되어 있지 않다면, 즉 사용자의 수면이 시작된 후 첫 10초가 지난 후라면 times의 첫 index에 현재 시간을 저장하고, 두 번째 index 부터는 이전 값에 한 시간(10초)을 더해줍니다.

* 여기서는 데모를 위해 10초를 한 시간이라고 가정하고 코드를 구현했습니다.

 

만약 사용자의 수면 상태를 확인했을 때 수면 중이 아니고 value값이 특정 기준치보다 크다면 사용자가 수면 상태에 들어간 것으로 판단해 isSleeping 상태를 true로 바꾸고, 다른 값들을 초기화합니다. 또한 사용자의 화면에 토스트로 '취침모드로 들어갑니다' 메세지를 출력합니다.

 

2. handleMessage

const handleMessage = (message) => {
    // service.call("luna://com.webos.notification/createToast", {message:message}, function(m2) {
    //     console.log(logHeader, "SERVICE_METHOD_CALLED:com.webos.notification/createToast");
    // });
    
    console.log('Recieved: ', message);
    let msg = JSON.parse(message);
    if (msg.msgType == 'sendValue') {
        if(msg.deviceType == 'sleepSensor') {
            handleSleepData(msg.deviceID, msg.value);
        }
    }
};

확인을 위해 디바이스 측에서 받아온 센서 값을 출력해줍니다.

 

3. startServer

var express = require('express');
var app = express();
var expressWs = require('express-ws')(app);
var aWss = expressWs.getWss('/');
const port = 9999;

app.use(function (req, res, next) {
  console.log('middleware');
  req.testing = 'testing';
  return next();
});

app.ws('/', function(ws, req) {
    console.log('Recieved something');
    ws.on('message', handleMessage);
});

const broadCastMessage = (message) => {
    aWss.clients.forEach(function (client) {
        client.send(message);
    });
}

service.register("startServer", function(message) {
    console.log('WebSocketServer on?');
    app.listen(port);
    console.log('WebSocketServer on!');

    // heartbeat 구독
    const sub = service.subscribe(`luna://${pkgInfo.name}/heartbeat`, {subscribe:true});
    const max = 120;
    let count = 0;
    sub.addListener("response", function(msg) {
        console.log(JSON.stringify(msg.payload));
        if (++count >= max) {
            sub.cancel();
            setTimeout(function(){
                // console.log(max+" responses recieved, exiting...");
                process.exit(0);
            }, 1000);
        }
    });

    message.respond({
        returnValue: true,
        Response: "Start Server!"
    });
});

express-ws 패키지를 사용해 web socket을 위한 서버를 열어줍니다.

또한 서버가 꺼지지 않도록 heartbeat를 구독해 계속해서 서버를 유지해줍니다.

서버 코드는 다음의 npm 사이트를 참고해서 작성했습니다: https://www.npmjs.com/package/express-ws

포트 번호는 클라이언트 측과 9999번으로 맞추었습니다.

 

4. heartbeat

// handle subscription requests
const subscriptions = {};
let interval;
let x = 1;
function createInterval() {
    if (interval) {
        return;
    }
    console.log(logHeader, "create_interval");
    console.log("create new interval");
    interval = setInterval(function() {
        sendResponses();
    }, 1000);
}

const heartbeat2 = service.register("heartbeat");
heartbeat2.on("request", function(message) {
    console.log(logHeader, "SERVICE_METHOD_CALLED:/heartbeat/request");
    console.log("heartbeat callback");
    message.respond({event: "beat"});
    if (message.isSubscription) {
        subscriptions[message.uniqueToken] = message;
        if (!interval) {
            createInterval();
        }
    }
});
heartbeat2.on("cancel", function(message) {
    console.log(logHeader, "SERVICE_METHOD_CALLED:/heartbeat/cancel");
    console.log("Canceled " + message.uniqueToken);
    delete subscriptions[message.uniqueToken];
    const keys = Object.keys(subscriptions);
    if (keys.length === 0) {
        console.log("no more subscriptions, canceling interval");
        clearInterval(interval);
        interval = undefined;
    }
});

서버가 꺼지지 않도록 유지하기 위한 heartbeat 서비스입니다.

heartbeat의 자세한 내용은 이전 포스팅인 heartbeat 사용하기를 참고해주시기 바랍니다.

 

5. getSleepData

let sleepDatas = {
    "Sleep00" : {
        isSleeping : false,
        accumulateCount : 0,
        accumulatedValue : 0,
        previousValue : 0,
        times : [],
        values : []
    }
}

// get data
service.register('getSleepData', function(message){
    let deviceID = message.payload.deviceID;
    console.log('getSleepData, Received: ', deviceID);
    
    message.respond({
        returnValue : true,
        data : sleepDatas[deviceID]
    });    
});

클라이언트(압력 센서) 측에서 사용자의 움직임 값을 받아옵니다.

 

5. createToast

function createToast(message) {
    service.call("luna://com.webos.notification/createToast", {message:message}, function(m2) {
        console.log(logHeader, "SERVICE_METHOD_CALLED:com.webos.notification/createToast");
    });
}

토스트 메세지를 출력하기 위한 서비스 코드입니다.

자세한 내용은 이전 포스팅인 Luna Service API 이용하기를 참고하시기 바랍니다.

 

전체 서비스 코드


서비스의 전체 코드는 다음 더보기를 통해 확인할 수 있습니다.

더보기
const pkgInfo = require('./package.json');
const Service = require('webos-service');

const service = new Service(pkgInfo.name); // Create service by service name on package.json
const logHeader = "[" + pkgInfo.name + "]";

var express = require('express');
var app = express();
var expressWs = require('express-ws')(app);
var aWss = expressWs.getWss('/');
const port = 9999;

// ***************************** Websocket Server *****************************
app.use(function (req, res, next) {
  console.log('middleware');
  req.testing = 'testing';
  return next();
});

app.ws('/', function(ws, req) {
    console.log('Recieved something');
    ws.on('message', handleMessage);
});

const handleMessage = (message) => {    
    console.log('Recieved: ', message);
    let msg = JSON.parse(message);
    if (msg.msgType == 'sendValue') {
        if(msg.deviceType == 'sleepSensor') {
            handleSleepData(msg.deviceID, msg.value);
        }
    }
};

service.register("startServer", function(message) {
    console.log('WebSocketServer on?');
    app.listen(port);
    console.log('WebSocketServer on!');

    // heartbeat 구독
    const sub = service.subscribe(`luna://${pkgInfo.name}/heartbeat`, {subscribe:true});
    // const max = 120;
    // let count = 0;
    sub.addListener("response", function(msg) {
        console.log(JSON.stringify(msg.payload));
        // if (++count >= max) {
        //     sub.cancel();
        //     setTimeout(function(){
        //         // console.log(max+" responses recieved, exiting...");
        //         process.exit(0);
        //     }, 1000);
        // }
    });

    message.respond({
        returnValue: true,
        Response: "Start Server!"
    });
});

// ***************************** Heartbeat *****************************
// send responses to each subscribed client
function sendResponses() {
    // console.log(logHeader, "send_response");
    // console.log("Sending responses, subscription count=" + Object.keys(subscriptions).length);
    for (const i in subscriptions) {
        if (Object.prototype.hasOwnProperty.call(subscriptions, i)) {
            const s = subscriptions[i];
            s.respond({
                returnValue: true,
                event: "beat " + x
            });
        }
    }
    x++;
}

// handle subscription requests
const subscriptions = {};
let interval;
let x = 1;
function createInterval() {
    if (interval) {
        return;
    }
    console.log(logHeader, "create_interval");
    console.log("create new interval");
    interval = setInterval(function() {
        sendResponses();
    }, 1000);
}

// ***************************** Create Toast *****************************
function createToast(message) {
    service.call("luna://com.webos.notification/createToast", {message:message}, function(m2) {
        console.log(logHeader, "SERVICE_METHOD_CALLED:com.webos.notification/createToast");
    });
}

const heartbeat2 = service.register("heartbeat");
heartbeat2.on("request", function(message) {
    console.log(logHeader, "SERVICE_METHOD_CALLED:/heartbeat/request");
    console.log("heartbeat callback");
    message.respond({event: "beat"});
    if (message.isSubscription) {
        subscriptions[message.uniqueToken] = message;
        if (!interval) {
            createInterval();
        }
    }
});
heartbeat2.on("cancel", function(message) {
    console.log(logHeader, "SERVICE_METHOD_CALLED:/heartbeat/cancel");
    console.log("Canceled " + message.uniqueToken);
    delete subscriptions[message.uniqueToken];
    const keys = Object.keys(subscriptions);
    if (keys.length === 0) {
        console.log("no more subscriptions, canceling interval");
        clearInterval(interval);
        interval = undefined;
    }
});


let sleepDatas = {
    "Sleep00" : {
        isSleeping : false,
        accumluateCount : 0,
        accumulatedValue : 0,
        previousValue : 0,
        times : [],
        values : []
    }
}

const SLEEP_THRESHOLD = 100;
const handleSleepData = (deviceID, value) => {
    let sleepData = sleepDatas[deviceID];
    if(sleepData.isSleeping) {
        if(value <= SLEEP_THRESHOLD) {
            sleepData.isSleeping = false;
            console.log('잠에서 깼습니다.', sleepData);
            createToast(deviceID + " : 잠에서 깼습니다.");
        }
        else {
            sleepData.accumluateCount += 1;
            sleepData.accumulatedValue += Math.abs(value - sleepData.previousValue);
            sleepData.previousValue = value;
            
            if(sleepData.accumluateCount == 5) {
                sleepData.values.push(sleepData.accumulatedValue);
                sleepData.accumluateCount = 0;
                sleepData.accumulatedValue = 0;
                if(sleepData.times.length == 0){
                    let currentTime = (new Date()).getHours();
                    sleepData.times.push(currentTime);
                }
                else {
                    let previousTime = sleepData.times[sleepData.times.length -1];
                    sleepData.times.push(previousTime + 1);
                }
            }
            console.log('데이터 모으는 중입니다.', sleepData);
        }
        return;
    }

    else if(!(sleepData.isSleeping) && value > SLEEP_THRESHOLD) {
        sleepData.isSleeping = true;
        sleepData.accumluateCount = 0;
        sleepData.accumulatedValue = 0;
        sleepData.previousValue = value;
        sleepData.times = [];
        sleepData.values = [];

        console.log('취침모드로 들어갑니다.', sleepData);
        createToast(deviceID + " : 취침모드로 들어갑니다.");
        return;
    } 
};

// get data
service.register('getSleepData', function(message){
    let deviceID = message.payload.deviceID;
    console.log('getSleepData, Received: ', deviceID);
    
    message.respond({
        returnValue : true,
        data : sleepDatas[deviceID]
    });    
});