로보테크AI

융합_로보테크 AI 자율주행 로봇 개발자 과정-26/03/03

steezer 2026. 3. 3. 18:30

board1_wifi

**────────────────────────────────**



**시스템 개요**



**Arduino Uno + HC-SR04 4개 + RGB LED + ESP8266(AT 펌웨어) 구성**

**전·후·좌·우 거리 측정 후 최소 거리 기준으로 위험 등급 판별 구조**

**판별 결과를 RGB LED로 시각화하고 UDP로 PC에 전송하는 IoT 노드 구조**



**전체 흐름 구조:**



**센서 측정**

**→ 최소 거리 계산**

**→ 위험 등급 분류**

**→ RGB 상태 표시**

**→ UDP 패킷 전송**

**→ Python 수신**



**────────────────────────────────**



**전체 동작 구조**



**setup()**

* **시리얼 통신 초기화**
* **초음파 및 RGB 핀 모드 설정**
* **RGB 점등 테스트**
* **WiFi 초기화 및 UDP 소켓 오픈**



**loop()**

* **4방향 거리 측정**
* **최소 거리 계산**
* **위험 등급 판별**
* **RGB 업데이트**
* **등급 변경 시 즉시 전송**
* **500ms 주기 전송**
* **디버그 출력**



**────────────────────────────────**



**통신 구조**



**ESP8266 AT 명령 기반 WiFi 연결 구조임**

**UDP 단방향 송신 구조임**

**PC에서 socket.bind(5000)로 수신 구조임**



**전송 데이터 형식:**

**B1,F:152,B:153,L:153,R:118,MIN:118,LV:SAFE**



**문자열 기반 CSV 구조**

**Python에서 split(",") 후 파싱 가능**



**────────────────────────────────**



**변수 역할 정리**



**WiFi 관련**



**WIFI\_SSID**

**접속할 무선 네트워크 이름 저장 변수임**

**WIFI\_PASS**

**무선 네트워크 비밀번호 저장 변수임**

**DEST\_IP**

**UDP 수신 PC의 IPv4 주소 저장 변수임**

**DEST\_PORT**

**UDP 수신 포트 번호 저장 변수임**



**센서 관련**



**TRIG\[4]**

**초음파 트리거 핀 배열**

**0=Front, 1=Back, 2=Left, 3=Right 구조**

**ECHO\[4]**

**초음파 에코 핀 배열**

**DIR\[4]**

**각 방향 문자열 저장 배열**

**디버그 출력용**



**RGB 관련**



**RGB\_R, RGB\_G, RGB\_B**

**RGB LED PWM 제어 핀 번호 저장 변수임**



**거리 기준 값**



**D\_DANGER**

**위험 거리 기준 10cm**

**D\_CLOSE**

**근접 거리 기준 30cm**

**D\_MID**

**중간 거리 기준 60cm**



**상태 관련**



**enum Level**

**위험 등급 열거형 구조**

**SAFE, MID\_LV, CLOSE\_LV, DANGER\_LV 정의**

**currentLevel**

**현재 위험 등급 저장 변수**

**dist\[4]**

**각 방향 거리값 저장 배열**

**lastSend**

**마지막 UDP 전송 시각 저장 변수**

**SEND\_MS**

**UDP 전송 주기(500ms) 저장 상수**

**wifiReady**

**WiFi 연결 성공 여부 저장 변수**



**────────────────────────────────**



**함수 역할 정리**



**measureDist(int trig, int echo)**

**초음파 거리 측정 함수**

**TRIG에 10µs 펄스 출력 후 pulseIn으로 echo 시간 측정**

**거리 = 시간 × 0.034 / 2 계산 구조**

**측정 실패 시 999 반환 구조**



**classify(float d)**

**거리값을 위험 등급으로 변환하는 함수**

**임계값 비교 기반 분기 구조**



**setRGB(int r, int g, int b)**

**RGB LED PWM 출력 제어 함수**



**updateRGB(float d)**

**거리 기반 색상 및 밝기 자동 제어 함수**

**DANGER: 빨강 점멸 구조**

**CLOSE: 빨강→주황 그라데이션 구조**

**MID: 파랑 밝기 변화 구조**

**SAFE: 약한 초록 점등 구조**



**sendUDP(float minD, Level lv)**

**UDP 전송용 문자열 생성 함수**

**CSV 포맷 구성 후 udpSend 호출 구조**



**lvStr(Level lv)**

**enum 값을 문자열로 변환하는 함수**

**UDP 패킷에 등급 텍스트 포함 목적**



**espFlush()**

**ESP 수신 버퍼 초기화 함수**

**이전 응답 제거 목적**



**atCmd(const char\* cmd, const char\* expect, unsigned long timeout)**

**AT 명령 전송 및 응답 대기 함수**

**특정 문자열 포함 여부로 성공 판별 구조**



**wifiInit()**

**WiFi 연결 및 UDP 소켓 오픈 함수**

**AT → ATE0 → CWMODE → CWJAP → CIFSR → CIPSTART 순서 구조**



**udpSend(const char\* data)**

**AT+CIPSEND 수행 함수**

**데이터 길이 전송 → '>' 프롬프트 대기 → 실제 데이터 전송 → SEND OK 확인 구조**



**────────────────────────────────**



**알고리즘 구조 요약**



**거리 측정 구조**

**각 센서를 순차 측정 후 배열에 저장 구조**



**최소값 탐색 구조**

**for문으로 dist 배열 중 최소값 선택 구조**



**위험 판단 구조**

**임계값 기반 if 분기 구조**



**이벤트 기반 전송 구조**

**등급 변경 시 즉시 전송**

**주기 기반 전송 병행 구조**



**────────────────────────────────**



**네트워크 동작 구조**



**ESP 부팅**

**→ AT 통신 확인**

**→ WiFi 접속**

**→ IP 할당**

**→ UDP 소켓 오픈**

**→ AT+CIPSEND 반복 수행**



**PC는 UDP 5000 포트에서 대기**

**패킷 수신 후 파싱 가능 구조**



**────────────────────────────────**



**시스템 특징**



**배열 기반 센서 확장 가능 구조**

**enum 기반 가독성 향상 구조**

**비차단 네트워크 처리 구조**

**실시간 LED 피드백 구조**

**UDP 사용으로 지연 최소화 가능 구조**



**────────────────────────────────**



**최종 구조 정의**



**본 코드는**

* **실시간 거리 센싱**
* **위험 판단 알고리즘**
* **RGB 시각화**
* **WiFi 네트워크 통신**
* **UDP 기반 IoT 데이터 전송**
* 
**을 통합한 임베디드 센싱 노드 구조임.**
/*  BOARD 1 — 사물 인식 모듈 (WiFi 버전)
 *  Arduino Uno R3 + ESP8266 (AT 펌웨어)
 *
 * ─────────────────────────────────────────────────────
 *  핀 배치
 * ─────────────────────────────────────────────────────
 *  HC-SR04 전방   TRIG=A0  ECHO=A1
 *  HC-SR04 후방   TRIG=A2  ECHO=A3
 *  HC-SR04 좌측   TRIG=2   ECHO=4
 *  HC-SR04 우측   TRIG=7   ECHO=8
 *
 *  RGB LED R      D11  (PWM~)
 *  RGB LED G      D10  (PWM~)
 *  RGB LED B      D6   (PWM~)
 *  RGB LED GND    GND
 *
 *  ESP8266 TX     D3   (SoftSerial RX ← ESP8266 TXD)
 *  ESP8266 RX     D5   (SoftSerial TX → ESP8266 RXD)=
 *  ESP8266 VCC    3.3V
 *  ESP8266 GND    GND
 *  ESP8266 CH_PD  3.3V
 *  ESP8266 RST    3.3V (또는 미연결)
 *
 * ─────────────────────────────────────────────────────
 *  동작
 *  - 4방향 거리 100ms마다 측정
 *  - RGB LED : 최솟값 기준 색상+밝기 변화
 *      < 10cm  : 빨강 깜빡임 (DANGER)
 *      10~30cm : 주황        (CLOSE)
 *      30~60cm : 파랑 dim    (MID)
 *      >  60cm : 초록 희미  (SAFE)
 *  - 500ms마다 (또는 등급 변화 시 즉시) UDP로 전송
 *    형식: B1,F:23,B:45,L:12,R:67,MIN:12,LV:CLOSE
 *
 *  전송 방식: UDP (목적지 IP: DEST_IP, 포트: DEST_PORT)
 *  라이브러리: SoftwareSerial (내장)
 * ─────────────────────────────────────────────────────
 */

#include <SoftwareSerial.h>

// ── WiFi 설정 ──────────────────────────────────────────
const char* WIFI_SSID = "3F_302"; // ESP8266이 접속할 WiFi 정보
const char* WIFI_PASS = "0424719222!!";
const char* DEST_IP   = "192.168.0.204";        // UDP 목적지 (Python이 실행 중인 PC의 IPv4 주소)
const int   DEST_PORT = 5000;                  // UDP 수신 포트(Python에서 bind한 포트와 동일해야 함)
// 윈도우 방화벽 -> 고급 설정 -> 인바운드 규칙(새규칙) -> 포트 -> UDP -> 5000 -> 연결 허용 -> 모든 프로필 허용
// 제어판에서 윈도우 방화벽 끄기

// ── 핀 정의 ───────────────────────────────────────────
// 4방향 초음파 센서 TRIG / ECHO 핀 배열
// 인덱스 0=Front, 1=Back, 2=Left, 3=Right
const int TRIG[4]      = {A0, A2,  2,  7};
const int ECHO[4]      = {A1, A3,  4,  8};

// 방향 표시용 문자열 배열
const char* DIR[4]     = {"F", "B", "L", "R"};

// RGB LED 핀 (PWM 출력 가능 핀)
const int RGB_R = 11;
const int RGB_G = 10;
const int RGB_B =  6;

// ESP8266 SoftwareSerial (RX, TX)
// D3 <- ESP TX
// D5 -> ESP RX
SoftwareSerial espSerial(3, 5);

// ── 거리 임계값 정의 (단위: cm) ─────────────────────────
const float D_DANGER = 10.0;   // 10cm 미만 -> 위험
const float D_CLOSE  = 30.0;   // 10~30cm -> 근접
const float D_MID    = 60.0;   // 30~60cm -> 중간

// ── 위험 등급 열거형(enum) ──────────────────────────────
enum Level {
  SAFE,        // 안전
  MID_LV,      // 중간
  CLOSE_LV,    // 근접
  DANGER_LV    // 위험
};

Level currentLevel = SAFE;   // 현재 등급 저장

// ── 전역 변수 ──────────────────────────────────────────
float dist[4]         = {999, 999, 999, 999}; // 각 방향 거리 저장 배열
unsigned long lastSend = 0; // UDP 전송 주기 관리용
const unsigned long SEND_MS = 500; // 500ms마다 전송
bool wifiReady = false; // WiFi 연결 성공 여부

// ── 함수 선언 ──────────────────────────────────────────
float      measureDist(int trig, int echo); // 거리 측정
Level      classify(float d); // 거리 -> 등급
void       setRGB(int r, int g, int b); // RGB 직접 설정
void       updateRGB(float d); // 거리 기반 RGB 제어
void       sendUDP(float minD, Level lv); // UDP 전송
const char* lvStr(Level lv); // enum -> 문자열

// ESP8266 AT 명령 관련
bool       atCmd(const char* cmd, const char* expect, unsigned long timeout = 3000);
void       espFlush();
bool       wifiInit();
bool       udpSend(const char* data);

// ══════════════════════════════════════════════════════
void setup() {
  // USB 시리얼 모니터용
  Serial.begin(9600);
  // ESP8266 통신용 시리얼
  espSerial.begin(9600);

  // 초음파 핀 설정
  for (int i = 0; i < 4; i++) {
    pinMode(TRIG[i], OUTPUT);
    pinMode(ECHO[i], INPUT);
  }

  // RGB 핀 설정
  pinMode(RGB_R, OUTPUT);
  pinMode(RGB_G, OUTPUT);
  pinMode(RGB_B, OUTPUT);

  // 시작 점등 확인 (R->G->B)
  setRGB(255, 0, 0); delay(300);
  setRGB(0, 255, 0); delay(300);
  setRGB(0, 0, 255); delay(300);
  setRGB(0, 0, 0);

  Serial.println("[BOARD1] Setup start");

  // WiFi 초기화
  wifiReady = wifiInit();

  if (wifiReady) {
    Serial.println("[WiFi] Connected!");
    setRGB(0, 50, 0);   // 초록 — 연결 성공 표시
    delay(500);
    setRGB(0, 0, 0);
  } else {
    Serial.println("[WiFi] FAILED — check wiring/SSID/PW");
    // 빨강 천천히 점멸 -> 연결 실패 표시
    for (int i = 0; i < 5; i++) {
      setRGB(200, 0, 0); delay(400);
      setRGB(0, 0, 0);   delay(400);
    }
  }

  Serial.println("[BOARD1] Ready");
}

// ══════════════════════════════════════════════════════
void loop() {
  unsigned long now = millis();

  // 4방향 거리 측정
  for (int i = 0; i < 4; i++) {
    dist[i] = measureDist(TRIG[i], ECHO[i]);
  }

  // 최솟 거리 계산
  float minD = dist[0];
  for (int i = 1; i < 4; i++) {
    if (dist[i] < minD) minD = dist[i];
  }

  // 등급 판별
  Level prev  = currentLevel;
  currentLevel = classify(minD);

  // RGB 업데이트
  updateRGB(minD);

  // 등급 변화 시 즉시 전송
  if (currentLevel != prev) {
    sendUDP(minD, currentLevel);
    lastSend = now;
  }

  // 정기 전송 500ms
  if (now - lastSend >= SEND_MS) {
    lastSend = now;
    sendUDP(minD, currentLevel);
  }

  // 시리얼 디버그
  for (int i = 0; i < 4; i++) {
    Serial.print(DIR[i]); Serial.print(":"); Serial.print((int)dist[i]); Serial.print(" ");
  }
  Serial.print("LV:"); Serial.println(lvStr(currentLevel));

  delay(100);
}

// ══════════════════════════════════════════════════════
//  초음파 거리 측정 (cm)
float measureDist(int trig, int echo) {
  // 트리거 신호 생성
  digitalWrite(trig, LOW);
  delayMicroseconds(2);
  digitalWrite(trig, HIGH);
  delayMicroseconds(10);
  digitalWrite(trig, LOW);

  //echo가 HIGH인 시간 측정
  long dur = pulseIn(echo, HIGH, 30000UL);
  if (dur == 0) return 999.0;
  // 음속 0.034cm/us 이용
  return dur * 0.034f / 2.0f;
}

// ── 거리 -> 등급 ────────────────────────────────────────
Level classify(float d) {
  if (d < D_DANGER) return DANGER_LV;
  if (d < D_CLOSE)  return CLOSE_LV;
  if (d < D_MID)    return MID_LV;
  return SAFE;
}

// ── RGB 직접 출력 ──────────────────────────────────────
void setRGB(int r, int g, int b) {
  analogWrite(RGB_R, r);
  analogWrite(RGB_G, g);
  analogWrite(RGB_B, b);
}

// ── 거리에 따른 RGB 자동 제어 ──────────────────────────
void updateRGB(float d) {
  if (d >= 400) { setRGB(0, 3, 0); return; }

  if (d < D_DANGER) {
    bool on = (millis() % 300) < 150;
    setRGB(on ? 255 : 60, 0, 0);

  } else if (d < D_CLOSE) {
    float t = (d - D_DANGER) / (D_CLOSE - D_DANGER);
    setRGB(255, (int)(100 * t), 0);

  } else if (d < D_MID) {
    float t = 1.0 - (d - D_CLOSE) / (D_MID - D_CLOSE);
    setRGB(0, 0, (int)(30 + 150 * t));

  } else {
    float t = 1.0 - min((d - D_MID) / 100.0f, 1.0f);
    setRGB(0, (int)(5 + 35 * t), 0);
  }
}

// ── UDP 전송 (WiFi 준비 안 됐으면 Serial만 출력) ────────
void sendUDP(float minD, Level lv) {
  char buf[80];
  snprintf(buf, sizeof(buf),
    "B1,F:%d,B:%d,L:%d,R:%d,MIN:%d,LV:%s",
    (int)dist[0], (int)dist[1],
    (int)dist[2], (int)dist[3],
    (int)minD, lvStr(lv)
  );

  Serial.print("[UDP] "); Serial.println(buf);

  if (wifiReady) {
    if (!udpSend(buf)) {
      Serial.println("[UDP] Send failed");
    }
  }
}

// ── enum -> 문자열 ─────────────────────────────────────
const char* lvStr(Level lv) {
  switch (lv) {
    case DANGER_LV: return "DANGER";
    case CLOSE_LV:  return "CLOSE";
    case MID_LV:    return "MID";
    default:        return "SAFE";
  }
}

// ══════════════════════════════════════════════════════
//  ESP8266 AT 명령 처리
// ══════════════════════════════════════════════════════

// ESP8266 수신 버퍼 비우기
void espFlush() {
  unsigned long t = millis();
  while (millis() - t < 200) {
    while (espSerial.available()) espSerial.read();
  }
}

// AT 명령 전송 후 기대 문자열 포함 여부 확인
bool atCmd(const char* cmd, const char* expect, unsigned long timeout) {
  espFlush();
  espSerial.println(cmd);
  Serial.print("[AT] >> "); Serial.println(cmd);

  String resp = "";
  unsigned long start = millis();
  while (millis() - start < timeout) {
    while (espSerial.available()) {
      char c = espSerial.read();
      resp += c;
    }
    if (resp.indexOf(expect) != -1) {
      Serial.print("[AT] << "); Serial.println(resp);
      return true;
    }
  }
  Serial.print("[AT] TIMEOUT — got: "); Serial.println(resp);
  return false;
}

// WiFi 연결 + UDP 소켓 열기
bool wifiInit() {
  delay(3000);  // ESP8266 부팅 대기

  // 1) 모듈 응답 확인
  if (!atCmd("AT", "OK", 3000)) return false;

  // 2) Echo off
  atCmd("ATE0", "OK", 2000);

  // 3) Station 모드
  if (!atCmd("AT+CWMODE=1", "OK", 2000) &&
    !atCmd("AT+CWMODE=1", "no change", 2000))
  return false;

  // 4) WiFi 연결 (최대 15초)
  char joinCmd[80];
  snprintf(joinCmd, sizeof(joinCmd), "AT+CWJAP=\"%s\",\"%s\"", WIFI_SSID, WIFI_PASS);
  if (!atCmd(joinCmd, "OK", 15000)) return false;
  atCmd("AT+CIFSR", "OK", 3000);

  // 5) UDP 소켓 열기 (링크 ID=0, 로컬포트=임의)
  char udpCmd[80];
  snprintf(udpCmd, sizeof(udpCmd),
    "AT+CIPSTART=\"UDP\",\"%s\",%d", DEST_IP, DEST_PORT);
  if (!atCmd(udpCmd, "OK", 5000) && !atCmd(udpCmd, "ALREADY CONNECT", 2000)) return false;

  return true;
}

// UDP 데이터 전송 (AT+CIPSEND)
bool udpSend(const char* data) {
  int len = strlen(data) + 2;  // \r\n 포함

  char sendCmd[30];
  snprintf(sendCmd, sizeof(sendCmd), "AT+CIPSEND=%d", len);

  espSerial.println(sendCmd);
  Serial.print("[AT] >> "); Serial.println(sendCmd);

  // '>' 프롬프트 대기
  unsigned long start = millis();
  bool gotPrompt = false;
  String resp = "";
  while (millis() - start < 3000) {
    while (espSerial.available()) {
      char c = espSerial.read();
      resp += c;
      if (c == '>') { gotPrompt = true; break; }
    }
    if (gotPrompt) break;
  }

  if (!gotPrompt) {
    Serial.println("[AT] No '>' prompt");
    return false;
  }

  // 실제 데이터 전송
  espSerial.println(data);

  // SEND OK 대기
  return atCmd("", "SEND OK", 3000);
}
import socket

UDP_IP = "0.0.0.0"
UDP_PORT = 5000

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((UDP_IP, UDP_PORT))
#sock.settimeout(2.0)

print(f"Listening on UDP {UDP_IP}:{UDP_PORT} ...")

while True:
    try:
        data, addr = sock.recvfrom(2048)
        msg = data.decode(errors="replace").strip()
        print(f"[FROM {addr}] {msg}")
    except socket.timeout:
        print("...no packet")

 

board2_wifi

(조도, 습도, 소리)

/*
 * ============================================================
 *  BOARD2 — 위험 감지 시스템
 *
 *  [사용 센서 & 실제 핀 구성]
 *   DHT11 모듈     : VCC / DATA / GND  (모듈형, 3핀)
 *   Sound 센서 모듈: +   / G   / A0 / D0
 *   포토센서(LDR)  : 단독 소자, 10kΩ 분압 회로 구성
 *   ESP8266 ESP-01 : VCC / GND / CH_PD / RST / IO0 / IO2 / TXD / RXD
 *
 *  [Arduino 핀 할당]
 *   D2  ← ESP TXD   (직결, 3.3V→5V 허용)
 *   D3  → ESP RXD   (10kΩ+10kΩ 분압 경유, 5V→2.5V)
 *   D4  ← DHT11 DATA
 *   A0  ← LDR 신호  (10kΩ 분압 회로)
 *   A1  ← Sound AO
 *
 *  [임계값 초과 시에만 UDP WiFi 전송]
 * ============================================================
 */

#include <SoftwareSerial.h>
#include <DHT.h>

// ── WiFi 설정 ──────────────────────────────────────────
const char* WIFI_SSID = "3F_302";
const char* WIFI_PASS = "0424719222!!";
const char* DEST_IP   = "192.168.0.133";
const int   DEST_PORT = 5000;

// ── 핀 정의 ───────────────────────────────────────────
#define DHT_PIN    4
#define DHT_TYPE   DHT11

#define LDR_PIN    A0
#define SOUND_PIN  A1

// ESP8266 SoftwareSerial (RX=D2 ← ESP TXD, TX=D3 → ESP RXD)
SoftwareSerial espSerial(2, 3);

// ── 임계값 ────────────────────────────────────────────
#define THRESH_TEMP    30.0f
#define THRESH_HUMID   70.0f
#define THRESH_DARK    300
#define THRESH_SOUND   550

// ── 전역 변수 ─────────────────────────────────────────
DHT dht(DHT_PIN, DHT_TYPE);
bool wifiReady = false;
unsigned long lastMs = 0;
const unsigned long INTERVAL = 2000;

// ── 함수 선언 ─────────────────────────────────────────
bool  atCmd(const char* cmd, const char* expect, unsigned long timeout = 3000);
void  espFlush();
bool  wifiInit();
bool  udpSend(const char* data);
void  printSerial(float t, float h, int ldr, int snd, bool* alr);

// ══════════════════════════════════════════════════════
void setup() {
  Serial.begin(9600);
  espSerial.begin(9600);
  dht.begin();

  Serial.println(F("================================"));
  Serial.println(F("  BOARD2  위험 감지 시스템 v1.0 "));
  Serial.println(F("================================"));
  Serial.println(F("[INIT] 센서 초기화 완료"));

  // Board1과 동일한 WiFi 초기화 함수 호출
  wifiReady = wifiInit();

  if (wifiReady) {
    Serial.println(F("[WiFi] Connected!"));
  } else {
    Serial.println(F("[WiFi] FAILED — check wiring/SSID/PW"));
  }

  Serial.println(F("--- 임계값 ---"));
  Serial.print(F(" 온도  > ")); Serial.print(THRESH_TEMP);  Serial.println(F(" C"));
  Serial.print(F(" 습도  > ")); Serial.print(THRESH_HUMID); Serial.println(F(" %"));
  Serial.print(F(" 조도  < ")); Serial.print(THRESH_DARK);  Serial.println(F(" /1023"));
  Serial.print(F(" 소음  > ")); Serial.print(THRESH_SOUND); Serial.println(F(" /1023"));
  Serial.println(F("================================\n"));
}

// ══════════════════════════════════════════════════════
void loop() {
  if (millis() - lastMs < INTERVAL) return;
  lastMs = millis();

  float temp  = dht.readTemperature();
  float humid = dht.readHumidity();
  int   ldr   = analogRead(LDR_PIN);
  int   snd   = analogRead(SOUND_PIN);

  if (isnan(temp) || isnan(humid)) {
    Serial.println(F("[ERR] DHT11 읽기 실패"));
    return;
  }

  bool alert =
      (temp  > THRESH_TEMP) ||
      (humid > THRESH_HUMID) ||
      (ldr   < THRESH_DARK) ||
      (snd   > THRESH_SOUND);

  // 한 줄 출력
  Serial.print("B2,");
  Serial.print("T:");
  Serial.print(temp, 1);
  Serial.print(" H:");
  Serial.print(humid, 1);
  Serial.print(" L:");
  Serial.print(ldr);
  Serial.print(" S:");
  Serial.print(snd);
  Serial.print(" LV:");
  Serial.println(alert ? "ALERT" : "SAFE");

  // UDP로 항상 전송
  if (wifiReady) {
    char buf[80];
    snprintf(buf, sizeof(buf),
      "B2,T:%.1f H:%.1f L:%d S:%d LV:%s", //Temp온도 Humid습도 ldr포토센서 snd사운드
      temp, humid, ldr, snd,
      alert ? "ALERT" : "SAFE"
    );

    udpSend(buf);
  }
}

// ══════════════════════════════════════════════════════
//  ESP8266 AT 명령 처리 — Board1과 완전 동일
// ══════════════════════════════════════════════════════

void espFlush() {
  unsigned long t = millis();
  while (millis() - t < 200) {
    while (espSerial.available()) espSerial.read();
  }
}

bool atCmd(const char* cmd, const char* expect, unsigned long timeout) {
  espFlush();
  espSerial.println(cmd);
  Serial.print("[AT] >> "); Serial.println(cmd);

  String resp = "";
  unsigned long start = millis();
  while (millis() - start < timeout) {
    while (espSerial.available()) {
      char c = espSerial.read();
      resp += c;
    }
    if (resp.indexOf(expect) != -1) {
      Serial.print("[AT] << "); Serial.println(resp);
      return true;
    }
  }
  Serial.print("[AT] TIMEOUT — got: "); Serial.println(resp);
  return false;
}

bool wifiInit() {
  delay(3000);  // ESP8266 부팅 대기

  // 1) 모듈 응답 확인
  if (!atCmd("AT", "OK", 3000)) return false;

  // 2) Echo off
  atCmd("ATE0", "OK", 2000);

  // 3) Station 모드
  if (!atCmd("AT+CWMODE=1", "OK", 2000) &&
      !atCmd("AT+CWMODE=1", "no change", 2000))
    return false;

  // 4) WiFi 연결 (최대 15초)
  char joinCmd[80];
  snprintf(joinCmd, sizeof(joinCmd), "AT+CWJAP=\"%s\",\"%s\"", WIFI_SSID, WIFI_PASS);
  if (!atCmd(joinCmd, "OK", 15000)) return false;
  atCmd("AT+CIFSR", "OK", 3000);

  // 5) UDP 소켓 열기
  char udpCmd[80];
  snprintf(udpCmd, sizeof(udpCmd),
    "AT+CIPSTART=\"UDP\",\"%s\",%d", DEST_IP, DEST_PORT);
  if (!atCmd(udpCmd, "OK", 5000) &&
    !atCmd(udpCmd, "ALREADY CONNECT", 2000) &&
    !atCmd(udpCmd, "CONNECT", 2000)) return false;

  return true;
}

bool udpSend(const char* data) {
  int len = strlen(data) + 2;  // \r\n 포함

  char sendCmd[30];
  snprintf(sendCmd, sizeof(sendCmd), "AT+CIPSEND=%d", len);

  espSerial.println(sendCmd);
  Serial.print("[AT] >> "); Serial.println(sendCmd);

  // '>' 프롬프트 대기
  unsigned long start = millis();
  bool gotPrompt = false;
  String resp = "";

  while (millis() - start < 3000) {
    while (espSerial.available()) {
      char c = espSerial.read();
      resp += c;
      if (c == '>') {
        gotPrompt = true;
        break;
      }
    }
    if (gotPrompt) break;
  }

  if (!gotPrompt) {
    Serial.println("[AT] No '>' prompt");
    return false;
  }

  // 데이터 전송
  espSerial.println(data);

  // SEND OK 대기 (추가 AT 명령 보내지 않음)
  resp = "";
  start = millis();
  while (millis() - start < 3000) {
    while (espSerial.available()) {
      char c = espSerial.read();
      resp += c;
    }
    if (resp.indexOf("SEND OK") != -1) {
      Serial.println("[AT] SEND OK");
      return true;
    }
    if (resp.indexOf("ERROR") != -1) {
      Serial.println("[AT] SEND ERROR");
      return false;
    }
  }

  Serial.print("[AT] SEND TIMEOUT — got: ");
  Serial.println(resp);
  return false;
}

// ══════════════════════════════════════════════════════
//  시리얼 모니터 출력
// ══════════════════════════════════════════════════════
void printSerial(float t, float h, int ldr, int snd, bool* alr) {

  // 전체 위험 여부 판단
  bool anyAlert = alr[0] || alr[1] || alr[2] || alr[3];

  Serial.print(F("T:"));
  Serial.print(t, 1);
  Serial.print(F("  H:"));
  Serial.print(h, 1);
  Serial.print(F("  L:"));
  Serial.print(ldr);
  Serial.print(F("  S:"));
  Serial.print(snd);
  Serial.print(F("  |  LV: "));

  if (anyAlert) {
    Serial.println(F("ALERT"));
  } else {
    Serial.println(F("SAFE"));
  }
}