MQTT 통신 시리즈입니다.
- 네트워크 - 1. MQTT
- 네트워크 - 2. MQTT Client
MQTT Client : 웹앱
웹앱에서 MQTT Client를 설치하는 방법은 다음과 같습니다.
- 일반적인 Javascript 코드라면, mqtt.js를 CDN으로 링크할 수 있습니다.
- Node 앱이라면 npm(node package manager)를 사용하여 mqtt.js로 설치할 수 있습니다.
- React 앱이라면 위 2번 방법으로 할 수도 있고 별도의 react-mqtt 패키지도 있긴 한데, 저는 개인적으로 2번 방식을 추천합니다.
우리는 간단한 React 앱을 만들어 볼 텐데요, 아래와 같이 MqttComponent를 품고 있는 앱인데요, MQTT 연결 상태나 설정은 MqttComponent 내에서 처리가 될 테고 다만 publish를 위한 버튼만 추가된 상태죠. 즉, 버튼을 클릭하면 publish()가 호출되면서 data state가 업데이트되고 그에 따라 MqttComponent가 갱신되겠죠.
const App = () => {
const [data, setData] = React.useState(undefined);
const [counter, setCounter] = React.useState(1);
const [connected, setConnected] = React.useState('Connected');
const publish = () => {
const date = new Date();
const time = date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds();
const s = "TestData-" + (counter) + " ---- " + time;
setData({ topic: 'TOPIC1', payload: s });
setCounter(counter + 1);
}
return (<>
<p>State: {connected}</p>
<button style={{ margin: '2em' }} onClick={publish}>Send Data</button>
{data && <span>[{data.topic}] {data.payload}</span>}
<MqttComponent
subscribeTo={[
'TOPIC1', 'TOPIC2'
]}
publish={data}
callbacks={{
onConnect: (isConnected = true) => setConnected(isConnected ? "Connected" : "Disconnected"),
onMessage: (topic, payload) => {
console.log("onMessage: topic=" + topic, payload);
}
}}
settings={true}
log={true}
/>
</>)
}
MqttComponent를 살펴 보기 전에 먼저 실행해 봅시다.
- 이전 편에서 설명한, MQTT Broker를 실행합니다. (node server/AedesBroker.js)
- 두 개의 터미널을 열고 각각 npm start하면 서로 다른 2개의 포트(e.g. 3000, 3001)로 앱이 열립니다.
- 한 쪽에서 Send Data를 해 보면, 자신과 상대방 모두 받는 것이 보입니다. 둘 다 'TOPIC1'이라는 토픽으로 subscribe했고 publish하기 때문이죠.
MqttComponent
이제 MqttComponent를 살펴 봅시다.
import React, { Fragment } from 'react'
import 'antd/dist/antd.css';
import { Alert, Form, Input, Button, Card, Collapse } from 'antd';
import mqtt from 'mqtt';
const { Panel } = Collapse;
let g_var = {};
const loadPreference = (m, v) => {
if (localStorage.getItem(m) === null || localStorage.getItem(m) === undefined) {
localStorage.setItem(m, v);
}
return localStorage.getItem(m);
}
GUI는 깔끔하고 예쁜 Ant Design을 사용했고 loadPreference 함수를 정의했는데, 이는 localStorage, 즉 브라우저의 저장영역을 이용해서 앱/브라우저를 종료하고 다음 번에 접속했을 때에도 변경된 MQTT address와 port 정보를 유지할 수 있습니다.
export const MqttComponent = (props) => {
const savedAddress = loadPreference('rubenchoi-mqtt-address', '127.0.0.1');
const savedPort = loadPreference('rubenchoi-mqtt-port', '8888');
const [form] = Form.useForm();
const [address, setAddress] = React.useState(savedAddress);
const [port, setPort] = React.useState(savedPort);
const [publish, setPublish] = React.useState(undefined);
const [received, setReceived] = React.useState(undefined);
const [error, setError] = React.useState(undefined);
const formItemLayout = { labelCol: { span: 6 }, wrapperCol: { span: 18, } };
const buttonItemLayout = { wrapperCol: { span: 14, offset: 4 } };
const onFinish = (data) => {
setAddress(data.ip);
setPort(data.port);
localStorage.setItem('rubenchoi-mqtt-address', data.ip);
localStorage.setItem('rubenchoi-mqtt-port', data.port);
};
이제 컴포넌트를 살펴 보면, 최초 state 정보를 방금 언급한 localStorage로부터 가져오고 onFinish()에서 변경된 address, port 값을 localStorage에 저장합니다.
React.useEffect(() => {
const handleMessage = (topic, payload) => {
let decodedPayload = new Buffer.from(payload, 'base64').toString('utf-8');
setReceived("[" + topic + "] " + decodedPayload);
props.callbacks.onMessage(topic, decodedPayload);
}
const handleError = (err) => {
setError(err);
props.callbacks.onConnect(false);
}
const connect = () => {
try {
const url = 'ws://' + address + ':' + port;
const mqttHandler = mqtt.connect(url);
mqttHandler.on('connect', () => {
props.subscribeTo.forEach(topic => {
mqttHandler.subscribe(topic);
})
g_var.mqtt = mqttHandler;
props.callbacks.onConnect(true);
});
mqttHandler.on('disconnect', () => handleError('MQTT Disconnected'));
mqttHandler.on('error', (err) => handleError(err));
mqttHandler.on('message', handleMessage);
} catch (err) {
handleError(err);
}
}
connect();
}, [])
React.useEffect(() => {
try {
g_var.mqtt.publish(props.publish.topic, props.publish.payload);
setPublish(props.publish);
} catch (err) {
console.log('ERROR: cannot publish');
}
}, [props.publish])
이후 useEffect(()=>{...}, []) 는 componentDidMount()에 상응하는, 즉 최초 1회 불리는 함수인데요, 여기에서 MQTT 연결을 시도합니다. 이 때 mqtt.js라는 npm 패키지를 사용하고 연결이 되면 g_var라는 글로벌 변수에 mqtt 객체를 저장하는데, useEffect() 내에서 할당된 변수를 useEffect() 외부 혹은 또 다른 useEffect() 내에서 사용할 수 있도록 제가 종종 사용하는 방법입니다. state를 이용하는 방법도 있겠지만, g_var가 꼼수 같아 보이긴 해도 간단하죠.
두 번째 useEffect는 props.publish가 변할 때 호출됩니다. 즉, 부모 컴포넌트(App.js)에서 publish 데이터를 변경할 때마다 이 구문이 호출되면서 mqtt.publish()를 호출합니다.
return (
<>
{props.settings &&
<Card size="small" title="Settings" extra={'MQTT'} style={{ width: '50vw' }}>
<Form
{...formItemLayout}
layout={'horizontal'}
form={form}
initialValues={{
ip: address,
port: port
}}
onFinish={onFinish}
>
<Form.Item label="IP" name="ip">
<Input />
</Form.Item>
<Form.Item label="Port" name="port">
<Input />
</Form.Item>
<Form.Item {...buttonItemLayout}>
<Button type="primary" htmlType="submit">Submit</Button>
</Form.Item>
</Form>
{error && <div style={{ position: 'absolute' }}>{error}</div>}
{props.log && <>
<p>Log</p>
<Collapse defaultActiveKey={['1', '2']}>
<Panel header="Published Message" key="1">
{publish && <p>[{publish.topic}] {publish.payload}</p>}
</Panel>
<Panel header="Received Message" key="2">
<p>{received}</p>
</Panel>
</Collapse>
</>}
</Card>
}
</>
);
마지막으로 렌더링 부분을 살펴 보면, MQTT 주소 및 포트 설정 화면과 로그를 볼 수 있는 엘리먼트들이 있습니다.
지금까지 MQTT Client를 살펴보았는데요, 다음 번에는 MQTT를 이용해서 웹캠 이미지를 보내는 방법, 파이썬 앱과 MQTT 통신 등에 대해 알아보겠습니다.
'IT > Network & OS' 카테고리의 다른 글
ROS - 3. React 웹앱 (0) | 2021.05.27 |
---|---|
ROS - 2. ROS2 Web Server & 웹앱 (0) | 2021.05.17 |
ROS - 1. ROS2 설치 및 CLOi 시뮬레이터 (0) | 2021.05.17 |
네트워크 - 1. MQTT (0) | 2021.04.02 |
Art Deco Fonts - 무료 폰트 (0) | 2021.03.11 |