인공지능&로봇 자료실

인공지능&로봇 자료실 입니다.

제목클로바(Clova)에 ESP-01 연동 : Clova Home extension2021-10-26 05:31
작성자user icon Level 4
첨부파일node-server.zip (3.9KB)logo+banner.zip (253.1KB)

88x31.png


네이버 클로바에서는 CEK 플랫폼을 통해 확장 기능을 제공합니다.

extension을 만들어 클로바와 커뮤니케이션이 가능합니다.


이 글에서는 네이버 클로바에 ESP-01을 연동하여 음성제어 해보겠습니다.

https://www.robotstory.co.kr/raspberry/?board_name=raspberry_bbs&order_by=fn_pid&order_type=desc&vid=41를 참고하여 미리 was를 구축해두세요.



1. 도메인 생성


우선 도메인이 필요합니다. duckDNS에서 무료 도메인을 발급받겠습니다.


https://www.duckdns.org/로 이동합니다.


로그인하면 다음과 같은 화면이 뜨는데 빨간색으로 표시한 부분에 사용할 도메인을 입력하고 add domain을 누릅니다. 저는 robotstory-iot라고 입력했습니다.

mb-file.php?path=2021%2F10%2F25%2FF4105_2.png
 

아래쪽에 ip는 가상ip가 아니라 공인ip를 입력해야 합니다.

https://www.findip.kr/에서 확인합시다.

mb-file.php?path=2021%2F10%2F25%2FF4106_3.png
공유기 설정에서 포트포워딩도 해야하는데 이부분은 설명 생략하겠습니다.


도메인이 만들어졌습니다. 해당 도메인으로 접속해보세요.

(저같은 경우 http://robotstory-iot.duckdns.org/)


인터넷 업체에서 80포트를 막았다면 접속이 안될수도 있습니다. 

이때는 nginx에서 포트를 변경해야합니다.


라즈베리파이 터미널에서 다음 명령을 입력합니다.

 $ sudo nano /etc/nginx/sites-available/default


다음처럼 접속 포트를 변경합니다. 여기서는 8080으로 변경했습니다.

server {

listen 8080;

location / {

proxy_pass http://127.0.0.1:30001/;

proxy_http_version 1.1;

proxy_set_header Upgrade $http_upgrade;

proxy_set_header Connection "upgrade";

}

}

Ctrl + O를 눌러 저장하고 Ctrl + X를 눌러 빠져나옵니다.


nginx를 재부팅합니다.

 $ sudo service nginx restart 


해당 포트로 접속해보세요. (http://robotstory-iot.duckdns.org:8080/)



2. SSL 인증서 발급


extension은 HTTPS 프로토콜에만 연결됩니다. SSL 인증서를 발급 받아 HTTPS 서버를 만들어야 합니다.

이 글에서는 acme.sh 스크립트를 사용하여 무료 ssl 인증서를 발급 받겠습니다. acme.sh 스크립트는 간단하게 무료 인증서를 받을 수 있고 갱신 기간이 되면 자동으로 인증서를 갱신해줍니다.


라즈베리 터미널에서 다음 명령어를 입력하여 루트 계정으로 들어갑니다.

 $ su


초기 암호를 설정하지 않았다면 다음 명령을 입력하여 암호를 생성하고 들어갑니다.

 $ sudo passwd


다음 명령어를 입력하여 스크립트를 설치합니다.

 $ curl https://get.acme.sh | sh -s email='이메일주소'


터미널 접속을 종료하고 다시 접속하면 루트 계정으로 acme.sh 명령어를 사용 가능합니다.

터미널 재접속 후 다시 su를 입력하여 루트 계정으로 들어갑니다.


인증서를 저장할 폴더를 만듭니다.

 $ mkdir /home/pi/node-server/ssl


변수 설정을 합니다. duckDNS 토큰은 duckDNS 메인페이지에 나옵니다.

 $ export DuckDNS_Token="duckDNS 토큰"

 # export CERT_FOLDER="/home/pi/node-server/ssl"


mb-file.php?path=2021%2F10%2F25%2FF4107_4.png
 

다음을 입력하여 인증서를 생성합니다.

 $ acme.sh --insecure --issue \

   -d "robotstory-iot.duckdns.org" --dns dns_duckdns \

   --cert-file "$CERT_FOLDER/cert.pem" \

   --key-file "$CERT_FOLDER/privkey.pem" \

   --fullchain-file "$CERT_FOLDER/fullchain.pem" \

   --capath "$CERT_FOLDER/chain.pem"


참고 : timeout 에러가 나면 다른 인증서 서버로 교체합시다. ZeroSSL에서 Letsencrypt로 교체합니다.

 $ acme.sh --set-default-ca --server letsencrypt

서버 교체 후 다시 위의 명령어로 인증서를 생성합니다.


참고 : duckdns가 아닌 다른 도메인의 경우 명령어가 조금 다릅니다. 이 글에서는 생략하겠습니다.


다음 명령어로 인증서가 생성됬는지 확인할 수 있습니다.

 $ acme.sh list


이제 다시 nginx 설정파일을 열고 다음처럼 수정하세요.

 $ nano /etc/nginx/sites-available/default


server {

        listen 443 ssl;

        listen [::]:443 ssl;


        server_name robotstory-iot.duckdns.org;


        ssl on;

        ssl_certificate /home/pi/node-server/ssl/fullchain.pem;

        ssl_certificate_key /home/pi/node-server/ssl/privkey.pem;

        ssl_trusted_certificate /home/pi/node-server/ssl/chain.pem;


        location / {

                proxy_pass http://127.0.0.1:30001/;

                proxy_http_version 1.1;

                proxy_set_header Upgrade $http_upgrade;

                proxy_set_header Connection "upgrade";

        }

}


 $ service nginx restart


루트 계정을 나옵시다.

 $ exit



이제 https://robotstory-iot.duckdns.org로 접속가능합니다.



3. Clova Home extension 만들기
 


우선 https://developers.naver.com/console/clova/cek/#/list에 들어가서 CLOVA Home extension 만들기를 누릅니다.

mb-file.php?path=2021%2F10%2F25%2FF4104_1.png


Extension ID는 도메인을 반대순서로 적어줍시다. (org.duckdns.robotstory_iot)

Extension 이름, 제작사, 서비스 담당자 정보는 마음대로 적습니다.

테스터 ID는 네이버 ID입니다. 클로바가 연결된 네이버 ID를 적어주세요. 오른쪽에 +를 눌러야 추가됩니다.


mb-file.php?path=2021%2F10%2F26%2FF4108_5.png

하단에 만들기를 누릅니다.


서버설정입니다.

Extension 서버 URL은 요청에 응답할 서버입니다. 

(여기서는 https://robotstory-iot.duckdns.org/clova)

로그인 URL은 인증 서버입니다.

(여기서는 https://robotstory-iot.duckdns.org/clova/login)

클라이언트 ID클라이언트 secret은 access token을 획득할때 전달되는 인수입니다. 아무렇게나 지정하셔도 됩니다.

Access token URI는 access token을 획득하기 위한 서버입니다.

(여기서는 https://robotstory-iot.duckdns.org/clova/token)

mb-file.php?path=2021%2F10%2F26%2FF4109_6.png
mb-file.php?path=2021%2F10%2F26%2FF4110_7.png

하단에 저장을 누르고 다음을 누릅니다.


연동 기기 정보입니다.

ESP-01을 LED를 껏다 켰다 하는 스위치로 사용하기때문에 기기타입은 스위치를 추가합니다.

모델명과 구매페이지는 아무렇게나 적어도 됩니다.

mb-file.php?path=2021%2F10%2F26%2FF4111_8.png
하단에 저장을 누르고 다음을 누릅니다.


푸시 알림설정은 클로바 스피커로 음성 메시지를 전송할 수 있습니다. 이 글에서는 사용하지 않겠습니다.


mb-file.php?path=2021%2F10%2F26%2FF4112_9.png
다음을 누릅니다.


연동 페이지 정보에는 250 * 250 사이즈 로고와 750 * 500 사이즈 배너를 선택합니다.

예제로 첨부파일에 올려두었습니다.

하단에 저장을 누르고 다음을 누릅니다. 


개인 정보 수집 여부는 아니요를 선택하고 저장을 누릅니다. (심사신청하기 누르지마세요!)

mb-file.php?path=2021%2F10%2F26%2FF4113_10.png
 

휴대폰에 클로바 어플을 확인하세요.

mb-file.php?path=2021%2F10%2F26%2FF4114_11.png
mb-file.php?path=2021%2F10%2F26%2FF4115_12.png
mb-file.php?path=2021%2F10%2F26%2FF4116_13.png
mb-file.php?path=2021%2F10%2F26%2FF4117_14.png
 


4. 서버 만들기


일단 웹소켓부터 수정해줍시다. 웹소켓의 보안 프로토콜은 wss 입니다.

 $ sudo nano /home/pi/node-server/html/script.js


const btn = document.getElementById('switch');

let webSocket;

const socketInit = () => {


webSocket = new WebSocket("wss://robotstory-iot.duckdns.org");


webSocket.onmessage = (e) => {

if (e.data == 'on') {

btn.className = "on";

btn.innerText  = "ON";

}

else if (e.data == 'off') {

btn.className = "off";

btn.innerText  = "OFF";

}

}


webSocket.onclose = () => {

btn.className = "unknown";

btn.innerText  = "UNKNOWN";

setTimeout(socketInit, 300);

}



btn.onclick = () => {

if (btn.className == 'on') {

webSocket.send('off');

}

else if (btn.className == 'off') {

webSocket.send('on');

}

}

}


socketInit();

Ctrl + O를 눌러 저장하고 Ctrl + X를 눌러 빠져나옵니다.


다음으로 app.js를 수정합니다. 클로바 서버를 추가하고 상수는 config.js로 따로 만들었습니다.

 $ sudo nano /home/pi/node-server/app.js


const express = require('express');

const wsModule = require('ws');

const mqtt = require('mqtt');

const clova = require('./clova');

const {

SERVER_PORT, 

MQTT_SERVER_IP, 

PUBLISH_TOPIC, 

SUBSCRIBE_TOPIC

} = require('./config');


// *** Web Application Server Create


const app = express();


app.use(express.json());

app.use(express.urlencoded({ extended: true }));


app.use(express.static(__dirname + '/html'));


// *** Clova Server

app.use('/clova', clova);


// *** 404 Error

app.use((req, res) => {

res.status(404).send('Not Found');

});


const HTTPServer = app.listen(SERVER_PORT, () => {

    console.info(`Server is running on ${SERVER_PORT} port`);

});


// ****************************


// *** MQTT Client Create


const mqtt_client = mqtt.connect(`mqtt://${MQTT_SERVER_IP}`);

let status = '';


mqtt_client.on('connect', function () {

mqtt_client.subscribe(SUBSCRIBE_TOPIC, {qos: 1});

});


// ****************************


// *** WebSocket Create


const webSocketServer = new wsModule.Server({server: HTTPServer});

let clients = [];


mqtt_client.on('message', (topic, msg) => {

for (let client of clients) {

if (client.readyState === client.OPEN) {

client.send(msg.toString().toLowerCase());

}

}

});


webSocketServer.on('connection', (client, req) => {


const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;


clients.push(client);

mqtt_client.publish(PUBLISH_TOPIC, '', {qos: 1});


console.info(`Enter the new client: ${ip}`);


client.on('message', (msg) => {

try {

if (msg.toString().toLowerCase() == 'on') {

mqtt_client.publish(PUBLISH_TOPIC, 'on', {qos: 1});

else if (msg.toString().toLowerCase() == 'off') {

mqtt_client.publish(PUBLISH_TOPIC, 'off', {qos: 1});

}

}

catch (err) {

console.error(err);

client.send('error');

}

});


client.on('error', (err) => {

console.error(`Client ${ip} connection error: ${err}`); 

});


client.on('close', () => {


const idx = clients.indexOf(client);

if (idx != -1) {

clients.splice(idx, 1);

}


console.info(`Client ${ip} connection has been closed.`);

});

});

Ctrl + O를 눌러 저장하고 Ctrl + X를 눌러 빠져나옵니다.


상수 파일 config.js를 만듭니다.

 $ sudo nano /home/pi/node-server/config.js


module.exports = {

SERVER_PORT: process.env.SERVER_PORT || 30001,

MQTT_SERVER_IP: '127.0.0.1',

PUBLISH_TOPIC: 'cmnd/tasmota/POWER',

SUBSCRIBE_TOPIC: 'stat/tasmota/POWER',

}

Ctrl + O를 눌러 저장하고 Ctrl + X를 눌러 빠져나옵니다.


클로바 서버 파일을 만듭니다.

 $ sudo mkdir /home/pi/node-server/clova && sudo nano /home/pi/node-server/clova/index.js


const express = require('express');

const mqtt = require('mqtt');

const router = express.Router();

const {MQTT_SERVER_IP, PUBLISH_TOPIC, SUBSCRIBE_TOPIC} = require('../config');

const client = mqtt.connect(`mqtt://${MQTT_SERVER_IP}`);


router.post('/', function (req, res) {


let command = req.body.header.name;


switch (command) {

case "DiscoverAppliancesRequest":

DiscoverAppliancesRequest(req, res);

break;

case "HealthCheckRequest":

HealthCheckRequest(req, res);

break;

case "TurnOnRequest":

TurnOnRequest(req, res);

break;

case "TurnOffRequest":

TurnOffRequest(req, res);

break;

default:

res.sendStatus(403);

break;

}

});


router.get('/login', function (req, res) {

console.log(req.query);

let url = decodeURIComponent(req.query.redirect_uri)+"?state="+req.query.state+"&code="+"FakeToken"+"&token_type=Bearer";

res.redirect(url);

});


router.post('/token',function(req,res){

  res.send(`

  {

"access_token":"fakeAccess",

"refresh_token":"fakeRefresh"

  }

  `);

});


function DiscoverAppliancesRequest(req, res) {


let messageId = req.body.header.messageId;


let resultObject = new Object();

resultObject.header = new Object();

resultObject.header.messageId = messageId;

resultObject.header.name = "DiscoverAppliancesResponse ";

resultObject.header.namespace = "HomeIOT";

resultObject.header.payloadVersion = "1.0";

resultObject.payload = new Object();

resultObject.payload.discoveredAppliances = new Array();


let switchbot = new Object();

switchbot.applianceId = "ESP01-001";

switchbot.manufacturerName = " Ai-Thinker";

switchbot.modelName = "ESP-01";

switchbot.friendlyName = "전등";

switchbot.version = "9.5.0"

switchbot.isIr = false;

switchbot.actions = ["HealthCheck", "TurnOn", "TurnOff"];

switchbot.applianceTypes = ["SWITCH"];

resultObject.payload.discoveredAppliances.push(switchbot);


res.send(resultObject);

}


function HealthCheckRequest(req, res) {


const client = mqtt.connect(`mqtt://${MQTT_SERVER_IP}`);


client.on('connect', function () {


client.subscribe(SUBSCRIBE_TOPIC, {qos: 1});


let messageId = req.body.header.messageId;


let resultObject = new Object();

resultObject.header = new Object();

resultObject.header.messageId = messageId;

resultObject.header.name = "HealthCheckResponse ";

resultObject.header.namespace = "HomeIOT";

resultObject.header.payloadVersion = "1.0";

resultObject.payload = new Object();

resultObject.payload.isReachable = true;


client.on('message', (topic, msg) => {

if (msg.toString().toLowerCase() == "on") {

resultObject.payload.isTurnOn = true;

else {

resultObject.payload.isTurnOn = false;

}

res.send(resultObject);

client.end();

});


client.publish(PUBLISH_TOPIC, "", {qos: 1});

});

}


function TurnOnRequest(req, res) {


client.publish(PUBLISH_TOPIC, "ON");


let applianceId = req.body.payload.appliance.applianceId;

let messageId = req.body.header.messageId;


let resultObject = new Object();

resultObject.header = new Object();

resultObject.header.messageId = req.body.header.messageId;

resultObject.header.name = "TurnOnConfirmation";

resultObject.header.payloadVersion = "1.0";

resultObject.payload = new Object();

res.send(resultObject);

}


function TurnOffRequest(req, res) {


client.publish(PUBLISH_TOPIC, "OFF");


let applianceId = req.body.payload.appliance.applianceId;

let messageId = req.body.header.messageId;


let resultObject = new Object();

resultObject.header = new Object();

resultObject.header.messageId = req.body.header.messageId;

resultObject.header.name = "TurnOffConfirmation";

resultObject.header.payloadVersion = "1.0";

resultObject.payload = new Object();

res.send(resultObject);

}


module.exports = router;

on/off 상태 체크를 어떻게 해야할지 고민하다가 요청이 들어올때마다 mqtt에 접속하여 상태를 받아오고 접속을 종료하는 방식으로 코드를 짯습니다.


이제 클로바에 들어가서 로그인을 누릅시다.

mb-file.php?path=2021%2F10%2F26%2FF4118_1.gif
 

스위치로 껏다 켜봅시다.

mb-file.php?path=2021%2F10%2F26%2FF4120_2.gif
 


음성으로도 가능합니다. 음성대신 키보드로 명령어를 입력해보겠습니다.

mb-file.php?path=2021%2F10%2F26%2FF4119_3.gif
 

#클로바# Clova Home extension# 클로바 연동
댓글
자동등록방지
(자동등록방지 숫자를 입력해 주세요)