gps 데이터와 sensor 데이터를 안정적으로 받기 위한 코드 수정 사항 (MQTT topic 분리)

하겅 ·2025년 5월 28일

Capstone

목록 보기
10/14

📁 MQTT Protocol topic 분리

1️⃣ Arduino

✔️ 기존 코드

String buildPayload(int count) {
  String payload = "{";
  payload += "\"count\":" + String(count);

  if (gps.location.isValid()) {
    payload += ",\"lat\":" + String(gps.location.lat(), 6);
    payload += ",\"lng\":" + String(gps.location.lng(), 6);
  } else {
    payload += ",\"lat\":null,\"lng\":null";  // 또는 기본값으로 0.0
  }

  payload += "}";
  return payload;
}

처음 작성했던 코드입니다 1. 적외선 센서로 받은 count 데이터2. gps 모듈로 받은 데이터 두 개를 하나의 json 데이터로 파싱합니다 코드상에서 봤을 때는 문제가 안 되지만 Spring Boot 에서 받으려니 기능이 수행되지 않아 이전 데이터가 오는 fallback이 발생했습니다

그 원인이 이것이다!라고 확신할 수는 없지만 코드를 보다 보니 문제점들이 보였습니다 우선 두 개의 모듈이 실행되는 조건이 달라도 너무 다릅니다

  1. gps: 2초마다 출력
  2. sensor: 적외선이 LOW가 될 때만 출력 (적외선이 가려질 때)

더 나아가 이 두 개 모듈로부터 정신없이 쏟아지는 데이터를 하나의 토픽으로 게시 중이었다는 점이!! 큰 원인이지 않을까 생각이 듭니다 (여기서 토픽은 MQTT 프로토콜에서 사용되는 문자열 주소 같은 것입니다)

따라서 해결한 방법은 아래와 같습니다

  • 토픽 분리 (esp32/ppl → esp32/ppl, esp32/gps)
  • interval 속도 조절

✔️ 수정 코드


// GPS 전용 payload 메서드 
// why? 이벤트 발생 시점이 달라 속도 차이 발생 
String buildGpsPayload(){
  if(gps.location.isValid()){
    String payload = "{"; // payload 데이터 시작 값 
    payload += "\"lat\":" + String(gps.location.lat(), 6);
    payload += ",\"lng\":" + String(gps.location.lng(), 6);
    payload += "}";
    return payload;
  } else{
    return "{\"lat\":null,\"lng\":null}";
  }
}

// SENSOR 전용 payload 메서드
String buildSensorPayload(int count){
  String payload = "{";
  payload += "\"count\":" + String(count);
   if (gps.location.isValid()) {
    payload += ",\"lat\":" + String(gps.location.lat(), 6);
    payload += ",\"lng\":" + String(gps.location.lng(), 6);
  } else {
    payload += ",\"lat\":null,\"lng\":null";
  }

  payload += "}";
  return payload;
}

이 두 개의 메서드는 publish 하기 전에 사용되거나 publish 메서드 파라미터로 직접 사용하거나 했습니다 {로 시작해서 }로 끝나도록 한 건 스프링 부트에서 JSON 데이터 형식으로 받기 위함이구요

buildSensorPayload에서 왜 gps 데이터를 파싱 중인가? 한다면 사실 굳이 필요는 없는 코드이기는 하지만 만약 buildGpsPayload가 gps 위치를 못 잡은 경우의 차선책으로 쓰일 것 같아 냅뒀습니다

const char* sensor_topic = "esp32/ppl";
const char* gps_topic = "esp32/gps";

두 개로 로 나눈 mqtt 토픽을 publish에 잘 배치해 주면


 else if (currentState == WAITING_FOR_2) {
    if (sensorState2 == LOW && sensor2PrevState == HIGH) {
      count++;
      Serial.println("🚎 탑승 완료 (1 → 2)");
      Serial.print("👥 현재 인원 수: ");
      Serial.println(count);

      client.publish(sensor_topic, buildSensorPayload(count).c_str(), false);

      
      currentState = IDLE;
      lastTriggerTime = now;
    }
  }

  (생략...)
  
  if (millis() - lastGpsSend  > gpsInterval) {
    lastGpsSend  = millis();

    if (gps.location.isValid()) {
      // GPS paylaod 게시 
      String gpsPayload = buildGpsPayload();
      client.publish("esp32/gps", gpsPayload.c_str(), false);
      Serial.println(gpsPayload);
      

    } else {
      Serial.println("GPS 수신 대기 중...");
    }
  }
  

👍 시리얼 모니터 결과 화면

gps, sensor 데이터 모두 안정적으로 출력되는 걸 알 수 있고

👍 스프링 부트 결과 화면

스프링 부트에서도 두 개의 데이터 모두 받아지는 걸 알 수 있었습니다




2️⃣ Spring Boot

여기서 그치지 않고 스프링 부트 코드도 수정을 해 줘야 합니다 기존에 application.properties에는 mqtt.topic=esp32/ppl 한 줄이었지만 이제 mqtt.topic=esp32/ppl, esp32/gps로 추가했기 때문입니다


👥 count: 0
📍 위치: 37.587215, 127.055927
MQTT 메시지 수신: {"lat":37.587227,"lng":127.055918}
👥 count: 0
📍 위치: 37.587227, 127.055918
MQTT 메시지 수신: {"count":3,"lat":37.587227,"lng":127.055918}
👥 count: 3
📍 위치: 37.587227, 127.055918
MQTT 메시지 수신: {"count":4,"lat":37.587212,"lng":127.055896}
👥 count: 4
📍 위치: 37.587212, 127.055896
MQTT 메시지 수신: {"lat":37.587218,"lng":127.055902}
👥 count: 0
📍 위치: 37.587218, 127.055902
MQTT 메시지 수신: {"count":5,"lat":37.587228,"lng":127.055907}
👥 count: 5
📍 위치: 37.587228, 127.055907
MQTT 메시지 수신: {"lat":37.587233,"lng":127.055908}
👥 count: 0
📍 위치: 37.587233, 127.055908

그대로 냅두면 위와 같이 위도, 경도가 들어올 때마다 count:0이 출력되는 문제가 발생합니다 따라서 스프링 부트에서도 토픽에 따라 분리해 줘야 합니다

✔️ 기존 코드

(수정 전까지는 ESP32 클래스를 썼지만 ESP32는 데이터의 정보가 현재 다른 상태라 파싱 자체가 불가능해 각각 BusPayload.javaGpsPayload.java를 만들어 구분해 줬습니다)

✔️ 수정 코드 (0528)


try {
                String payload = message.getPayload().toString();
                System.out.println("MQTT 메시지 수신: " + payload);

                // mqtt_receivedTopic: 스프링 Integration MQTT에서 자동으로 메시지에 붙여주는 헤더 키
                String receivedTopic = message.getHeaders().get("mqtt_receivedTopic").toString();
                if (receivedTopic.equals("esp32/ppl")) {
                    
                    BusPayload busData = objectMapper.readValue(payload, BusPayload.class);
                    System.out.println("count: "+ busData.getCount());

                } else if (receivedTopic.equals("esp32/gps")){
                    GpsPayload gpsData = objectMapper.readValue(payload, GpsPayload.class);
                    System.out.println("위치: "+ gpsData.getLat() + "," + gpsData.getLng());
                }

            } catch (Exception e) {
                System.err.println("❌ JSON 파싱 실패: " + e.getMessage());
            }
            

receivedTopic은 헤더에 있는 topic에 대한 정보를 가지고 있는 변수입니다 @Value("${mqtt.topic}") private String[] topic;topic을 그대로 적으니 topic이라는 이름이 배열 필드와 지역 변수로 충돌 중이어서 새로 만들어 줬습니다

↪ 첫 번째 if문으로 mqtt topic이 esp32/ppl임을 확인합니다 true인 경우 BusPayload.java 클래스의 오브젝트와 매핑하고, 매핑한 데이터 countgetCount()로 가져와 출력합니다

↪ 두 번째 else if문으로 mqtt topic이 esp32/gps임을 확인합니다 true인 경우 GpsPayload.java 클래스의 오브젝트와 매핑하고, 매핑한 데이터 latlng를 각각 getter 메서드를 통해 가져와 출력합니다

👍 결과 화면

적외선 센서로 count가 되지 않는 상황에는 gps 정보(위도, 경도)만 오다가 적외선 센서에서 LOW 값이 인식이 되자 "count: 1"이 함께 들어오는 것을 알 수 있습니다 또 count:0이 계속 사이에 껴서 출력되던 오류가 사라졌습니다




전체 코드 (아두이노)

#include <WiFi.h>
#include <PubSubClient.h>
#include "BluetoothSerial.h"
#include <TinyGPS++.h>

#define txPin1 18   // 송신기 2 (IR LED)
#define txPin2 22   // 송신기 1 (IR LED)
#define rxPin1 19   // 수신기 2 (DO)
#define rxPin2 23   // 수신기 1 (DO)

#define GPS_RX_PIN 27 // GPS 
#define GPS_TX_PIN 4 // GPS 

TinyGPSPlus gps;

const char* ssid = "";
const char* password = "";

const char* mqtt_server = "test.mosquitto.org";

const char* sensor_topic = "esp32/ppl";
const char* gps_topic = "esp32/gps";

WiFiClient wifiClient;
PubSubClient client(wifiClient);

int sensorState1 = 0;
int sensorState2 = 0;
int sensor1PrevState = HIGH;
int sensor2PrevState = HIGH;

int count = 0;

enum FlowState {
  IDLE,
  WAITING_FOR_2,
  WAITING_FOR_1
};

FlowState currentState = IDLE;


// GPS 전용 payload 메서드 
// why? 이벤트 발생 시점이 달라 속도 차이 발생 
String buildGpsPayload(){
  if(gps.location.isValid()){
    String payload = "{"; // payload 데이터 시작 값 
    payload += "\"lat\":" + String(gps.location.lat(), 6);
    payload += ",\"lng\":" + String(gps.location.lng(), 6);
    payload += "}";
    return payload;
  } else{
    return "{\"lat\":null,\"lng\":null}";
  }
}

// SENSOR 전용 payload 메서드
String buildSensorPayload(int count){
  String payload = "{";
  payload += "\"count\":" + String(count);
   if (gps.location.isValid()) {
    payload += ",\"lat\":" + String(gps.location.lat(), 6);
    payload += ",\"lng\":" + String(gps.location.lng(), 6);
  } else {
    payload += ",\"lat\":null,\"lng\":null";
  }

  payload += "}";
  return payload;
}



void reconnectMQTT(){
  Serial.println("Connecting to MQTT Broker...");
  while(!client.connected()){
    String clientId = "ESP32_Client-" + String(random(0xffff), HEX);
    if (client.connect(clientId.c_str())) {
      Serial.println("Connected to MQTT Broker!");
    }
    else{
      Serial.print("Failed to connect, ERROR: ");
      Serial.println(client.state());
      delay(5000);
    }
  }
}




void setup() {
  Serial.begin(115200);


  pinMode(txPin1, OUTPUT);
  pinMode(txPin2, OUTPUT);
  pinMode(rxPin1, INPUT);
  pinMode(rxPin2, INPUT);

  WiFi.begin(ssid, password);

  if (WiFi.status() != WL_CONNECTED) {
  Serial.println("WiFi Not Connected. Reconnecting...");
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
 
    Serial.print(".");
    Serial.println("WiFi status: ");
    Serial.println(WiFi.status());
    delay(1000);
  }
  Serial.println("WiFi Reconnected");
}



  // MQTT Broker과 연결
  client.setServer(mqtt_server, 1883);
  Serial.println("\n Wifi Connected.");
  wifiClient.setTimeout(5000);  // 5초 타임아웃
  client.setKeepAlive(30);      // MQTT Keep Alive 30초

  Serial2.begin(9600, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN); //RX,TX
  
  reconnectMQTT(); 
}




// 상태 변화가 발생했을 때만 true가 되도록 디바운스 시간 도입
// 디바운스: 통신 중 생기는 노이즈로 인해 발생하는 오차
// 센서 디바운싱 
unsigned long lastTriggerTime = 0;
const unsigned long debounceDelay = 2000; // 200ms 

// gps 디바운싱 (충돌 방지)
unsigned long lastGpsSend = 0;
const unsigned long gpsInterval = 5000;  // 5초마다 GPS 전송

void loop() {

  while (Serial2.available() > 0) {
      gps.encode(Serial2.read());
    }
    
  // MQTT 연결 확인
  if(!client.connected()){
    reconnectMQTT();
    }

  // MQTT 연결 유지 
  client.loop();


  digitalWrite(txPin1, HIGH);
  digitalWrite(txPin2, HIGH);

  sensorState1 = digitalRead(rxPin2); // 센서1 (DO23)
  sensorState2 = digitalRead(rxPin1); // 센서2 (DO19)

  unsigned long now = millis();
  
  // 상태 변화 감지
 if (now - lastTriggerTime > debounceDelay) {
  if (currentState == IDLE) {
    if (sensorState1 == LOW && sensor1PrevState == HIGH) {
      currentState = WAITING_FOR_2;
      Serial.println("1번 감지됨 → 2번 대기 중 (탑승 흐름 시작)");
      lastTriggerTime = now;
    } 
    else if (sensorState2 == LOW && sensor2PrevState == HIGH) {
      currentState = WAITING_FOR_1;
      Serial.println("2번 감지됨 → 1번 대기 중 (하차 흐름 시작)");
      lastTriggerTime = now;
    }
  }

  else if (currentState == WAITING_FOR_2) {
    if (sensorState2 == LOW && sensor2PrevState == HIGH) {
      count++;
      Serial.println("🚎 탑승 완료 (1 → 2)");
      Serial.print("👥 현재 인원 수: ");
      Serial.println(count);

      client.publish(sensor_topic, buildSensorPayload(count).c_str(), false);

      
      currentState = IDLE;
      lastTriggerTime = now;
    }
  }

  else if (currentState == WAITING_FOR_1) {
    if (sensorState1 == LOW && sensor1PrevState == HIGH) {
      if(count > 0){
        count--;
        Serial.println("🚎 하차 완료 (2 → 1)");
      }else{
        Serial.println("전원 하차 완료");
      }
      
      
      Serial.print("👥 현재 인원 수: ");
      Serial.println(count);

      
      client.publish(sensor_topic, buildSensorPayload(count).c_str(), true);

    
      currentState = IDLE;
      lastTriggerTime = now;
    }
  }
    
    if (millis() - lastGpsSend  > gpsInterval) {
    lastGpsSend  = millis();

    if (gps.location.isValid()) {
      // GPS paylaod 게시 
      String gpsPayload = buildGpsPayload();
      client.publish("esp32/gps", gpsPayload.c_str(), false);
      Serial.println(gpsPayload);
      

    } else {
      Serial.println("GPS 수신 대기 중...");
    }
  }
    

}


  // 상태 저장
  sensor1PrevState = sensorState1;
  sensor2PrevState = sensorState2;
}
void displayInfo(Stream &serial){
  // serial.print(F("위치: "));
  // 만약 gps 값이 유효하다면
  if(gps.location.isValid()){
    // 소수점 5자리까지 
    serial.print(gps.location.lat(), 5);
    serial.print(F(","));
    serial.print(gps.location.lng(), 5);

    
 
  } else{
    // serial.print(F("실패"));
  }

}

이런 문제를 발견하고 효율적인 코드를 찾아내는 과정이 정말 공부임을 다시 한번 배웠습니다 아직도 모자라겠지만 재미를 느낀 걸 잊지 않고 꾸준하게 공부해가고 싶으네용

코드 작성하면서 몰랐던 개념들을 적은 포스팅을 또 적겠쯥니다

profile
건강합시다

0개의 댓글