webOS Article/4. webOS 활용하기

다른 어플 실행 중에 내가 만든 서비스로 모니터링하기

문윤미 2021. 9. 12. 10:30

이 포스팅은 webOS OSE 개발자 사이트 : 를 참고하여 작성되었습니다.  

  • App 개발
  • Service 개발 
  • 실행 결과

 

WebOS에서 다른 어플리케이션을 실행 하는 도중(예: youtube 등)  백그라운드에서 버튼이 눌렸는지 주기적으로 모니터링하여 눌렸으면 toast를 띄우는 예제를 구성해 보았습니다. 백그라운드에서 원하는 기능이 동작하도록 웹 서비스로 개발을 진행하였으며, 스위치는 라즈베리파이 4 GPIO를 이용하여 연결해 주었습니다.  webOS OSE에서는 서비스를 단독으로 만들어서 설치하는 것이 안되므로 web application과 함께 만들어 설치를 진행하였습니다. 

 

App 개발


1. json 코드

{
  "id": "com.cosmos.team4.app",
  "version": "1.0.0",
  "vendor": "My Company",
  "type": "web",
  "main": "index.html",
  "title": "Team 4 Service Demo",
  "icon": "icon.png",
  "requiredPermissions": [
    "time.query",
    "activity.operation",
    "notification.operation",
    "peripheralmanager.gpio.operation"
  ]
}

id를 본인이 하고싶은대로 바꾸어줍니다. 그리고 requiredPermissions에는 아래 첨부한 LunaService API를 참고하여 "notification.operation"와 "peripheralmanager.gpio.operation"를 추가해줍니다. "notification.operation"은 toast를 띄우기 위하여, "peripheralmanager.gpio.operation"은 GPIO를 이용하기 위하여 추가해 줍니다.

 * Luna Service API 이용하기 

이번 포스팅에서는 Luna Service API 중 notification의 createToast와 peripheralmanage의 gpio를 사용하므로 url을 선언해줍니다. 그리고 아래의 링크를 참고하여 필수로 요구되는 parameter 를 지정해줍니다.

https://www.webosose.org/docs/reference/ls2-api/com-webos-notification/

 

com.webos.notification

API Summary Manages the system notifications. Overview of the API Enables apps or services to manage system notifications. The main types of notifications supported are: Toast: This is an info alert that displays the message and title. It gets dismissed

www.webosose.org

https://www.webosose.org/docs/reference/ls2-api/com-webos-service-peripheralmanager/

 

com.webos.service.peripheralmanager

API Summary The peripheralmanager is a service to provide APIs to monitor sensors and control actuators connected to I/O peripherals using industry-standard protocols.  Currently, supported protocols are UART ,GPIO, SPI and I2C. This service manages para

www.webosose.org

 

 

 

2. index.html 코드

    function callHello() {
        console.log("call my service");
        const url = 'luna://com.cosmos.team4.app.service/hello';
        const params = {
            name: "yunmi"
        };
        bridge.onservicecallback = (msg) => {
            console.log(msg);
            let res = JSON.parse(msg);
            document.getElementById("txt_msg").innerHTML = res.Response;
        };

        bridge.call(url, JSON.stringify(params));
    }

    function callToast() {
        console.log("call my service");
        const url = 'luna://com.cosmos.team4.app.service/toast';
        const params = {};
        bridge.onservicecallback = (msg) => {
            console.log(msg);
            let res = JSON.parse(msg);
            document.getElementById("txt_msg").innerHTML = res.Response;
        };

        bridge.call(url, JSON.stringify(params));
    }

    function callMonitorOn() {
        console.log("call my service");
        const url = 'luna://com.cosmos.team4.app.service/monitorOn';
        const params = {};
        bridge.onservicecallback = (msg) => {
            console.log(msg);
            let res = JSON.parse(msg);
            document.getElementById("txt_msg").innerHTML = res.Response;
        };

        bridge.call(url, JSON.stringify(params));
    }
</script>
</head>
<body>
    <div>
        <h1 id="txt_msg">Hello, Web Application!!</h1>
        <button id="hello" onclick="callHello()">hello</button>
        <button id="toast" onclick="callToast()">toast</button>
        <button id="on" onclick="callMonitorOn()">Monitor On</button>
    </div>
</body>
</html>

callMonitorOn : 버튼을 주기적으로 모니터링하는 기능은 서비스로 작성되어 있습니다(아래 Service 개발에서 service.js 코드 참고). Web application에서는 만들어둔 서비스를 호출하여 모니터링을 시작하도록만 하였습니다.

 

 

Service 개발 


1. json 코드

{
  "id": "com.cosmos.team4.app.service",
  "description": "Tact-switch monitoring Service",
  "services": [
    {
      "name": "com.cosmos.team4.app.service"
    }
  ]
}

id를 app 이름.service로 지정해주어서 그 앱에 종속된 서비스임을 알 수 있도록 합니다. 이때 app이름은 APP 개발에서 설정해준 이름을 말합니다. 

 

2. service.js 

서비스 코드가 동작하는 흐름을 요약하면 다음과 같습니다.

  • "monitorOn"이라는 메서드 호출
  • GPIO 설정을 진행
  • heartbeat 서비스를 구독
  • heartbeat 서비스에서 1초 간격으로 메시지 보내면 이벤트 핸들러를 이용하여 GPIO 값 읽고 스위치 눌렸으면 toast 띄우기
  • 2분 모니터링 실행되면 heartbeat 구독 취소하고 모니터링 종료

2.1 monitorOn 메서드 등록

아래의 코드를 이용하여 스위치 모니터링을 시작하는 코드를 작성해줍니다. "monitorOn"이 실행되면 GPIO 설정을 진행합니다. 버튼은 GPIO pin4와 연결하였으며 input 모드로 전압 값을 읽어야하니 direction을 'in'으로 설정하였습니다.

[참고] WebOS OSE에서 GPIO 사용하는 내용을 더 알아보고 싶으시면 링크를 참고해주세요.

 

"monitorOn" 메서드에서 create interval을 이용하여 주기적으로 GPIO 핀 값을 읽어오도록 구현할 수도 있습니다. 하지만 이렇게 구현하는 경우 서비스는 기본적으로 한 번 호출되면 5초의 실행 수명을 갖기때문에 5초가 지나가면 프로세스가 종료되어 더이상 모니터링을 진행하지 못 합니다.

 

이를 해결하기 위해 heartbeat 방식을 사용하며 저희는 1초 간격으로 오는 heartbeat 메세지가 오면 "response" 이벤트 리스너에서 GPIO 핀을 읽는 방식으로 구현하였습니다. 

 

이 방식을 사용하면 webOS OSE가 켜져있는 동안 계속하여 서비스가 살아있게 할 수 있으나 저희는 2분만 모니터링하도록 코드를 추가해 주었습니다.

[참고] WebOS OSE에서 heartbeat 서비스 관련 내용을 더 알아보고 싶으시면 링크를 참고해주세요.

 

let doInitGPIO = true;

service.register("monitorOn", (msg)=> {
    console.log(logHeader, msg);

    // GPIO 설정
    if(doInitGPIO) {
        let url = "luna://com.webos.service.peripheralmanager/gpio/open";
        let params = {
            pin: "gpio4"
        };

        service.call(url, params, (m2)=>{
            console.log(logHeader, m2);
        });

        url = "luna://com.webos.service.peripheralmanager/gpio/setDirection";
        params = {
            pin: "gpio4",
		    direction: "in"
        };

        service.call(url, params, (m2)=>{
            console.log(logHeader, m2);
        });

        doInitGPIO = false;
    }

    // ==== heartbeat 구독
    const sub = service.subscribe('luna://com.cosmos.team4.app.service/heartbeat', {subscribe: true});
    const max = 120;
    let count = 0;
    sub.addListener("response", function(msg) {
        console.log(JSON.stringify(msg.payload));
        readPin4();
        if (++count >= max) {
            sub.cancel();
            setTimeout(function(){
                console.log(max+" responses received, exiting...");
                process.exit(0);
            }, 1000);
        }
    });

    msg.respond({
        returnValue: true,
        Response: "Button-monitor-interval created."
    });
});

"monitorOn" 서비스 등록하는 코드

 

 

var heartbeat = service.register("heartbeat");
heartbeat.on("request", function(message) {
    console.log(logHeader, "SERVICE_METHOD_CALLED:/heartbeat");
    message.respond({event: "beat"}); // initial response 
    if (message.isSubscription) { 
        subscriptions[message.uniqueToken] = message; //add message to "subscriptions" 
        if (!heartbeatinterval) {
            createHeartBeatInterval();
        }
    } 
}); 
heartbeat.on("cancel", function(message) { 
    delete subscriptions[message.uniqueToken]; // remove message from "subscriptions" 
    var keys = Object.keys(subscriptions); 
    if (keys.length === 0) { // count the remaining subscriptions 
        console.log("no more subscriptions, canceling interval"); 
        clearInterval(heartbeatinterval);
        heartbeatinterval = undefined;
    } 
});

"heartbeat" 기능

 

2.2 readPin4()

1초 간격으로 readPin4() 함수가 호출됩니다. Luna service 중 라즈베리파이4의 GPIO 핀 값을 읽어오는 서비스를 이용하였습니다. 읽어온 값이 "high"면 스위치가 눌린 상태이므로 createToast()를 호출하여 toast를 출력합니다..

function readPin4() {
    let url = "luna://com.webos.service.peripheralmanager/gpio/getValue";
    let params = {
        pin: "gpio4"
    };

    service.call(url, params, (m2)=>{
        console.log(logHeader, m2);
        if(m2.payload.value == "high") {
            creatToast();
        }
    });
}

 

2.3 createToast()

Luna service 중 createToast를 이용하여 "Ding~dong~!"이라는 문구를 포함한 toast를 출력합니다. 

function creatToast() {
    let url = "luna://com.webos.notification/createToast";
    let params = {
        message: "Ding-dong~!"
    };

    service.call(url, params, (m2)=>{
        console.log(logHeader, "SERVICE_METHOD_CALLED:com.webos.notification/createToast");
    });
}

 

 

 

2.4 service.js 전체코드

// eslint-disable-next-line import/no-unresolved
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 + "]";

let doInitGPIO = true;

function creatToast() {
    let url = "luna://com.webos.notification/createToast";
    let params = {
        message: "Ding-dong~!"
    };

    service.call(url, params, (m2)=>{
        console.log(logHeader, "SERVICE_METHOD_CALLED:com.webos.notification/createToast");
    });
}

function readPin4() {
    let url = "luna://com.webos.service.peripheralmanager/gpio/getValue";
    let params = {
        pin: "gpio4"
    };

    service.call(url, params, (m2)=>{
        console.log(logHeader, m2);
        if(m2.payload.value == "high") {
            creatToast();
        }
    });
}

service.register("monitorOn", (msg)=> {
    console.log(logHeader, msg);

    // GPIO 설정
    if(doInitGPIO) {
        let url = "luna://com.webos.service.peripheralmanager/gpio/open";
        let params = {
            pin: "gpio4"
        };

        service.call(url, params, (m2)=>{
            console.log(logHeader, m2);
        });

        url = "luna://com.webos.service.peripheralmanager/gpio/setDirection";
        params = {
            pin: "gpio4",
		    direction: "in"
        };

        service.call(url, params, (m2)=>{
            console.log(logHeader, m2);
        });

        doInitGPIO = false;
    }

    // ==== heartbeat 구독
    const sub = service.subscribe('luna://com.cosmos.team4.app.service/heartbeat', {subscribe: true});
    const max = 120;
    let count = 0;
    sub.addListener("response", function(msg) {
        console.log(JSON.stringify(msg.payload));
        readPin4();
        if (++count >= max) {
            sub.cancel();
            setTimeout(function(){
                console.log(max+" responses received, exiting...");
                process.exit(0);
            }, 1000);
        }
    });
    // ==== 여기까지

    msg.respond({
        returnValue: true,
        Response: "Button-monitor-interval created."
    });
});

// handle subscription requests
const subscriptions = {};
let heartbeatinterval;
let x = 1;
function createHeartBeatInterval() {
    if (heartbeatinterval) {
        return;
    }
    console.log(logHeader, "create_heartbeatinterval");
    heartbeatinterval = setInterval(function() {
        sendResponses();
    }, 1000);
}

// 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++;
}

var heartbeat = service.register("heartbeat");
heartbeat.on("request", function(message) {
    console.log(logHeader, "SERVICE_METHOD_CALLED:/heartbeat");
    message.respond({event: "beat"}); // initial response 
    if (message.isSubscription) { 
        subscriptions[message.uniqueToken] = message; //add message to "subscriptions" 
        if (!heartbeatinterval) {
            createHeartBeatInterval();
        }
    } 
}); 
heartbeat.on("cancel", function(message) { 
    delete subscriptions[message.uniqueToken]; // remove message from "subscriptions" 
    var keys = Object.keys(subscriptions); 
    if (keys.length === 0) { // count the remaining subscriptions 
        console.log("no more subscriptions, canceling interval"); 
        clearInterval(heartbeatinterval);
        heartbeatinterval = undefined;
    } 
});

 

 

3. 보드 구성 

택트 스위치, 10k 옴 저항을 브레드보드에 다음과 같이 구성한 뒤, 라즈베리파이 4 보드의 GPIO 핀과 연결해 줍니다.

 

실행 결과


서비스 구현이 끝나면 web application과 web service를 함께 packaging한 뒤 설치를 진행합니다. Application을 실행한 뒤 monitor on 버튼을 눌러 모니터링을 실행합니다. 이후 다른 app을 실행한 뒤 버튼을 눌러봅니다.

[참고] 1초 간격, polling 방식으로 스위치 값을 읽으므로 버튼을 누르고 toast 가 출력되기까지 1초 정도 딜레이가 있을 수 있습니다.