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이 발생했습니다
그 원인이 이것이다!라고 확신할 수는 없지만 코드를 보다 보니 문제점들이 보였습니다 우선 두 개의 모듈이 실행되는 조건이 달라도 너무 다릅니다
- gps: 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 데이터 모두 안정적으로 출력되는 걸 알 수 있고

스프링 부트에서도 두 개의 데이터 모두 받아지는 걸 알 수 있었습니다
여기서 그치지 않고 스프링 부트 코드도 수정을 해 줘야 합니다 기존에 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.java와 GpsPayload.java를 만들어 구분해 줬습니다)
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 클래스의 오브젝트와 매핑하고, 매핑한 데이터 count를 getCount()로 가져와 출력합니다
↪ 두 번째 else if문으로 mqtt topic이 esp32/gps임을 확인합니다 true인 경우 GpsPayload.java 클래스의 오브젝트와 매핑하고, 매핑한 데이터 lat와 lng를 각각 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("실패"));
}
}
이런 문제를 발견하고 효율적인 코드를 찾아내는 과정이 정말 공부임을 다시 한번 배웠습니다 아직도 모자라겠지만 재미를 느낀 걸 잊지 않고 꾸준하게 공부해가고 싶으네용
코드 작성하면서 몰랐던 개념들을 적은 포스팅을 또 적겠쯥니다