본문 바로가기

iOS/SwiftUI

[SwiftUI] iOS에서 MQTT 통신 구현하기

 

오늘은 제가 iOS에서 MQTT 통신 연결에 대해서 설명하려고 합니다.

일단 MQTT 통신을 사용한 이유에 대해서 설명할게요!

 

졸업전시에서 미디어아트를 만드는데 아두이노와 iOS 앱 사이에서 통신이 필요했어요.

아두이노의 실시간 좌표값을 iOS 앱에서 받아야 했기 때문이죠.

 

그래서 제일 유명한 방법 중에 하나인 Firebase Realtime database 를 사용했는데.... ❌

이게 데이터 전달에서 1~2초 정도의 딜레이가 있어서.. 이 방법을 포기하고 MQTT를 구현하기 시작했다.

 

MQTT 장점

1. 별다른 장치가 필요없다.

2. 실시간 통신이 가능하다. (딜레이가 X)

 

먼저 MQTT란?

MQTT(Message Queuing Telemetry Transport)는 클라이언트에서 메시지를 발행(Publish)하고 수신(Subscribe) 할 수 있는 시스템이다. 간단한 메시징 프로토콜이기 때문에 사물 인터넷 애플리케이션에 적합하다. MQTT를 사용하면 여러 장치 사이에 명령을 보내 출력을 제어하고 센서 노드에서 데이터를 읽고 게시(Publish)하는 설정을 용이하게 할 수 있다.

그래서 나는

Publisher ➡️ 아두이노, Broker ➡️ 노트북, Subscriber ➡️ 아이패드 라고 생각하면서 구현했다.

 

1. Borker 구현 (노트북에 mosquitto 설치)

설치 ⚙️

brew install mosquitto

 

중지 ❌

brew services start mosquitto

 

재실행 🛠️

brew services restart mosquitto

 

비서비스 시작 📂

/opt/homebrew/opt/mosquitto/sbin/mosquitto -c /opt/homebrew/etc/mosquitto/mosquitto.conf

1️⃣ Broker 서비스 시작

: 앞의 비서비스 시작 명령어를 통하여 브로커를 실행시켜주자

/opt/homebrew/sbin/mosquitto -c /opt/homebrew/etc/mosquitto/mosquitto.conf

 

근데 여기서 자기 mosquitto가 어디에 설치되어 있는지 모르겠다면?

경로 찾는 명령어를 사용하면 된다.

which mosquitto

또는

sudo find / -name mosquitto

2️⃣ 해당 브로커의 토픽을 구독하자

👉🏻 mosquitto_sub -h [주소] -p [포트] -t [주제, 방제목]

 

주소, 포트, 토픽을 맞게 구독했다면 메시지를 publish하면 터미널에 메세지가 쭉 전송됩니다.

 

근데 이렇게 다 했는데도 pub이랑 sub이 잘 안된다? 밑에 블로그 참고해서 mosquitto 설정을 변경해주세요!

https://www.inflearn.com/community/questions/1221682/mqtt-%EC%84%9C%EB%B2%84-%EC%84%A4%EC%A0%95-%EB%B0%A9%EB%B2%95 


2. Publisher 아두이노 구현

MQTT 관련 아두이노 코드에는 ⭐️ 표시를 해두었습니다.

#include <Arduino.h>
#include <WiFi.h>
// 1. 라이브러리 추가 ⭐️
#include <PubSubClient.h>

// WiFi 설정
const char* ssid = "";
const char* password = "";

// 2. mqtt 서버 설정 ⭐️
const char* mqtt_server = ""; // MQTT server 주소
#define mqtt_topic "topic"    // topic (자유롭게 작성}
   

// mqtt Publisher 완성 ⭐️
WiFiClient espClient;
PubSubClient client(espClient);    // wifi client를 사용해서 client 만들어주기
int _idx = 0; //MQTT publish msg에 넣을 일련 번호

// ADXL335 핀번호 설정
const int xPin = 36;
const int yPin = 39;
const int zPin = 34;

void setup_wifi() {

  // WiFi 연결 ⭐️
  WiFi.begin(ssid, password);
  Serial.print("Connecting to Wi-Fi");
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(300);
  }

  Serial.println("");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
  Serial.println(WiFi.macAddress());
  Serial.println(WiFi.gatewayIP());
  Serial.println(WiFi.dnsIP());
  Serial.println(WiFi.subnetMask());

  client.setServer(mqtt_server, 8884); // 8884는 포트 번호 ⭐️
}

void reconnect() {
  while (!client.connected()) {
    if (client.connect("ESP32")) { // 연결 ID, anonymous
      Serial.println("MQTT Broker connected");
      return ;
    }
    else {
      Serial.println("MQTT Broker connection unsuccessful, retry in 5 sec.");
      delay(5000);
    }
  }
}

void callback(char* topic, byte* payload, unsigned int length) {
  //Serial.print("Message arrived [");
  //Serial.print(topic);
  //Serial.print("] ");
  String msg = "";
  for (int i = 0; i < length; i++) {
    msg +=(char)payload[i];
  }
  //Serial.print(msg);
  //Serial.println();
}


void setup() {
  Serial.begin(115200);
  setup_wifi();
  client.setServer(mqtt_server, 8884);
  client.setCallback(callback);
}

// 전역 변수로 이전 pitch와 roll 값을 저장할 변수 선언
int prevPitch = 0;
int prevRoll = 0;

float prevPitchF = 0.0;
float prevRollF = 0.0;

// 기본 방향 상태 변수
bool isUp = false;
bool isDown = false;
bool isLeft = false;
bool isRight = false;

void loop() {

  while (!client.connected()) {
    //Broker에 연결 시도
    reconnect();
  }

  client.loop();

  client.publish("topic", "up-right"); // topic 자리에 자신의 topic 넣어주기 ⭐️


  delay(400);
}

3. Subscriber iOS 구현

1️⃣  SPM 사용해서 MQTT 패키지 추가

https://github.com/emqx/CocoaMQTT

 

GitHub - emqx/CocoaMQTT: MQTT 5.0 client library for iOS and macOS written in Swift

MQTT 5.0 client library for iOS and macOS written in Swift - emqx/CocoaMQTT

github.com

 

2️⃣  MQTT 객체 생성 및 관련 함수 작성

이렇게 객체를 생성해주고 메세지를 전달한 메서드도 작성해줍니다.

import SwiftUI
import CocoaMQTT

final class MQTTManager: ObservableObject {
    private var mqtt: CocoaMQTT?
    @Published var receivedMessage: String = ""

    init() {
        setupMQTT()
    }

    private func setupMQTT() {
        let clientID = "CocoaMQTT-" + String(ProcessInfo().processIdentifier)

        mqtt = CocoaMQTT(clientID: clientID, host: "\(서버 IP⭐️)", port: [포트번호⭐️])
        mqtt?.keepAlive = 60
        mqtt?.willMessage = CocoaMQTTMessage(topic: "\(topic⭐️)", string: "==== Connected iOS")
        mqtt?.autoReconnect = true
        mqtt?.allowUntrustCACertificate = true
        mqtt?.delegate = self
        mqtt?.connect()
    }

    func subscribe(to topic: String) {
        if (self.mqtt?.connState == .connected) {
            print("✅ topic 구독 성공")

            self.mqtt?.didReceiveMessage = { mqtt, message, id in
                print("Message received in topic \(message.topic) with payload \(message.string!)")
            }

        }else{
            print("❌ 구독 연결이 끊어져있습니다.")
        }
    }

    func publish(topic: String, message: String) {
        mqtt?.publish(topic, withString: message)
    }
}

extension MQTTManager: CocoaMQTTDelegate {
    /// MQTT 연결 완료 콜백
    func mqtt(_ mqtt: CocoaMQTT, didConnectAck ack: CocoaMQTTConnAck) {

        // print(":::: didConnectAck ::::")

        if ack == .accept{
            // print(":::: 👏didConnectAck ::::\n")
            // print(":::: 👏브로커 연결 완료 ::::\n")
            self.mqtt?.subscribe("topic", qos: CocoaMQTTQoS.qos1)
        }
    }

    /// 발행 메시지
    func mqtt(_ mqtt: CocoaMQTT, didPublishMessage message: CocoaMQTTMessage, id: UInt16) {
    }

    /// 발행 완료
    func mqtt(_ mqtt: CocoaMQTT, didPublishAck id: UInt16) {
    }

    /// 구독한 토픽 메시지 Receive
    func mqtt(_ mqtt: CocoaMQTT, didReceiveMessage message: CocoaMQTTMessage, id: UInt16) {
        receivedMessage = message.string ?? "No didReceiveMessage"
    }

    func handleReceivedMessage(_ message: String) {
        // 받은 메시지에 대한 처리 로직
        print("메시지 처리: \(message)")
    }
    
    /// 토픽 구독 성공 콜백
    func mqtt(_ mqtt: CocoaMQTT, didSubscribeTopics success: NSDictionary, failed: [String]) {
        self.mqtt?.didReceiveMessage = { mqtt, message, id in
            //print("Message received in topic \(message.topic) with payload \(message.string!)")
        }
    }

    /// 토픽 구독 취소 콜백
    func mqtt(_ mqtt: CocoaMQTT, didUnsubscribeTopics topics: [String]) {
    }

    /// 연결 상태 체크 ping
    func mqttDidPing(_ mqtt: CocoaMQTT) {
    }

    /// 연결 상태 체크 pong
    func mqttDidReceivePong(_ mqtt: CocoaMQTT) {
    }

    func mqttDidDisconnect(_ mqtt: CocoaMQTT, withError err: Error?) {
    }
}

 

3️⃣ 메세지 Subscribe

전송 받은 메세지 값이 궁금한 곳에서 프린트를 출력해보면 잘 전송받았음을 확인할 수 있습니다.

 .onReceive(mqttManager.$receivedMessage) { newValue in
    print("📢📢📢📢=== Here: \(newValue)")
}

 

🗣️ 그리고 노트북에서 사용하는 와이파이와 아두이노에서 사용하는 와이파이가 같아야 합니다!!!!

 

 

MQTT를 구현한 iOS 깃허브 링크입니다.

https://github.com/god-fun-re-fun/Ressentiment-iOS/blob/main/Ressentiment-iOS/Ressentiment-iOS/UIKitContentView.swift

 

Ressentiment-iOS/Ressentiment-iOS/Ressentiment-iOS/UIKitContentView.swift at main · god-fun-re-fun/Ressentiment-iOS

르상티망 모바일 인터랙션. Contribute to god-fun-re-fun/Ressentiment-iOS development by creating an account on GitHub.

github.com

 

이렇게 3가지의 구현을 모두 마쳤다면 메세지가 잘 전송되고 구독되고 있음을

터미널 모스키토를 구독하거나 Xcode에서 로그를 프린트를하여 확인 할 수 있습니다.

 

혹시 구현하다가 안되는 것이나 궁금한 점이 있으시다면 댓글 남겨주세요! 제가 아는 선에서 답변 남겨드리겠습니다!