이번 프로젝트는 iot를 이용하여 세콤같은 안전 잠금장치를 만드는 것이다. 이를 위해선 라즈베리파이와 서버(frontend)의 실시간 통신이 필요했다.
기존에 사용해보았던 socket 통신으로 구현이 가능할 것 같기도 했지만 이번엔 mqtt를 사용해보고자 했다.
MQTT(Message Queueing Telemetry Transport)는 2016년 국제 표준화 된 (ISO 표준 ISO/IEC PRF 20922) 발행-구독(Publish-Subscribe) 기반의 메시지 송수신 프로토콜이다.
작은 코드 공간이 필요하거나 네트워크 대역폭이 제한되는 원격 통신을 위해, 즉 IoT와 같은 제한된, 혹은 대규모 트래픽 전송을 위해 만들어진 프로토콜이다. 그렇기에 TCP/IP 프로토콜 위에서 동작하지만 동시에 굉장히 가벼우며, 많은 통신 제약들을 해결해준다.
(그러나 이 말은 동시에 MQTT는 Bluetooth나 Zigbee처럼 별도의 모듈로 별도의 대역폭을 갖는 통신 규약이 아닌, WiFi나 기타 방법을 통해 인터넷을 통해 TCP/IP 기반의 메시지 송수신을 한다는 것을 의미하기도 한다.)
< 출처: https://underflow101.tistory.com/22 >
이번 프로젝트에서 mqtt를 사용하려는 이유는 두가지이다.
우선 실시간 통신이 가능하다. 기존에 사용했던 http 통신은 1요청, 1응답 통신이기에 실시간 통신에 제약이 있다. 우리 프로젝트의 경우에 라즈베리파이로 찍은 이미지를 서버에 전송을 하려 요청을 보내면 서버는 처리하여 프론트 사이드에 보내주지 않고 라즈베리파이에 응답을 보내준다. 또한 프론트 사이드에서 버튼을 누르면 프론트에 응답 처리를 해주게 된다. 라즈베리파이와 프론트 사이드가 데이터를 실시간으로 주고받아야 한다는 점에서 http 통신은 부적절하다 판단하였다.
둘째로는 단순하고 가볍다. 기존에 사용해본 socket 통신 역시 실시간 통신이 가능하다. 그러나 iot를 사용하는 이번 프로젝트에서는 비교적 mqtt가 유리하다고 한다. mqtt는 발행/구독 구조를 가지고 있어 비교적 헤더가 짧다. 따라서 전송해야하는 패킷이 적어지게 된다. 이는 많은 양의 데이터를 전송할 수 있고 저전력 환경에서 사용하기 유리하다는 장점으로 이어진다.
mqtt의 발행/구독 구조를 그림으로 표현해보면 이러하다.
<출처 : https://underflow101.tistory.com/22 >
mqtt는 토픽을 발행하는 publisher, 토픽을 구독하는 subscriber, 둘을 중계하는 broker로 나뉜다. 이 중에 broker는 서버와 같은 개념으로 이해하면 된다.
broker를 띄워놓고 subscriber가 토픽으로 구독을 하면 publisher가 토픽과 데이터를 보낸다. broker는 이 토픽들을 받아 데이터를 전송해주는 역할을 한다. 그리하여 broker를 띄워놓을 pc가 필요하다.
pc에 mqtt를 세팅해보자. 나는 mac환경에서 구축해보고자 하였다.
$brew install mosquitto // brew로 mqtt 브로커 설치
$brew services start mosquitto
$brew services stop mosquitto
// 여기까지는 mosquitto 설치 및 확인
다음과 같이 구현하면 된다.
// broker side
$/opt/homebrew/Cellar/mosquitto/2.0.15/sbin/mosquitto -c /opt/homebrew/Cellar/mosquitto/2.0.15/etc/mosquitto/mosquitto.conf
--> conf 파일 위치를 확인할 것
// subscriber side
$/opt/homebrew/Cellar/mosquitto/2.0.15/bin/mosquitto_sub -h 127.0.0.1 -p 1883 -t topic
--> mosquitto 경로를 꼭 확인해볼 것.
--> -h는 localhost, -p는 포트 번호, -t는 발행 토픽
--> 포트번호는 로컬 실행시 broker에서 출력됨(보통1883)
// publisher side
$/opt/homebrew/Cellar/mosquitto/2.0.15/bin/mosquitto_pub -h 127.0.0.1 -p 1883 -t topic -m "test message"
--> 토픽을 맞추고 메세지를 전송
실행한 화면이다. 위에서부터 subscirber, publisher, broker의 cli이다.
라즈베리파이는 데이터를 전송하는 입장, 프론트 사이드는 데이터를 받는 입장이 되어야 한다. node-red를 사용하는 라즈베리파이는 pubisher가 되어야 한다.
이번에는 node-red로 데이터를 전송해서 subscriber가 받는 것을 구현해보고자 한다.
inject에서 데이터를 보내 mqtt out으로 데이터를 전송하고자 한다.
이리하여 node-red에서 mqtt로 데이터를 주는 것이 가능해졌다. 다음은 서버에서 데이터를 받는 것을 구현해보자.
++) IP 수정 및 외부 연결 허용
위의 방법으로는 localhost인 127.0.0.1로만 접속이 가능하다. 그러나 라즈베리파이에서 서버에 접속하려면 로컬호스트로 접근해서는 안된다.
따라서 아이피를 수정해줘야하는데 이 방법을 공유해보고자 한다.
- .conf파일의 내용을 수정한다.
// conf파일의 가장 밑에 써주면 해결된다. allow_anonymous true bind_address __원하는 IP__
- 구독자와 발행자에 세팅되어있는 아이피를 수정한다.
/opt/homebrew/Cellar/mosquitto/2.0.15/bin/mosquitto_sub -h __해당 IP__ -p 1883 -t topic
- 모스키토를 켠다.
라즈베리파이에서 보낸 데이터는 PC에 있는 서버에서 받아야 한다. 그러기 위해서는 데이터를 받을 서버를 구축해야 한다.
이번에는 nodeJS로 mqtt 서버를 구축해보고자 한다.
// npm i mqtt --save
const mqtt = require("mqtt");
const client = mqtt.connect("mqtt://__IpAdress__");
client.on("connect", function () {
console.log("Connection Success");
client.subscribe("topic", function (err) {
if (!err) {
// 구독되었을 시에 실행할 코드를 적는다.
}
});
});
// subscirbe가 정상적으로 실행되면 작동됨
client.on("message", function (topic, message) {
// message is Buffer
console.log(message.toString());
});
//
mqtt를 npm에서 다운받아 예제를 내 주소에 맞게 실행했더니 성공하였다.
최종 코드는 이러하다.
const path = require("path");
const mqtt = require("mqtt");
const fs = require("fs");
// 메인 페이지를 렌더링해주는 API
exports.mainRender = async (req, res, next) => {
try {
// 모스키토를 띄운 서버의 ip를 입력
const client = mqtt.connect("mqtt://192.168.0.122");
// 메인페이지에 들어왔을때 최초 연결 시 subscirbe 해줌
client.on("connect", function () {
console.log("Connection Success");
// 각각의 토픽에 대해 subscirbe 해줌
client.subscribe("picture", (error) => {
if (error) console.log(error);
});
client.subscribe("open", (error) => {
if (error) console.log(error);
});
});
// subscirbe로 들어오는 메세지에 대한 처리
client.on("message", function (topic, message) {
// topic을 조건문으로 비교하여 메세지를 처리
switch (topic) {
// 문 열기 기능 시
case "open":
// true or false
console.log(message.toString());
break;
// 사진을 찍으면
case "picture":
console.log("picture save");
// 메세지를 버퍼로 받으므로 인코딩 및 디코딩 과정을 한번씩 해줌(확실하지 X)
const buffer = message.toString("base64");
const picture = Buffer.from(buffer, "base64");
// 버퍼상태의 이미지를 읽어와서 새로운 파일을 씀
fs.writeFileSync("public/new-path.jpg", picture);
break;
}
});
// 메인 페이지 렌더링
return res.sendFile(path.join(__dirname, "../views/phone.html"));
} catch (error) {
console.log(error);
next(error);
}
};
// 잠금장치를 여는 API
exports.openChecker = async (req, res) => {
try {
const client = mqtt.connect("mqtt://192.168.0.122");
// 버튼 클릭에 따라 body에 담기는 값 가져오기
const { open } = req.body;
// publishing 해줌
client.publish("open", open);
// 10초 후에 열려있다면 반드시 닫혀야하기에
if (open === "true") {
// 10초 후에 함수 실행
setTimeout(() => {
client.publish("open", "false");
}, 10000);
}
return res.status(200).send("btn ok");
} catch (error) {
console.log(error);
next(error);
}
};