IT/Network & OS

네트워크 - 2. MQTT Client

루벤초이 2021. 4. 8. 17:52

MQTT 통신 시리즈입니다.

★Sample Code


MQTT Client : 웹앱

웹앱에서 MQTT Client를 설치하는 방법은 다음과 같습니다.

  1. 일반적인 Javascript 코드라면, mqtt.js를 CDN으로 링크할 수 있습니다.
  2. Node 앱이라면 npm(node package manager)를 사용하여 mqtt.js로 설치할 수 있습니다.
  3. 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를 살펴 보기 전에 먼저 실행해 봅시다.

  1. 이전 편에서 설명한, MQTT Broker를 실행합니다. (node server/AedesBroker.js)
  2. 두 개의 터미널을 열고 각각 npm start하면 서로 다른 2개의 포트(e.g. 3000, 3001)로 앱이 열립니다.
  3. 한 쪽에서 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 통신 등에 대해 알아보겠습니다.

 

728x90
반응형

'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