10.11 실전 예제: IoT 단말 노드 구축

10.11 실전 예제: IoT 단말 노드 구축

10장의 마지막 관문이다. 모든 이론, 포팅, 튜닝이 통합된 완성형 프로젝트를 세팅한다.
현존하는 가장 널리 쓰이는 두 개의 칩셋(STM32와 ESP32)을 활용하여 “어떤 산업 현장에서든 당장 팔아먹을 수 있는” 규격의 노드를 구축한다.

이 장은 단순한 예제 코드가 아니다. 인터넷이 수시로 끊기고 다른 센서들이 버스의 대역폭을 가득 채우는 적대적인 환경에서 칩이 스스로 살아남아 클라우드에 생존 보고를 날리는 “IoT 생존 시나리오 런북” 이다.

1. STM32 기반의 다중 센서 데이터 퍼블리셔(Publisher) 제작

가장 기본적인 “데이터 수집기” 형태다. STM32G4 칩셋이 보드에 박혀있고, 이 칩은 인터넷이 되는 이더넷 칩(예: W5500)과 SPI 통신을 맺어 외부 라우터와 소통하는 상황을 가정한다.

1.0.1 [Runbook] 다채널 텔레메트리 멀티플렉싱 전술

온도, 습도, 진동 등 성격과 전송 주기가 다른 센서 3개를 한 칩에서 다스려야 한다.

#include <zenoh-pico.h>

// [센서 라인업 정리]
z_owned_keyexpr_t key_temp, key_hum, key_vib;
z_publisher_t pub_temp, pub_hum, pub_vib;

void setup_multi_sensors(z_session_t *session) {
    key_temp = z_keyexpr_new("factory/cnc/01/temperature");
    key_hum  = z_keyexpr_new("factory/cnc/01/humidity");
    key_vib  = z_keyexpr_new("factory/cnc/01/vibration"); // 제일 중요한 데이터

    // 일반 센서는 Best-Effort (유실 허용)
    z_publisher_options_t opts = z_publisher_options_default();
    opts.reliability = Z_RELIABILITY_BEST_EFFORT;
    pub_temp = z_declare_publisher(z_loan(*session), z_loan(key_temp), &opts);
    pub_hum  = z_declare_publisher(z_loan(*session), z_loan(key_hum), &opts);

    // 진동 센서(Vibration)는 기계 고장을 막는 최우선 데이터이므로 Reliable 세팅!
    opts.reliability = Z_RELIABILITY_RELIABLE;
    pub_vib = z_declare_publisher(z_loan(*session), z_loan(key_vib), &opts);
}

void loop_sensors() {
    uint32_t tick = HAL_GetTick();
    char buf[16];

    // 1. 온/습도는 1초(1000ms) 단위로 천천히 발송
    if (tick % 1000 == 0) {
        int t = read_temp();
        int len = sprintf(buf, "%d", t);
        z_publisher_put(&pub_temp, (uint8_t*)buf, len, NULL);
        
        int h = read_humidity();
        len = sprintf(buf, "%d", h);
        z_publisher_put(&pub_hum, (uint8_t*)buf, len, NULL);
    }

    // 2. 진동 센서는 10ms(100Hz) 단위로 무자비하게 쏜다!
    // (메인 루프의 스톨(Stall)을 막기 위해 내부적으로 캐시 포인터만 던짐)
    if (tick % 10 == 0) {
        uint8_t* vib_raw_data = read_vibration_dma();
        z_publisher_put(&pub_vib, vib_raw_data, 128, NULL);
    }
}

이처럼 하나의 세션(Session) 하에 성격이 다른 다수의 토픽들을 완전히 격리시켜 발송 주기를 제어하는 것. 이것이 멀티 쓰레드를 쓰지 않고도 칩이 수십 개의 센서를 감당하게 하는 기법이다.

2. ESP32를 활용한 엣지 라우터 연동 및 원격 제어 수신 노드 개발

“스마트 전구” 를 만들어보겠다. 클라우드에서 전 세계의 수만 대 전구를 즉각적으로 통제(cmd/light)하고, 전구는 자신의 현재 색상값 상태(status/color)를 1분마다 보고해야 한다.

2.0.1 [Runbook] ESP32 양방향 통제 펌웨어 전술

ESP32 의 넉넉한 RAM (수백 KB) 을 활용해, Queryable (원격상태조회) 과 Subscriber (원격명령수신) 를 모두 박아넣는다.

#include "freertos/FreeRTOS.h"
#include <zenoh-pico.h>

extern z_session_t session;
int current_light_brightness = 0;

// [명령 하달 라인] - 클라우드가 "불 켜(100)" 강제 명령을 내릴 때
void cmd_brightness_callback(const z_sample_t *s, void *ctx) {
    char val_str[10];
    memcpy(val_str, s->payload.start, s->payload.len);
    val_str[s->payload.len] = '\0';
    
    current_light_brightness = atoi(val_str);
    HW_Set_PWM(current_light_brightness); // 실제 하드웨어 전구 밝기 조절
    
    printf("💡 전구 밝기 강제 세팅: %d\n", current_light_brightness);
}

// [상태 조회 라인] - 클라우드가 "지금 불 밝기 몇이냐?" 고 물어볼 때
void query_status_callback(const z_query_t *query, void *ctx) {
    char reply[10];
    int len = sprintf(reply, "%d", current_light_brightness);
    
    z_owned_keyexpr_t k = z_keyexpr_clone(z_query_keyexpr(query));
    z_query_reply_options_t opts = z_query_reply_options_default();
    
    // 내 하드웨어 상태를 답변(Reply) 으로 라우터에게 반송한다
    z_query_reply(query, z_loan(k), (const uint8_t*)reply, len, &opts);
    z_drop(z_move(k));
}

void setup_smart_bulb() {
    // 1. 구독자 생성
    z_owned_keyexpr_t cmd_key = z_keyexpr_new("room/101/bulb/cmd");
    z_owned_closure_sample_t sub_cb;
    z_closure(&sub_cb, cmd_brightness_callback, NULL, NULL);
    z_declare_subscriber(z_loan(session), z_loan(cmd_key), z_move(sub_cb), NULL);
    
    // 2. 쿼리어블 생성 (대답 대기조)
    z_owned_keyexpr_t q_key = z_keyexpr_new("room/101/bulb/status");
    z_owned_closure_query_t q_cb;
    z_closure(&q_cb, query_status_callback, NULL, NULL);
    z_declare_queryable(z_loan(session), z_loan(q_key), z_move(q_cb), NULL);
}

이 코드가 들어가면 이 ESP32 스마트 전구는 절대 중앙 서버의 IP 를 알 필요가 없다.
스마트폰 앱이나 구글 홈 허브(라우터)가 로컬 망에 나타나는 즉시 Zenoh 의 Peer 탐색이 이 둘을 물리적으로 엮어버린다(Edge Routing).

3. 마이크로컨트롤러 간의 Zenoh Peer-to-Peer 통신 테스트

가장 하드코어한 시나리오. 당신은 라우터(PC/Raspberry Pi) 조차 없다. 오직 드론(ESP32) 한 대와 로봇 장갑(STM32, 리모콘) 하나뿐이다.
이 두 칩이 어떻게 중간 매개자 없이 서로 다이렉트로 Zenoh 통신(Peer-to-Peer)을 할 수 있는가?

3.0.1 [Runbook] 라우터 블라인드 데이트(Router-less Direct Connect) 전술

이것은 Pico 끼리의 연결이 아니다! (Pico는 Client 모드만 지원하기 때문).
한쪽 칩을 아예 SoftAP(공유기 역할)로 켜버리고, 그 위에서 상호 TCP 소켓을 직결시킨 다음 UDP 멀티캐스트 로 서로가 상대방의 IP를 알아챌 수 있게 스카우팅 옵션을 폭주시키는 방법이다.

1. ESP32 (로봇 본체) - SoftAP 모드 기동
자신을 공유기(“ROBOT_WIFI”) 로 만든다. 자신의 게이트웨이 주소는 192.168.4.1 이다.
그리고 Zenoh 세션을 열 때 강제로 UDP 리스너를 연다. (Pico 끼리는 보통 불가능하지만 ESP-IDF 상의 약간의 포팅 꼼수로 UDP 멀티캐스트를 허용해야 한다).

2. STM32 (컨트롤러) - 접속 및 조작 송신
STM32 와 물려있는 WiFi 칩이 “ROBOT_WIFI” 에 접속한다(IP 192.168.4.2 할당).

z_config_t config = z_config_default();
// 상대방(ESP32본체)의 IP를 락온 해버린다. Peer(동등한 위치) 로 접속 시도!
zp_config_insert_json(&config, ZP_PEER_KEY, "\"tcp/192.168.4.1:7447\""); 

z_open(&session, &config, NULL);
// 그 뒤로 PUT 을 쏘면 라우터 없이 다이렉트로 ESP32 에 명령이 꽂힌다!

이 다이렉트 통신 방식은 “Zenoh의 라우터 의존성” 을 완전히 박살 내버리고 오직 두 칩 간의 물리적 전파만으로 통신망을 완성하는, 국방이나 드론 레이싱에서 쓰는 폐쇄 로컬망의 극의다.