Luat Air700E 기반 4G 환경 모니터링 노드(온습도·기압 등) MQTT로 알리바바 IoT 플랫폼에 업로드

Air700E 기반 4G 환경 모니터링 노드(온도·습도·기압 등)를 MQTT로 알리바바 IoT 플랫폼에 업로드

소개

Air700E 4G 모듈이 센서(온습도기압 등) 데이터를 읽어 MQTT 프로토콜로 알리바바 클라우드 IoT 플랫폼에 전송하며, 동시에 0.96인치 OLED 화면에 U8g2 그래픽 라이브러리로 표시합니다.

ESP32C3 WiFi 모듈은 MQTT로 4G 노드가 올린 데이터를 구독해 LCD 화면에 LVGL로 표시합니다.

이 프로젝트는 제 수업 설계로, 대충 만들어서 완벽하지 않습니다.

과제 요구사항: 무선 통신 시스템을 설계·제작하고, 지난 학기 지식을 활용해 온·습도/초음파 거리 등을 로컬에서 수집 후 무선으로 송신하여 수신단 LCD에 표시.
⑴ 블루투스 무선 전송
⑵ WiFi 무선 전송
⑶ 지그비 무선 전송
⑷ GPRS/GSM 무선 전송
⑸ 4G 무선 전송
⑹ NB-IoT/LoRa 무선 전송
힌트: 8/16/32비트 프로세서로 메인 컨트롤러 선정, (1)(2)(3) 중 1개, (4)(5)(6) 중 1개 이상 완수.

Air700E·ESP32C3 모두 LuatOS + Lua 스크립트 방식으로 개발했습니다.
LuatOS 펌웨어는 Air700E 클라우드 빌드로 생성했습니다.
ESP32C3 기본 4MB flash를 8MB로 교체했으며, LVGL·폰트 포함으로 펌웨어 크기가 4MB를 초과합니다.

알리바바 클라우드 학생 할인: https://www.aliyun.com/daily-act/ecs/activity_share?userCode=jdjc69nf

주의: 알리바바 IoT 플랫폼은 디바이스 간 토픽 구독이 불가능하므로 메시지 전달→클라우드 제품 전달 규칙을 추가해 4G 노드가 발행한 메시지를 WiFi 노드 토픽으로 전달해야 합니다.

LCSC 오픈소스 링크: https://oshwhub.com/zeruns/wen-shi-du-cai-ji-4g-shang-chuan

전자/MCU 기술 단톡: 2169025065

Air700E 소개

Air700E는 Luat에서 출시한 LTE Cat.1 bis 통신 모듈로 EC618 칩셋 기반, LTE 3GPP Rel.13 지원. LTE TDD 밴드만 남겨 중국 이동통신 주파에 최적화, 초소형·초저가로 소형·비용 민감 제품에 적합.

주요 특징

  • 1.8/3.3V USIM
  • 1.8/3.3V 설정 가능 UART
  • USB 2.0
  • OTA 펌웨어 업그레이드
  • PSM 디지털 음성 인터페이스
  • USB 넷동/AT/LuatOS/C-SDK 등 다양한 개발 방식 지원

Air700E는 풍부한 네트워크 프로토콜·산업 표준 인터페이스를 내장, Windows/Linux/Android 드라이버 지원으로 CPE·라우터·데이터카드·태블릿·차량·보안·산업 PDA 등 M2M 응용 확장 가능.

기술 사양
LTE-TDD 밴드: B34/B38/B39/B40/B41
LTE-TDD 속도: DL 8Mbps(配比2) / 6Mbps(配比1), UL 2Mbps(配比2) / 4Mbps(配比1)
인터페이스: USB 2.0 480Mbps, (U)SIM, NETLIGHT, I2S, 3×UART, PWRKEY, 2×ADC, 13×GPIO+2×인터럽트, I2C

ESP32-C3 소개

CORE ESP32-C3 코어 보드는 21mm×51mm 초소형 스탬프홀 설계. UART/GPIO/SPI/I2C/ADC/PWM 등 인터페이스 지원.

하드웨어 리소스

  • 21mm×51mm
  • 4MB SPI FLASH(최대 16MB 지원)
  • 2×UART(UART0=다운로드)
  • 5채널 12비트 ADC, 최대 100KSPS
  • 1×SPI 마스터
  • 1×I2C
  • 4×PWM(임의 GPIO)
  • 15×GPIO(멀티플렉스)
  • 2×LED, 1×리셋 + 1×BOOT 키
  • USB-TTL 다운로드/디버그
  • 2.4GHz PCB 안테나

실물 사진

데모 영상: https://www.bilibili.com/video/BV1MH4y1B7Df/

전체 사진

4G 노드

WiFi 노드

WiFi 연결 중:

WiFi 연결 후 MQTT·NTP 동기화:

4G 노드 데이터 표시:

온습도 변화 곡선:

IoT 플랫폼

알리바바 IoT 플랫폼 데이터:

회로도

4G 노드

WiFi 노드

PCB

4G 노드

4G 모듈 패키지가 정확하지 않습니다.

Top:

Bottom:

WiFi 노드

Top:

Bottom:

코드

코드 다운로드 방법은 공식 문서를 참고하세요.

Air700E 문서: https://doc.openluat.com/wiki/44?wiki_page_id=4730
LVGL for LuatOS 매뉴얼: https://url.zeruns.com/7z7fN
ESP32-C3 문서: https://url.zeruns.com/497AP
Lua 튜토리얼: https://url.zeruns.com/Pc4PA

4G 노드

펌웨어 다운로드: https://url.zeruns.com/2G3K7

main.lua:

-- LuaTools는 PROJECT, VERSION 정보가 필요합니다
PROJECT = "환경 모니터링 노드"
VERSION = "1.1.0"

-- 필수 라이브러리 로드
sys = require("sys")
aht10 = require "aht10"
bmx = require "bmx"

-- 하드웨어 워치독
if wdt then
    wdt.init(9000)          -- 9초 설정
    sys.timerLoopStart(wdt.feed, 3000) -- 3초마다 감시견 피딩
end

print(_VERSION)

log.info(mobile.ipv6(true)) -- IPv6 활성화

local ProductKey = "xxxxxx"
local DeviceName = "xxxxxx"
local DeviceSecret = "xxxxxx"
local client_id, user_name, password = iotauth.aliyun(ProductKey, DeviceName, DeviceSecret)
log.info("MQTT 파라미터", client_id, user_name, password)

local softI2C = i2c.createSoft(29, 31, 2)
local aht10_data, bmx_data
local RH, Temp, Press, High, distance, BAT_Voltage, CPU_T = 0,0,0,0,0,0,0
local RH_C, Temp_C, Press_C, High_C, distance_C, BAT_Voltage_C, CPU_T_C = 0,0,0,0,0,0,0
local AHT10_flag, BMP180_flag, US100_flag, BAT_Voltage_flag, CPU_T_flag = false,false,false,false,false
all_data = {params = {CurrentVoltage = 0, CurrentTemperature = 0, CurrentHumidity = 0, Atmosphere = 0, Altitude = 0, DetectDistance = 0, CPUTemperature = 0}}

function US_100()   -- US-100 초음파 거리 측정
    uart.write(1, string.char(0x55))
    sys.wait(50)
    local hData, lData = string.byte(uart.read(1, 2), 1, 2)
    uart.rxClear(1)
    if hData and lData then
        local Distance_mm = tonumber((hData * 256) + lData)
        if Distance_mm > 4500 then Distance_mm = 4500 end
        return (Distance_mm / 10)
    end
end

sys.taskInit(function() -- 센서 데이터 읽기 스레드
    sys.wait(500)
    aht10.init(softI2C)
    bmx.init(softI2C)
    uart.setup(1, 9600, 8, 1, uart.NONE)
    adc.open(0)
    adc.open(adc.CH_CPU)
    sys.wait(500)

    local RH_sum, Temp_sum, Press_sum, High_sum, distance_sum, BAT_Voltage_sum, CPU_T_sum = 0,0,0,0,0,0,0
    local avg_count, avg_count2, avg_count3, avg_count4, avg_count5 = 0,0,0,0,0

    while 1 do
        aht10_data = aht10.get_data()
        bmx_data = bmx.get_data()
        distance_C = US_100()
        BAT_Voltage_C = ((adc.get(0) / 1000 / 1007) * 1473)
        CPU_T_C = adc.get(adc.CH_CPU) / 1000

        -- AHT10 평균 계산
        if aht10_data.RH and aht10_data.T then
            all_data.params.CurrentTemperature = aht10_data.T
            all_data.params.CurrentHumidity = aht10_data.RH * 100
            if avg_count < 6 then
                RH_C, Temp_C = aht10_data.RH * 100, aht10_data.T
                RH_sum = RH_C + RH_sum
                Temp_sum = Temp_C + Temp_sum
                avg_count = avg_count + 1
            elseif avg_count == 6 then
                RH = RH_sum / 6
                Temp = Temp_sum / 6
                log.info("AHT10_data.RH: "..(string.format("%.2f", RH)).." %", " AHT10_data.T: "..(string.format("%.2f", Temp)).." ℃")
                AHT10_flag = true
                sys.publish("MQTT_P")
                avg_count, RH_sum, Temp_sum = 0,0,0
            end
        end
```--BMP180 기압 및 고도 평균 계산
        if bmx_data.press and bmx_data.high then
            all_data.params.Atmosphere = bmx_data.press
            all_data.params.Altitude = bmx_data.high
            if avg_count2 < 10 then       -- 10 미만인지 판단하여 평균 누적
                Press_C,High_C = bmx_data.press, bmx_data.high  -- 현재 기압과 고도 값 읽기
                Press_sum = Press_C + Press_sum
                High_sum = High_C + High_sum
                avg_count2 = avg_count2 + 1
            elseif avg_count2 == 10 then
                Press = Press_sum / 10
                High = High_sum / 10
                log.info("BMP180_data.press: "..(string.format("%.2f", Press)).." hPa"," BMP180_data.high: "..(string.format("%.2f", High)).." m")
                BMP180_flag = true
                sys.publish("MQTT_P")
                avg_count2,Press_sum,High_sum=0,0,0
            end
        end

        -- US-100 초음파 거리 측정 거리 평균 계산
        if distance_C then    -- 데이터를 읽었는지 판단
            all_data.params.DetectDistance = distance_C
            if avg_count3 < 3 then
                distance_sum = distance_C + distance_sum
                avg_count3 = avg_count3 + 1
            elseif avg_count3 == 3 then
                distance = distance_sum / 3
                log.info("US-100: "..(string.format("%.1f", distance)).." cm")
                US100_flag = true
                sys.publish("MQTT_P")
                avg_count3,distance_sum = 0,0
            end
        end

        -- 배터리 전압
        if BAT_Voltage_C then    -- 데이터를 읽었는지 판단
            all_data.params.CurrentVoltage = BAT_Voltage_C
            if avg_count4 < 20 then
                BAT_Voltage_sum = BAT_Voltage_C + BAT_Voltage_sum
                avg_count4 = avg_count4 + 1
            elseif avg_count4 == 20 then
                BAT_Voltage = BAT_Voltage_sum / 20
                log.info("BAT_Voltage: "..(string.format("%.2f", BAT_Voltage)).." V")
                BAT_Voltage_flag = true
                sys.publish("MQTT_P")
                avg_count4,BAT_Voltage_sum = 0,0
            end
        end

        -- CPU 온도
        if CPU_T_C then    -- 데이터를 읽었는지 판단
            all_data.params.CPUTemperature = CPU_T_C
            if avg_count5 < 20 then
                CPU_T_sum = CPU_T_C + CPU_T_sum
                avg_count5 = avg_count4 + 1
            elseif avg_count5 == 20 then
                CPU_T = CPU_T_sum / 20
                log.info("CPU_T: "..(string.format("%.2f", CPU_T)).." ℃")
                CPU_T_flag = true
                sys.publish("MQTT_P")
                avg_count5,CPU_T_sum = 0,0
            end
        end

        sys.wait(500)
    end
end)

--[[sys.taskInit(function() -- 초음파 거리 측정 작업 생성, 1.5초마다
    uart.setup(1, 9600, 8, 1, uart.NONE)    -- 시리얼 포트 1 초기화
    sys.wait(1000)
    while 1 do
        distance = US_100() -- US-100 데이터 읽기
        if distance then    -- 데이터를 읽었는지 판단
            log.info("US-100: "..(string.format("%.1f", distance)).." cm")
            US100_flag = true
            sys.publish("MQTT_P")
        end
        sys.wait(1500)
    end
end)]]

sys.taskInit(function() -- MQTT 초기화 및 데이터 보고 스레드 생성
    mqttc = mqtt.create(nil, "a1sJbDQiEqr.iot-as-mqtt.cn-shanghai.aliyuncs.com", 1883,true,true)
    mqttc:auth(client_id, user_name, password)
    mqttc:keepalive(120) -- 기본값 240s
    mqttc:autoreconn(true, 3000) -- 자동 재연결 메커니즘
    mqttc:on(function(mqtt_client, event, data, payload)
        if event == "conack" then
            sys.publish("mqtt_conack")
            log.info("mqtt", "mqtt 연결됨")
            mqtt_client:subscribe("/sys/"..ProductKey.."/"..DeviceName.."/thing/service/property/set")
        elseif event == "recv" then
            log.info("mqtt", "메시지 수신", data, payload)
            local mqtt_date = json.decode(payload)
        elseif event == "sent" then
            log.info("mqtt", "sent", "pkgid", data)
        end
    end)
    mqttc:connect()
    while true do
        sys.waitUntil("MQTT_P", 3000)
        if AHT10_flag then
            local json_str = json.encode({params = {CurrentTemperature = Temp, CurrentHumidity = RH}}, "2f") -- JSON 형식으로 변환, 소수점 2자리 유지
            mqttc:publish("/sys/"..ProductKey.."/".. DeviceName.."/thing/event/property/post", json_str)  -- MQTT 속성 보고
            mqttc:publish("/"..ProductKey.."/".. DeviceName.."/user/update", json.encode(all_data,"2f"))  -- MQTT 속성 보고
            AHT10_flag = false
        end
        if BMP180_flag then
            local json_str = json.encode({params = {Atmosphere = Press, Altitude = High}}, "2f") -- JSON 형식으로 변환, 소수점 2자리 유지
            mqttc:publish("/sys/"..ProductKey.."/"..DeviceName.."/thing/event/property/post", json_str)  -- MQTT 속성 보고
            mqttc:publish("/"..ProductKey.."/".. DeviceName.."/user/update", json.encode(all_data,"2f"))  -- MQTT 속성 보고
            BMP180_flag = false
        end
        if US100_flag then
            local json_str = json.encode({params = {DetectDistance = distance}}, "1f") -- JSON 형식으로 변환, 소수점 2자리 유지
            mqttc:publish("/sys/"..ProductKey.."/"..DeviceName.."/thing/event/property/post", json_str)  -- MQTT 속성 보고
            mqttc:publish("/"..ProductKey.."/".. DeviceName.."/user/update", json.encode(all_data,"2f"))  -- MQTT 속성 보고
            US100_flag = false
        end
        if BAT_Voltage_flag then
            local json_str = json.encode({params = {CurrentVoltage = BAT_Voltage}}, "2f") -- JSON 형식으로 변환, 소수점 2자리 유지
            mqttc:publish("/sys/"..ProductKey.."/"..DeviceName.."/thing/event/property/post", json_str)  -- MQTT 속성 보고
            mqttc:publish("/"..ProductKey.."/".. DeviceName.."/user/update", json.encode(all_data,"2f"))  -- MQTT 속성 보고
            BAT_Voltage_flag = false
        end
        if CPU_T_flag then
            local json_str = json.encode({params = {CPUTemperature = CPU_T}}, "2f") -- JSON 형식으로 변환
            mqttc:publish("/sys/"..ProductKey.."/"..DeviceName.."/thing/event/property/post", json_str)  -- MQTT 속성 보고
            mqttc:publish("/"..ProductKey.."/".. DeviceName.."/user/update", json.encode(all_data,"2f"))  -- MQTT 속성 보고
            CPU_T_flag = false
        end
    end
end)


-- https://blog.zeruns.com
sys.taskInit(function()
    u8g2.begin({ic = "ssd1306",direction = 0,mode="i2c_hw",i2c_id=1,i2c_speed = i2c.FAST}) -- direction 선택 가능 0 90 180 270
    u8g2.ClearBuffer()  -- 화면 지우기
    u8g2.SetFont(u8g2.font_opposansm10)  -- 글꼴 전환
    u8g2.DrawUTF8("IMEI:", 0, 16)
    u8g2.DrawUTF8(mobile.imei(), 0, 32)
    u8g2.SendBuffer()

    --ntp 시간 동기화
    --socket.sntp()
    socket.sntp({"ntp.aliyun.com","ntp1.aliyun.com","ntp2.aliyun.com"}) -- sntp 사용자 지정 서버 주소
    --socket.sntp(nil, socket.ETH0) -- sntp 사용자 지정 어댑터 번호
    sys.subscribe("NTP_UPDATE", function()
        log.info("sntp", "time", os.date())
        sys.publish("NTP_OK")
    end)
    sys.subscribe("NTP_ERROR", function()
        log.info("socket", "sntp error")
        socket.sntp()
    end)

    sys.wait(1000)

    sys.waitUntil("NTP_OK", 3000)
    u8g2.ClearBuffer()  -- 화면 지우기
    u8g2.SetFont(u8g2.font_opposansm10)  -- 글꼴 전환
    u8g2.DrawUTF8(os.date("%Y-%m-%d"), 0, 16) -- 시간 표시
    u8g2.DrawUTF8(os.date("%H:%M:%S"), 0, 32) -- 시간 표시
    local IP = socket.localIP()     -- IP 주소 표시
    if IP then
        u8g2.DrawUTF8("IP:"..IP, 0, 63)
    end
    u8g2.SendBuffer()
    sys.wait(2500)

    while true do
        u8g2.ClearBuffer()  -- 화면 지우기
        u8g2.SetFont(u8g2.font_sarasa_m10_ascii)
        if Temp_C then
            u8g2.DrawUTF8("T:"..(string.format("%.2f", Temp_C)).."°C", 0, 16)
        end
        if RH_C then
            u8g2.DrawUTF8("RH:"..(string.format("%.2f", RH_C)).."%", 0, 32)
        end
        if Press_C then
            u8g2.DrawUTF8("P:"..(string.format("%.2f", Press_C)).."hPa", 0, 48)
        end
        if distance_C then
            u8g2.DrawUTF8("D:"..(string.format("%.1f", distance_C)).."cm", 0, 63)
        end
        if High_C then
            u8g2.DrawUTF8("H:"..(string.format("%.2f", High_C)).."m", 61, 16)
        end
        if BAT_Voltage_C then
            u8g2.DrawUTF8("B:"..(string.format("%.2f", BAT_Voltage_C)).."V", 66, 32)
        end
        if CPU_T_C then
            u8g2.DrawUTF8("CPUT:"..(string.format("%d", CPU_T_C)).."°C", 63, 63)
        end
        u8g2.SendBuffer()   -- 화면에 데이터 업데이트 표시
        sys.wait(200)
    end
end)sys.taskInit(function() --스레드 생성, 500ms마다 LED 깜빡임
    gpio.setup(27, 0)   -- gpio27을 출력으로 설정, 초기 레벨은 LOW, 하드웨어 기본 풀업/풀다운 사용
    while true do
        gpio.toggle(27) -- gpio 레벨 토글
        sys.wait(500)
    end
end)

--[[
    지원 글꼴
    ["unifont_t_symbols","open_iconic_weather_6x_t","opposansm8","opposansm10","opposansm12","opposansm16","opposansm20",
    "opposansm24","opposansm32","sarasa_m8_ascii","sarasa_m10_ascii","sarasa_m12_ascii","sarasa_m14_ascii",
    "sarasa_m16_ascii","sarasa_m18_ascii","sarasa_m20_ascii","sarasa_m22_ascii"]
]]
-- 사용자 코드 종료---------------------------------------------
-- 항상 이 문장으로 끝남
sys.run()
-- sys.run() 이후에는 어떤 문장도 추가하지 마세요!!!!!

### WiFi 노드

펌웨어 다운로드 주소: [https://url.zeruns.com/a7eXJ](https://url.zeruns.com/a7eXJ)

**main.lua 파일:**

```lua
-- LuaTools는 PROJECT와 VERSION 정보가 필요합니다
PROJECT = "데이터 표시 노드"
VERSION = "1.0.5"

-- sys 라이브러리는 필수
_G.sys = require("sys")
require("sysplus")

-- 하드웨어 워치독 추가로 프로그램 정지 방지
wdt.init(9000)-- 워치독 초기화, 6s로 설정
wdt_timer_id = sys.timerLoopStart(function()
    wdt.feed()
end, 2000)  -- 2s마다 워치독 피드

local ProductKey= "xxxxxx"                         -- 제품 키
local DeviceName= "xxxxxx"                -- 자신의 장치 이름으로 변경
local DeviceSecret = "xxxxxx" -- 자신의 장비 시크릿으로 변경
local client_id, user_name, password = iotauth.aliyun(ProductKey, DeviceName ,DeviceSecret) -- MQTT 3요소 생성
log.info("MQTT 매개변수", client_id, user_name, password)

local local_CPU_T_C = 0;
local all_data = {params = {CurrentVoltage = 0,CurrentTemperature = 0, CurrentHumidity = 0,
Atmosphere = 0, Altitude = 0,DetectDistance = 0,CPUTemperature = 0}};
local key1_flag,key2_flag = false,false

sys.taskInit(function() -- 스레드 생성
    
    log.info("wlan", "wlan_init:", wlan.init()) -- wlan 초기화
    wlan.setMode(wlan.STATION)                  -- wlan 모드를 STATION으로 설정
    wlan.connect("Mate 40 Pro", "123456789", 1)     -- wlan 네트워크 연결
    local result, data = sys.waitUntil("IP_READY",6000)-- IP_READY 이벤트 대기
    log.info("wlan", "IP_READY", result, data)  -- IP_READY 이벤트 결과 및 데이터 출력
    if result then        
        socket.setDNS(socket.STA, 1, "114.114.114.114")    
        log.info("wlan", "info", json.encode(wlan.getInfo()))   -- wlan 네트워크 상세 정보 출력
        sys.publish("WiFi_connected_OK",data)   -- 메시지 발행, WiFi 연결 성공
        --ntp 시간 동기화
        --socket.sntp()
        socket.sntp({"ntp.aliyun.com","time.windows.com","ntp.tencent.com","ntp2.aliyun.com"}, socket.STA) --sntp 사용자 지정 서버 주소
        --socket.sntp(nil, socket.STA) --sntp 사용자 지정 어댑터 번호
        -- NTP_UPDATE 이벤트 구독
        sys.subscribe("NTP_UPDATE", function()
            sys.publish("NTP_OK")   -- 메시지 발행, NTP 시간 동기화 성공
            log.info("sntp", "time", os.date()) -- 시스템 시간 출력
        end)
        sys.subscribe("NTP_ERROR", function()
            log.info("socket", "sntp error")
            socket.sntp()
        end)
    else
        while true do
            sys.wait(1000)
        end
    end

    adc.open(adc.CH_CPU)-- ADC 채널 열기-CPU 내부 온도 채널
    local CH_CPU = adc.get(adc.CH_CPU) / 1000    -- CPU 온도 읽기
    if CH_CPU then  -- 데이터 유효성 판단
        local_CPU_T_C = CH_CPU
    end

    mqttc = mqtt.create(nil, "a1sJbDQiEqr.iot-as-mqtt.cn-shanghai.aliyuncs.com", 1883,true)    -- MQTT 클라이언트 생성
    mqttc:auth(client_id, user_name, password)  -- MQTT 인증 수행
    mqttc:keepalive(60)                        -- MQTT keepalive 시간 설정, 기본값 60초
    mqttc:autoreconn(true, 3000)                -- 자동 재연결 메커니즘 활성화
    mqttc:on(function(mqtt_client, event, data, payload)-- MQTT 이벤트 콜백 함수 정의
        if event == "conack" then               -- 연결 확인 이벤트(conack) 수신 시
            sys.publish("mqtt_conack")          -- mqtt_conack 메시지 발행
            log.info("mqtt", "mqtt 연결됨")       -- 연결 성공 정보 출력
            mqtt_client:subscribe("/sys/"..ProductKey.."/"..DeviceName.."/thing/service/property/set")  -- 지정 토픽 구독
            mqtt_client:subscribe("/"..ProductKey.."/"..DeviceName.."/user/get")
        elseif event == "recv" then             -- 메시지 수신 이벤트(recv) 시
            if data == "/"..ProductKey.."/"..DeviceName.."/user/get" then   -- 수신 토픽 판단
                all_data = json.decode(payload)             -- 메시지 내용을 JSON 형식으로 파싱
                mqttc:publish("/sys/"..ProductKey.."/"..DeviceName.."/thing/event/property/post", payload)  -- MQTT 속성 보고
            else
                log.info("mqtt", "메시지 수신", data, payload) -- 수신 메시지 정보 출력
                local mqtt_date = json.decode(payload)      -- 메시지 내용을 JSON 형식으로 파싱
            end
        elseif event == "sent" then             -- 메시지 발송 이벤트(sent) 시
            --log.info("mqtt", "sent", "pkgid", data)-- 발송 메시지 정보 출력
        end
    end)
    mqttc:connect()

    while true do
        local CH_CPU = adc.get(adc.CH_CPU) / 1000    -- CPU 온도 읽기
        if CH_CPU then  -- 데이터 유효성 판단
            CPU_T_C = CH_CPU
        end

        gpio.toggle(12) --gpio 레벨 토글
        sys.wait(500)
    end
end)

sys.taskInit(function() -- 스레드 생성 
    spi.setup(2, 7, 0, 0, 8, 80 * 1000 * 1000, spi.MSB, 1, 1)   -- SPI 초기화
    log.info("lcd.init", lcd.init("st7789",{
        port = 2,           -- spi 포트
        -- pin_cs = 7,         -- SPI 칩 선택
        pin_dc = 8,         -- lcd 데이터/명령 선택 핀
        -- pin_pwr = 18,       -- lcd 백라이트 핀 옵션, 설정 안 해도 됨
        pin_rst = 6,        -- lcd 리셋 핀
        direction = 2,      -- lcd 화면 방향 0:0° 1:180° 2:270° 3:90°
        w = 320,            -- lcd 수평 해상도
        h = 172,            -- lcd 수직 해상도
        xoffset = 0,        -- x 오프셋(다른 화면 ic 및 방향에 따라 다름)
        yoffset = 34;       -- y 오프셋(다른 화면 ic 및 방향에 따라 다름)
    }))

    log.info("lvgl", lvgl.init())   --- LVGL 초기화

    local style_screen_label_main = lvgl.style_create()         -- style_screen_label_main 이름의 스타일 객체 생성
    lvgl.style_set_text_font(style_screen_label_main, lvgl.STATE_DEFAULT, lvgl.font_get("opposans_m_16")) -- 라벨 기본 글꼴 설정
```local Init_scr = lvgl.obj_create(nil, nil) -- 화면 객체 생성
    lvgl.scr_load(Init_scr)  -- 화면 객체 로드하여 디스플레이에 표시
    local spinner = lvgl.spinner_create(Init_scr, nil) -- 회전 아이콘 객체 생성
    lvgl.obj_set_size(spinner, 100, 100) -- 회전 아이콘 객체 크기 설정
    lvgl.obj_align(spinner, nil, lvgl.ALIGN_CENTER, 0, -20) -- 회전 아이콘 객체 중앙 정렬
    local label_WiFiConnecting = lvgl.label_create(Init_scr, nil) -- 라벨 객체 생성
    lvgl.label_set_recolor(label_WiFiConnecting, true) -- 라벨 재색상 기능 활성화, 텍스트 변경 시 색상 자동 업데이트
    lvgl.obj_add_style(label_WiFiConnecting, lvgl.LABEL_PART_MAIN, style_screen_label_main) -- 스타일 객체를 라벨 객체의 main 부분에 할당
    lvgl.label_set_text(label_WiFiConnecting, "WiFi 연결 중...") -- 라벨 객체 텍스트 설정
    lvgl.label_set_align(label_WiFiConnecting, lvgl.LABEL_ALIGN_CENTER) -- 라벨 객체 텍스트 중앙 정렬
    lvgl.obj_align(label_WiFiConnecting, nil, lvgl.ALIGN_CENTER, 0, 46) -- 라벨 객체 중앙 정렬
    local result, ip_data = sys.waitUntil("WiFi_connected_OK", 5000)    -- WiFi 연결 성공 메시지 대기, 5초 타임아웃
    if result == false then
        lvgl.label_set_text(label_WiFiConnecting, "#ff0000 WiFi 연결 실패!#\\n기기를 재시작하거나 네트워크를 확인하세요") -- 라벨 객체 텍스트 설정 (WiFi 연결 실패 표시)
        lvgl.label_set_align(label_WiFiConnecting, lvgl.LABEL_ALIGN_CENTER) -- 라벨 객체 텍스트 중앙 정렬
        lvgl.obj_align(label_WiFiConnecting, nil, lvgl.ALIGN_CENTER, 0, 51) -- 라벨 객체 중앙 정렬
        while true do
            sys.wait(1000) -- 프로그램 진행 방지를 위한 무한 대기
        end
    end
    lvgl.label_set_text(label_WiFiConnecting, "WiFi 연결 성공!") -- 라벨 객체 텍스트 설정 (WiFi 연결 성공 표시)
    lvgl.label_set_align(label_WiFiConnecting, lvgl.LABEL_ALIGN_CENTER) -- 라벨 객체 텍스트 중앙 정렬
    lvgl.obj_align(label_WiFiConnecting, nil, lvgl.ALIGN_CENTER, 0, 51) -- 라벨 객체 중앙 정렬
    sys.wait(500) -- 500밀리초 대기
    lvgl.obj_del(label_WiFiConnecting) -- 라벨 객체 삭제
    lvgl.obj_align(spinner, nil, lvgl.ALIGN_CENTER, 0, 0) -- 회전 아이콘 객체 중앙 정렬
    local label_Init_log = lvgl.label_create(Init_scr, nil) -- 라벨 객체 생성
    lvgl.label_set_recolor(label_Init_log, true) -- 라벨 재색상 기능 활성화
    lvgl.label_set_align(label_Init_log, lvgl.LABEL_ALIGN_LEFT) -- 라벨 객체 텍스트 좌측 정렬
    lvgl.obj_align(label_Init_log, nil, lvgl.ALIGN_IN_TOP_LEFT, 10, 5) -- 라벨 객체 화면 좌상단에 정렬
    local Init_log = "#0000ff 초기화 로그: #\\nWiFi 연결 성공!\\n" -- 초기화 로그 정보
    lvgl.label_set_text(label_Init_log, Init_log) -- 라벨 객체 텍스트 설정
    Init_log = Init_log.."IP: "..ip_data.."\\nMQTT 서버 연결 중...\\n" -- 로그 정보 업데이트
    lvgl.label_set_text(label_Init_log, Init_log) -- 라벨 객체 텍스트 설정
    if sys.waitUntil("mqtt_conack", 10000) == false then
        Init_log = Init_log.."#ff0000 MQTT 서버 연결 실패!\\n기기를 재시작하세요!#\\n" -- 로그 정보 업데이트 (MQTT 연결 실패 표시)
        lvgl.label_set_text(label_Init_log, Init_log) -- 라벨 객체 텍스트 설정
        mqttc:disconnect() -- MQTT 연결 해제
        mqttc:close() -- MQTT 연결 닫기
        while true do
            sys.wait(1000) -- 프로그램 진행 방지를 위한 무한 대기
        end
    end
    Init_log = Init_log.."MQTT 서버 연결 성공!\\n" -- 로그 정보 업데이트 (MQTT 연결 성공 표시)
    lvgl.label_set_text(label_Init_log, Init_log) -- 라벨 객체 텍스트 설정
    Init_log = Init_log.."NTP 시간 동기화 중...\\n" -- 로그 정보 업데이트
    lvgl.label_set_text(label_Init_log, Init_log) -- 라벨 객체 텍스트 설정
    local result = sys.waitUntil("NTP_OK", 3500) -- NTP 시간 동기화 성공 메시지 대기, 3초 타임아웃
    if result then
        Init_log = Init_log.."NTP 시간 동기화 성공!\\n" -- 로그 정보 업데이트 (NTP 시간 동기화 성공 표시)
        lvgl.label_set_text(label_Init_log, Init_log) -- 라벨 객체 텍스트 설정
    else
        Init_log = Init_log.."NTP 시간 동기화 실패!\\n" -- 로그 정보 업데이트 (NTP 시간 동기화 실패 표시)
        lvgl.label_set_text(label_Init_log, Init_log) -- 라벨 객체 텍스트 설정
    end
    Init_log = Init_log.."현재 시간: "..(os.date("%Y-%m-%d %H:%M:%S")).."\\n" -- 로그 정보 업데이트 (현재 시간 표시)
    lvgl.label_set_text(label_Init_log, Init_log) -- 라벨 객체 텍스트 설정
    Init_log = Init_log.."시스템 초기화 완료!\\n" -- 로그 정보 업데이트 (시스템 초기화 완료 표시)
    lvgl.label_set_text(label_Init_log, Init_log) -- 라벨 객체 텍스트 설정

    sys.wait(2000)

    local alldata_scr = lvgl.obj_create(nil, nil)               -- 모든 데이터 표시용 화면 객체 생성, 부모 객체와 스타일 없음
    lvgl.scr_load(alldata_scr)                                  -- 화면 객체 로드하여 디스플레이에 표시

    local label_alldata = lvgl.label_create(alldata_scr, nil)   -- 모든 데이터 표시용 Label 객체 생성
    lvgl.label_set_recolor(label_alldata, true)                 -- 라벨 재색상 기능 활성화
    lvgl.label_set_align(label_alldata, lvgl.LABEL_ALIGN_LEFT)  -- 라벨 정렬 방식을 좌측 정렬로 설정
    lvgl.obj_align(label_alldata, nil, lvgl.ALIGN_IN_TOP_LEFT, 10, 5)   -- 라벨을 화면 좌상단에 정렬, 좌측 여백 10픽셀, 상단 여백 5픽셀
    lvgl.obj_add_style(label_alldata, lvgl.LABEL_PART_MAIN, style_screen_label_main)-- 스타일 객체를 label_alldata 라벨의 main 부분에 할당
    
    local label_time = lvgl.label_create(alldata_scr, nil)      -- 현재 시간 표시용 Label 객체 생성
    lvgl.label_set_text(label_time, os.date("%Y-%m-%d").."\\n"..os.date("%H:%M:%S"))
    lvgl.label_set_align(label_time, lvgl.LABEL_ALIGN_CENTER)   -- 라벨 정렬 방식을 중앙 정렬로 설정
    lvgl.obj_align(label_time, nil, lvgl.ALIGN_IN_TOP_RIGHT, -25, 10)-- 라벨을 화면 우상단에 정렬, 좌측 여백 10픽셀, 상단 여백 5픽셀
    lvgl.obj_add_style(label_time, lvgl.LABEL_PART_MAIN, style_screen_label_main)
    
    lvgl.obj_del(Init_scr)

    local chart_scr = lvgl.obj_create(nil, nil)               -- 차트 표시용 화면 객체 생성, 부모 객체와 스타일 없음

    local chart = lvgl.chart_create(chart_scr, nil);
    lvgl.obj_set_size(chart, 310, 150);
    lvgl.obj_align(chart, nil, lvgl.ALIGN_CENTER, 0, 0);
    lvgl.chart_set_type(chart, lvgl.CHART_TYPE_LINE);   -- 라인과 포인트도 표시
    lvgl.chart_set_point_count(chart, 20)
    lvgl.chart_set_y_range(chart, lvgl.CHART_AXIS_PRIMARY_Y, 0, 100)

    local ser1 = lvgl.chart_add_series(chart, lvgl.color_make(0xFF, 0x00, 0x00));
    local ser2 = lvgl.chart_add_series(chart, lvgl.color_make(0x00, 0x80, 0x00));
    

    -- 메모리 사용량 출력
    log.info("mem.lua", rtos.meminfo())
    log.info("mem.sys", rtos.meminfo("sys"))

    while true do
        lvgl.label_set_text(label_alldata, string.format([[#0000ff 환경 온도: %.2f°C#
        #ff00ff 환경 습도: %.2f%%#
        #ff0000 기압: %.2fhPa#
        #800080 해발 고도: %.2fm#
        #0000ff 초음파 거리 측정: %.2fcm#
        #ff00ff 원격 노드 배터리 전압: %.2fV#
        #ff0000 원격 노드 CPU 온도: %.0f°C#
        #800080 로컬 노드 CPU 온도: %.0f°C#
        ]],all_data.params.CurrentTemperature,all_data.params.CurrentHumidity,all_data.params.Atmosphere,
        all_data.params.Altitude,all_data.params.DetectDistance,all_data.params.CurrentVoltage,
        all_data.params.CPUTemperature,local_CPU_T_C)); -- 라벨 텍스트 설정
        lvgl.label_set_text(label_time, os.date("%Y-%m-%d").."\\n"..os.date("%H:%M:%S"))

        lvgl.chart_set_next(chart, ser1, all_data.params.CurrentTemperature);
        lvgl.chart_set_next(chart, ser2, all_data.params.CurrentHumidity);
        lvgl.chart_refresh(chart);

        if key1_flag then
            lvgl.scr_load(chart_scr)
            key1_flag = false
        end
        if key2_flag then
            lvgl.scr_load(alldata_scr)
            key2_flag = false
        end
        gpio.toggle(12) --gpio 레벨 토글
        sys.wait(300)
    end
end)

sys.taskInit(function() --500밀리초마다 LED 깜빡이는 스레드 생성
    gpio.setup(13, 0)   -- gpio13을 출력으로 설정, 초기 레벨은 낮음, 하드웨어 기본 풀업/풀다운 사용
    gpio.setup(12, 0)   -- gpio12를 출력으로 설정, 초기 레벨은 낮음, 하드웨어 기본 풀업/풀다운 사용
    while true do
        gpio.toggle(13) --gpio 레벨 토글
        sys.wait(500)
    end
end)

sys.taskInit(function() --키 스캔 스레드 생성
    gpio.setup(18, nil, gpio.PULLUP)
    gpio.setup(19, nil, gpio.PULLUP)
    while true do
        if gpio.get(18) == 0 then
            sys.wait(20)
            if gpio.get(18) == 0 then
                key1_flag = true
            end
        end
        if gpio.get(19) == 0 then
            sys.wait(20)
            if gpio.get(19) == 0 then
                key2_flag = true
            end
        end
        sys.wait(50)
    end
end)


--[[
    지원 글꼴 ["sarasa_m8_ascii","sarasa_m10_ascii","sarasa_m12_ascii",
    "sarasa_m14_ascii","sarasa_m16_ascii","sarasa_m18_ascii","sarasa_m20_ascii","sarasa_m22_ascii"]
]]

-- 사용자 코드 종료---------------------------------------------
-- 항상 이 문장으로 끝남
sys.run()
-- sys.run() 이후에는 어떤 문장도 추가하지 마세요!!!!!

## 사용된 하드웨어

사용된 하드웨어 모듈 및 구매 링크:- 4G 모듈: Air700E, [https://s.click.taobao.com/gMXE14u](https://uland.taobao.com/coupon/edetail?e=LUUvClZZaoalhHvvyUNXZfh8CuWt5YH5OVuOuRD5gLJMmdsrkidbOUV9IBA4kmjLCKSz6NMEOpOk6DkEOOy39ANONOd65bn6k%2BQD1WLWk7D58pWrh7e7ViECg58T7b6E7qOXUj4MT3dyBOK%2B8KjzSuzY3MUSAX0G1TP3uC6T%2BzrKa4jyh4U%2Bo5gBVnx58RcK3x24QQZ%2BkKRI%2Bf4%2BtcDu44oLvKgaQ5D83qmGq7GPCXsH2XAjD13xyT%2FhMZgljYbZj4NAR0X%2B9c23Sv8LMk9JOkIQtV1V6tmtIciWFcjrsPzH6emLlAySAVD9ZzTfOOXe%2Bm9PGdJ%2FlaUZUAGa9XIpkqJ7%2BkHL3AEW&traceId=213d811317025749037687621e65ab&union_lens=lensId%3APUB%401702574899%40212be3f7_0d90_18c695f4255_c520%400212cIwjWn6h4JZ67isD7Et6%40eyJmbG9vcklkIjo4MDY3NCwiic3BtQiiI6Il9wb3J0YWxfdjJfcGFnZXNfcHJvbW9fZ29vZHNfaW5kZXhfaHRtIiiwiic3JjRmxvb3JJZCI6IjgwNjc0In0ie%3Bscm%3A1007.30148.329090.pub_search-item_ea9ed1dc-121c-4a26-85aa-1c27c9c4e982_)
- WiFi 모듈: ESP32-C3, [https://s.click.taobao.com/VU6E14u](https://s.click.taobao.com/t?e=m%3D2%26s%3DZlxzb7VA%2BERw4vFB6t2Z2ueEDrYVVa64YUrQeSeIhnK53hKxp7mNFhiPRfK6WZZbW%2BG7BH3F%2BVP0JlhLk0Jl4QTquP0kWxBLBDnvz6xo38xspWc9%2BCL4bTGF1ceZMhPo8mL8HhJ3EdVrH4ks4QyiY4z4rjZDGVMA7PT7P4MIjv0iDnQnCfxRCKLWMw3EOEsyJN2owMjhufwDudUsQ2T%2BdtwdcXrvJnQ7ZHuYE9Ro5OOZ8DKCp9t3JAIHG%2FUGGufvqH4FkV%2B42f2z1OkmXn0f%2BU%2FuprW1TdmBLeMqtJBmsqBWpXpRELkuYcbEo3ADJdJgcSpj5qSCmbA%3D&union_lens=lensId%3APUB%401702575245%4021334feb_0c8e_18c6964886f_3037%4002FXjqZ71jUAMAWD59wyxDr%40eyJmbG9vcklkIjo4MDY3NCwiic3BtQiiI6Il9wb3J0YWxfdjJfcGFnZXNfcHJvbW9fZ29vZHNfaW5kZXhfaHRtIiiwiic3JjRmxvb3JJZCI6IjgwNjc0In0ie%3Bscm%3A1007.30148.329090.pub_search-item_86a564e3-eb01-4328-b966-1f368fd8dd53_)
- 온습도 센서: AHT10, [https://s.click.taobao.com/0bmD14u](https://s.click.taobao.com/t?e=m%3D2%26s%3D771Jk01ERgJw4vFB6t2Z2ueEDrYVVa64g3vZOarmkFi53hKxp7mNFhiPRfK6WZZbARW0aicXof30JlhLk0Jl4QTquP0kWxBLBDnvz6xo38xspWc9%2BCL4bTGF1ceZMhPo8mL8HhJ3EdVrH4ks4QyiY4z4rjZDGVMAhscfsB2%2FyzZJq71CBMBeP%2F1SarTXhIOTsgIpc1WFZiJNubylQlnZt9wh0O3n7rEs7VSraZkgkhwlqDRRVlGxRqvyzFinE2vlF34xAtPUu%2BiG3j8B3S26WYwmLWBgEm80VKxcI130SjETn2JhMaQNtYWLhXkoGmYy4m7hkqalpkwvY%2BXqRXyCksYOae24fhW0&union_lens=lensId%3APUB%401702575301%4021055555_16ff_18c69656404_8e69%40024dxCNyvH1jSaZZpYn2bbD3%40eyJmbG9vcklkIjo4MDY3NCwiic3BtQiiI6Il9wb3J0YWxfdjJfcGFnZXNfcHJvbW9fZ29vZHNfaW5kZXhfaHRtIiiwiic3JjRmxvb3JJZCI6IjgwNjc0In0ie%3Bscm%3A1007.30148.329090.pub_search-item_e4951d39-761b-41f5-8840-673a3c0c7f1f_)
- 기압 센서: BMP180, [https://s.click.taobao.com/lj5E14u](https://s.click.taobao.com/t?e=m%3D2%26s%3DbnhyRgad73lw4vFB6t2Z2ueEDrYVVa64YUrQeSeIhnK53hKxp7mNFhiPRfK6WZZbbJVKleC4LJX0JlhLk0Jl4QTquP0kWxBLBDnvz6xo38xspWc9%2BCL4bTGF1ceZMhPo8mL8HhJ3EdVrH4ks4QyiY4z4rjZDGVMAFBcM8wkWlGmBPeR%2FUFCetKLWMw3EOEsyJN2owMjhufwDudUsQ2T%2BduXhptvf7sm0vQrr15PsJfJByQLpIiyxZXY%2Fio3PXH82JxEGSxl4yKUtXkuY7xjNZU%2FuprW1TdmBLeMqtJBmsqAZVo9hr9WaqYgTf7YQgcVJcSpj5qSCmbA%3D&union_lens=lensId%3APUB%401702575360%402106d2ff_0cdf_18c69664982_7aaf%40021C2JTg214QOPnilxsDb2w%40eyJmbG9vcklkIjo4MDY3NCwiic3BtQiiI6Il9wb3J0YWxfdjJfcGFnZXNfcHJvbW9fZ29vZHNfaW5kZXhfaHRtIiiwiic3JjRmxvb3JJZCI6IjgwNjc0In0ie%3Bscm%3A1007.30148.329090.pub_search-item_f8a710ac-0037-4ec2-bbff-896f3c1af2c5_)
- 초음파 거리 측정 모듈: US-100(직렬 통신 모드 사용), [https://s.click.taobao.com/K0o5X3u](https://s.click.taobao.com/t?e=m%3D2%26s%3DInz6n3%2B%2FZwxw4vFB6t2Z2ueEDrYVVa64YUrQeSeIhnK53hKxp7mNFhiPRfK6WZZbq%2FcxWpaeRwj0JlhLk0Jl4QTquP0kWxBLBDnvz6xo38xspWc9%2BCL4bTGF1ceZMhPo8mL8HhJ3EdVrH4ks4QyiY4z4rjZDGVMAFBcM8wkWlGmBPeR%2FUFCetKLWMw3EOEsyJN2owMjhufwDudUsQ2T%2Bdg68505k9IiwAZQz22%2FRFMA5pQILDo6IOf16Gn%2BAIx7L96W3Eb16pLNyU%2FAPg17%2FFE%2FuprW1TdmBLeMqtJBmsqB4r3mjD%2F4H9bgUNPZMnoHFcSpj5qSCmbA%3D&skuId=4276770277565&union_lens=lensId%3APUB%401702575435%40213e7f1b_0c9e_18c69676fe9_4d3a%40036cIiBD2TZI9c9YU2Vchohd%40eyJmbG9vcklkIjo2MTQyOSwiic3BtQiiI6Il9wb3J0YWxfdjJfcGFnZXNfcHJvbW9fZ29vZHNfZGV0YWlsX2h0bSIsInNyY0Zsb29ySWQiiOiiI4MDY3NCJ9%3Bscm%3A1007.30148.329090.pub_search-item_972f304d-0e03-4fbf-b740-fd750c7af1d6_)
- LCD 화면: 1.47인치 ST7789, 해상도 172*320, [https://s.click.taobao.com/qalD14u](https://s.click.taobao.com/t?e=m%3D2%26s%3DgP4fG3vZI%2FNw4vFB6t2Z2ueEDrYVVa64g3vZOarmkFi53hKxp7mNFhiPRfK6WZZbfpmjeIyJHHf0JlhLk0Jl4QTquP0kWxBLBDnvz6xo38xspWc9%2BCL4bTGF1ceZMhPo8mL8HhJ3EdVrH4ks4QyiY4z4rjZDGVMAhscfsB2%2FyzZJq71CBMBeP%2F1SarTXhIOTsgIpc1WFZiJNubylQlnZt8KAViuQdyPNc8ER84P6KktsP7OMD4hiwaj%2Fx4cHJV5ILJ9HqoY3BUsLMohkjCbsqowmLWBgEm80VKxcI130SjETn2JhMaQNtYWLhXkoGmYyruoikO2OymYJLixcAe8pyMYOae24fhW0&union_lens=lensId%3APUB%401702575502%40212c48cb_0c8f_18c69687670_23aa%40024B9sLEl2iRKmna6a9po7FA%40eyJmbG9vcklkIjo4MDY3NCwiic3BtQiiI6Il9wb3J0YWxfdjJfcGFnZXNfcHJvbW9fZ29vZHNfaW5kZXhfaHRtIiiwiic3JjRmxvb3JJZCI6IjgwNjc0In0ie%3Bscm%3A1007.30148.329090.pub_search-item_5e0d1922-0ca6-44f9-99a9-821166c9de7d_)
- OLED 화면: 0.96인치 SSD1306, I2C 인터페이스, [https://s.click.taobao.com/otWD14u](https://s.click.taobao.com/t?e=m%3D2%26s%3DKH7sw7eX%2F2Bw4vFB6t2Z2ueEDrYVVa64YUrQeSeIhnK53hKxp7mNFhiPRfK6WZZbUnl%2Fg%2F1GNuL0JlhLk0Jl4QTquP0kWxBLBDnvz6xo38xspWc9%2BCL4bTGF1ceZMhPo8mL8HhJ3EdVrH4ks4QyiY4z4rjZDGVMAc7QBxLpsf4TZIsq6%2BWiNSqLWMw3EOEsyJN2owMjhufwDudUsQ2T%2BdqwhF6wjgAjmYU48%2FUVSeeMab%2F%2BCSuws4OiHLQ4Yn8csh%2BdzzS7mHW11b5eySA3UA0%2FuprW1TdmBLeMqtJBmsqChj81ryXyWXeLqYXvNN1cOcSpj5qSCmbA%3D&union_lens=lensId%3APUB%401702575558%40213f6e5c_0cfd_18c69694f33_999c%40021vqRKMT5nzj9ALhoKTH8lx%40eyJmbG9vcklkIjo4MDY3NCwiic3BtQiiI6Il9wb3J0YWxfdjJfcGFnZXNfcHJvbW9fZ29vZHNfaW5kZXhfaHRtIiiwiic3JjRmxvb3JJZCI6IjgwNjc0In0ie%3Bscm%3A1007.30148.329090.pub_search-item_b4129703-d754-49d5-b48a-82cdfefb2b5f_)
- 배터리 충·방전 칩: IP5306-CK(CK 버전을 구매해야 함, 일반 버전은 방전 전류가 너무 작으면 자동으로 출력이 차단되어 마이크로컨트롤러에 적합하지 않음), [https://s.click.taobao.com/eu85X3u](https://s.click.taobao.com/t?e=m%3D2%26s%3DjX7JisUJSllw4vFB6t2Z2ueEDrYVVa64g3vZOarmkFi53hKxp7mNFhiPRfK6WZZbiJKehPSaPYD0JlhLk0Jl4QTquP0kWxBLBDnvz6xo38xspWc9%2BCL4bTGF1ceZMhPo8mL8HhJ3EdVrH4ks4QyiY4z4rjZDGVMAhscfsB2%2FyzZJq71CBMBeP%2F1SarTXhIOTsgIpc1WFZiJNubylQlnZt4mEKVwjSJcGFXvRQPy5FMG3%2FxXe%2BxQvKcLHPTEEdUKrO5xtKw8OA8bUMx%2BC19Mn3YwmLWBgEm80VKxcI130SjETn2JhMaQNtYWLhXkoGmYy5%2FXtHaC5QvUT2J2P4FlLNcYOae24fhW0&union_lens=lensId%3APUB%401702575629%4021055b80_0c9f_18c696a64f6_4c08%40022k2R4UBS6bcyB2aIphUK2l%40eyJmbG9vcklkIjo4MDY3NCwiic3BtQiiI6Il9wb3J0YWxfdjJfcGFnZXNfcHJvbW9fZ29vZHNfaW5kZXhfaHRtIiiwiic3JjRmxvb3JJZCI6IjgwNjc0In0ie%3Bscm%3A1007.30148.329090.pub_search-item_9fa456ea-a73f-470b-94dc-a6e6199bf4a4_)
- 배터리: 이웨이 35V-18650, 3500mAh, [https://s.click.taobao.com/uUc5X3u](https://s.click.taobao.com/t?e=m%3D2%26s%3DS952IFxDAbxw4vFB6t2Z2ueEDrYVVa64g3vZOarmkFi53hKxp7mNFhiPRfK6WZZbhQCvBpU9uKb0JlhLk0Jl4QTquP0kWxBLBDnvz6xo38xspWc9%2BCL4bTGF1ceZMhPo8mL8HhJ3EdVrH4ks4QyiY4z4rjZDGVMA2wPTjyt1VFhJDk80jsGTwv1SarTXhIOT%2FcbizUjsed%2BEr%2BInjt7kJlMJo4xVYoI6QTU2mSOmawi8xTwUuWBJq9spaWqoW5XN2MTzYpfP%2FlmjO9AJYjY8CXJ%2BwEVkOqHFdIW9JNkz7%2FgDXc3BRboPm%2FkFZVUCNgR0oMbC7PHmBq9RLBgaW5udaw%3D%3D&union_lens=lensId%3APUB%401702575711%400b8b8151_148e_18c696ba6d8_29d5%40021a7vszDH3fGcvCMrLJyErd%40eyJmbG9vcklkIjo4MDY3NCwiic3BtQiiI6Il9wb3J0YWxfdjJfcGFnZXNfcHJvbW9fZ29vZHNfaW5kZXhfaHRtIiiwiic3JjRmxvb3JJZCI6IjgwNjc0In0ie%3Bscm%3A1007.30148.329090.pub_search-item_52261151-42b3-4abc-8483-08f2c5c3cc3d_)

**부품 구매는 리창몰(立创商城) 추천, 할인 가입 링크: [https://activity.szlcsc.com/invite/D03E5B9CEAAE70A4.html](https://activity.szlcsc.com/invite/D03E5B9CEAAE70A4.html)**

**보드의 모든 부품은 리창몰에서 구매 가능하며, 오픈소스 링크의 BOM表에서 ‘즉시 리창몰로 이동해 주문하기’를 클릭하면 사용된 부품을 한 번에 장바구니로 가져올 수 있습니다.**

## IoT 플랫폼 설정 설명

먼저 알리윤 IoT 플랫폼에서 신제품을 생성하고, 노드 유형은 직접 연결 디바이스로 선택합니다.

![](upload://xLPj0bhLZ1rL7RZEilLQN6ik7Bg.png)

기능 정의 설정:

![](upload://zZrpMZ1m81Csig5pO9hwdwzZdVd.png)

이어서 디바이스 2개를 추가합니다

![](upload://2GKNz5ZfLb0gJ4QKViv65uUFYQx.png)

스크립트의 제품 키 등 매개변수를 수정한 후 펌웨어와 스크립트를 Air700E에 다운로드하여 디바이스가 정상적으로 온라인 상태가 되고 데이터를 업로드하는지 확인합니다.

![](upload://AsnIwH6TlnbEPb35i9fHDakJ7SQ.png)

메시지 전달→클라우드 제품 전달로 이동해 데이터 소스를 생성합니다:

![](upload://bEfZtuEN8lpD89diTlYSFGzClTK.png)

방금 생성한 데이터 소스 오른쪽 ‘보기’를 클릭한 뒤 Topic 추가, 첫 번째는 사용자 지정, 두 번째는 생성한 제품 선택, 세 번째는 4G 노드 디바이스 선택, 네 번째는 `user/update` 선택

![](upload://mrY2OUxHOIUvmF53kY8QYf5sxzj.png)

데이터 대상 생성, 작업 선택은 ‘다른 Topic에 게시’, 제품도 위에서 생성한 제품 선택

![](upload://55n4tbfW0Ag9TVFQ3laxDjBmNIB.png)

파서 생성 후 ‘편집하러 가기’ 클릭, 혹은 오른쪽 ‘보기’ 클릭

![](upload://rTDklqhUCwOY2zZ5ZkEfyJzo9At.png)

연결 데이터 소스에서 방금 생성한 데이터 소스 선택:

![](upload://vw789eZvaAKycMT2jbHJupaoWk4.png)

연결 데이터 대상에서 방금 생성한 데이터 대상 선택

![](upload://rGanTyGyLN3DK8NVT5MJFI02Fvx.png)

파서 스크립트에서 `deviceName()`을 WiFi 노드 디바이스 이름으로 변경, 아래 그림 참고

![](upload://ctS29c3tGAFXsA5Oz6vCWFGRZeN.png)

편집 완료 후 ‘게시’ 클릭, 이후 시작하면 `/ProductKey/DeviceName/user/update` 주제의 데이터가 `/ProductKey/DeviceName/user/get` 주제로 자동 전달됩니다.

## 기타 오픈소스 프로젝트 추천- STM32F030C8T6 최소 시스템 보드와 물결 LED (회로도 및 PCB): [https://blog.zeruns.com/archives/715.html](https://blog.zeruns.com/archives/715.html)
- MSP430F149 최소 시스템 보드를 그려서 오픈소스로 공개했습니다: [https://blog.zeruns.com/archives/713.html](https://blog.zeruns.com/archives/713.html)
- 2007년 전자 설계 대회 전원 문제: 30~36V 가변 승압 DCDC 모듈(UC3843): [https://oshwhub.com/zeruns/36v-sheng-ya-dcdc-mo-kuai-uc3842](https://oshwhub.com/zeruns/36v-sheng-ya-dcdc-mo-kuai-uc3842)
- STC12C5A60S2 최소 시스템 보드/51 MCU 온도 표시 및 온도 제어 팬: [https://blog.zeruns.com/archives/721.html](https://blog.zeruns.com/archives/721.html)
- U8g2 그래픽 라이브러리를 이식한 STM32F407 표준 라이브러리 프로젝트 템플릿: [https://blog.zeruns.com/archives/722.html](https://blog.zeruns.com/archives/722.html)
- Qinheng CH32V307VCT6 최소 시스템 보드 오픈소스: [https://blog.zeruns.com/archives/726.html](https://blog.zeruns.com/archives/726.html)
- LM25118 자동 승압/강압 가변 DCDC 전원 모듈: [https://blog.zeruns.com/archives/727.html](https://blog.zeruns.com/archives/727.html)
- EG1164 대용량 동기 정류 승압 모듈 오픈소스, 최대 효율 97%: [https://blog.zeruns.com/archives/730.html](https://blog.zeruns.com/archives/730.html)

## 추천 글

- **높은 가성비와 저렴한 VPS/클라우드 서버 추천:** [https://blog.zeruns.com/archives/383.html](https://blog.zeruns.com/archives/383.html)
- 마인크래프트 서버 구축 튜토리얼: [https://blog.zeruns.com/tag/mc/](https://blog.zeruns.com/tag/mc/)
- 집 네트워크 랙, 홈 네트워크 장비 추천 공유: [https://blog.zeruns.com/archives/732.html](https://blog.zeruns.com/archives/732.html)
- Orange Pi 3B(RK3566) 개발 보드 개봉기 및 리뷰: [https://blog.zeruns.com/archives/729.html](https://blog.zeruns.com/archives/729.html)
- PC 업그레이드, 그래픽 카드 교체, Yeston RTX3070: [https://blog.zeruns.com/archives/746.html](https://blog.zeruns.com/archives/746.html)