ESP32 4G/5G 및 GPS 인터페이스
이 가이드에서는 ESP32 기반의 매우 인상적인 보드인 Walter를 살펴보겠습니다. Walter는 Wi-Fi 없이도 모바일 네트워크 범위 내에서 인터넷에 완벽하게 접속할 수 있도록 해줍니다. 내장된 4G LTE-M/NB-IoT 모뎀과 GPS를 갖춘 Walter는 현장이나 이동 중에도 작동하는 연결형 프로젝트를 구축하려는 사용자에게 적합합니다.
이 강의에서는 MicroPython 설정, 네트워크 연결, GPS 데이터 수집, 클라우드 전송 과정을 단계별로 안내합니다. 강의가 끝나면 Walter를 사용하여 셀룰러 연결 IoT 기기를 직접 구축하는 데 필요한 모든 것을 갖추게 될 것입니다.
자, 시작해 볼까요!
내용물:
월터는 누구인가요?
준비물
월터 설정하기
LTE에 연결하고 인터넷에 접속하기
MQTT GPS 추적기 데모
Walter를 저전력 모드로 실행
앞으로 어떻게 해야 할까요?
월터는 누구인가요?
Walter는 최신 셀룰러 IoT 프로젝트를 위해 설계된 소형 고품질 ESP32-S3 개발 보드입니다. 핵심은 2MB의 PSRAM과 16MB의 플래시 메모리를 갖춘 ESP32-S3 마이크로컨트롤러로, 대부분의 프로젝트에 충분한 성능을 제공합니다. 이 칩은 Bluetooth 5 LE와 최대 150Mbit/s의 Wi-Fi를 지원하여 그 자체로도 뛰어난 성능을 자랑하지만, Walter는 일반적인 ESP32보다 훨씬 더 많은 기능을 제공합니다.

보드 반대편에는 Sequans Monarch 2 모뎀이 탑재되어 Walter에 완벽한 4G LTE-M 및 NB-IoT 연결을 제공합니다. 이 기술들은 IoT 기기를 위해 특별히 설계된 저전력, 저대역폭 셀룰러 기술로, 배터리 전원으로 수 주 또는 수개월 동안 현장에서 작동할 수 있는 보드에 적합합니다. Walter 웹사이트에서는 "4G/5G"를 많이 광고했지만, 이 모뎀은 4G만 지원하는 것 같습니다. Walter의 연결은 4G 표준을 기반으로 하지만, LTE-M과 NB-IoT는 대부분의 5G 인프라에서도 지원되므로 5G 또는 4G 네트워크 여부와 관계없이 모바일 통신망이 있는 곳이라면 거의 어디에서든 Walter를 사용할 수 있습니다. 또한, 아직은 먼 미래의 일이지만 4G 네트워크가 꺼진 상황에서도 이러한 기술들이 작동할 가능성이 높다는 점은 큰 장점입니다.
Sequans 칩에는 GPS 및 Galileo 위성 항법 시스템을 지원하는 GNSS 수신기가 포함되어 있습니다. GNSS라고 하면 흔히 지구 주위를 도는 위성 네트워크를 떠올리지만, GPS는 미국의 위성 네트워크일 뿐이며, GLONASS(러시아), BeiDou(중국), Galileo(유럽)와 같은 다른 네트워크도 사용할 수 있습니다. 이러한 위성 시스템은 모두 해당 지역뿐만 아니라 지구상의 어느 곳이든 위치를 파악할 수 있으며, 이 칩이 GPS와 Galileo 시스템을 모두 사용할 수 있다는 것은 위치 파악에 있어 더 많은 위성을 활용하여 안정성을 높이고 정확도를 향상시킬 수 있음을 의미합니다 (또한 Galileo는 GPS보다 정확도가 약간 더 높습니다).
Walter의 하드웨어는 실용적인 측면에서도 뛰어납니다. EU에서 설계 및 제조되었고, 완전한 오픈 소스이며, 대부분의 주요 지역에서 인증을 받았습니다. 따라서 메이커 프로젝트를 넘어 상업 또는 소량 생산 환경으로 확장될 가능성이 있는 제품을 제작하는 경우 탁월한 선택이 될 수 있습니다.
게다가 Walter는 훌륭한 라이브러리와 문서를 이미 준비해 두었습니다. 앞으로 살펴보겠지만, C++와 MicroPython으로 작성된 라이브러리를 통해 Sequans 모뎀의 거의 모든 기능을 활용할 수 있습니다. 보드 자체와 라이브러리에 대한 문서도 잘 정리되어 있어 신선한 느낌입니다.
준비물
따라하기 위해서는 다음이 필요합니다.
- Walter 월터 보드
- LTE 및 GPS 안테나 . 이 기능을 사용하려면 안테나가 필요합니다. 안테나가 연결되지 않은 상태에서 Walter의 전원을 켜지 마십시오. 적절한 부하 없이 LTE 모뎀이나 GPS 수신기를 작동시키면 기판이 영구적으로 손상될 수 있습니다.
- 나노 SIM 카드가 필요합니다. 해당 지역에서 사용 가능한 데이터 요금제가 있는 SIM 카드가 필요합니다. Walter는 지역 제한이 없으므로 글로벌 SIM 카드를 사용할 수 있습니다. 테스트 목적으로는 휴대폰의 SIM 카드를 사용해도 되지만, 실제 현장에 배포할 경우에는 IoT 전용 SIM 요금제를 사용하는 것이 훨씬 저렴합니다.
- USB-C 케이블 .
- 선택 사항: Walter Feels 보드 . 이 보드는 Walter를 위한 캐리어 보드로, CAN 버스와 같은 여러 산업 표준을 지원하며 몇 가지 센서가 포함되어 있습니다. 대부분의 메이커 프로젝트에는 과할 수 있지만, Walter를 산업적 또는 상업적 용도로 사용하려는 경우 고려해 볼 만합니다.

월터 설정하기
먼저 두 개의 안테나를 연결하세요. LTE 안테나는 5G라고 표시된 포트에, GNNS 안테나는 위성 기호가 있는 포트에 연결합니다. 이 커넥터는 작고 섬세하므로 비틀거나 옆으로 밀어 넣지 말고 단단히 눌러서 제대로 연결해야 합니다.
경고: 적절한 안테나가 연결되지 않은 상태에서 LTE 또는 GNNS를 사용하지 마십시오. 그렇게 하면 모뎀이 손상되거나 완전히 고장날 수 있습니다. Walter를 처음 켤 때 이러한 기능을 사용하려고 시도하므로 반드시 안테나가 연결되어 있는지 확인하십시오.
보드 가장자리에 있는 슬롯에 나노 SIM 카드를 삽입하세요. 테스트를 위해 일반 휴대폰 SIM 카드를 사용해도 됩니다.
Walter를 현장에 두고 사용할 계획이라면 IoT 전용 SIM 요금제를 알아보는 것이 좋습니다. 많은 통신사에서 이제 기기 간 데이터 또는 여러 기기에서 사용할 수 있는 데이터 옵션을 제공하며, 몇 달러만 추가하면 매달 몇 메가바이트의 데이터를 SIM 카드에 저장할 수 있습니다. 대부분의 IoT 앱은 데이터 사용량이 매우 적기 때문에 여러 기기에서 하나의 요금제를 공유하는 것이 가능합니다. 국제 연결이 필요한 경우 글로벌 IoT SIM 제공업체를 이용하는 것도 좋은 방법입니다.
Walter의 전원을 켜면 미리 설치된 데모 버전이 실행됩니다. Walter 데모 페이지 로 이동하면 기기가 표시되고 보드의 기본 정보가 나타날 것입니다. MAC 주소를 통해 어떤 보드가 본인의 것인지 확인할 수 있으며, MAC 주소는 Walter와 함께 제공된 봉투에 적혀 있습니다. 이 데모 버전을 사용하면 모든 설정이 올바르게 되었는지 간단하게 테스트할 수 있습니다.

이곳은 공개 사이트이므로 누구나 사용자의 Walter 위치(사용자 위치)를 볼 수 있다는 점에 유의하십시오. Walter의 전원을 끄면 위치 정보가 더 이상 표시되지 않습니다. 이 데모 버전을 사용하지 않으려면, 보드에 자체 펌웨어를 설치하기 전까지 SIM 카드를 삽입하지 마십시오.
또한, 거대 감시 국가가 당신의 모든 움직임을 감시하고 있다는 걱정은 하지 마세요. 데모 코드는 이 사이트로 데이터를 전송하도록 작성되었으며, 다른 코드로 교체하면 더 이상 정보를 수신하지 못하게 됩니다.
데모에서는 MicroPython을 사용할 예정이므로, 저희가 선택한 IDE인 Thonny를 사용하여 MicroPython을 설정하는 방법을 살펴보겠습니다 . C++ 사용을 선호하시는 분들은 Thonny 문서를 참고하시면 쉽게 시작하실 수 있습니다.

Walter는 ESP32-S3용 표준 MicroPython을 실행하므로 설정 과정은 다른 일반 ESP32 보드와 동일합니다. Thonny를 실행하고 실행 > 인터프리터 구성을 선택한 다음 MicroPython 설치 또는 업데이트를 클릭합니다 . Walter를 PC에 연결하고 COM 포트 를 선택합니다 . 그런 다음 MicroPython 제품군으로 ESP32-S3를 선택하고 변형으로 Espressif ESP32-S3를 선택합니다 . 설치를 클릭하면 완료됩니다!
이제 Sequans 모뎀을 사용하기 위한 라이브러리를 설치해야 합니다. Walter MicroPython GitHub 페이지 에서 다운로드 하거나 여기에서 직접 다운로드할 수 있습니다 . 압축을 풀면 "walter_modem"이라는 폴더가 있고 그 안에 몇 가지 Python 라이브러리 파일이 있습니다. 이 "walter_modem" 폴더를 Walter에 복사하면 됩니다.
Thonny에서 [보기]를 선택 하고 [파일]이 선택되어 있는지 확인하여 왼쪽 창에 파일 탐색기가 표시되는지 확인합니다. 아래쪽 창(Walter의 파일)에서 마우스 오른쪽 버튼을 클릭하고 "lib"라는 새 디렉터리를 만든 다음, 해당 폴더를 두 번 클릭하여 들어갑니다. 그런 다음 위쪽 창(PC의 파일)에서 "walter_modem" 폴더로 이동하여 마우스 오른쪽 버튼을 클릭하고 [/lib에 업로드]를 선택합니다 .
이제 Walter님, 모든 설정이 완료되어 바로 사용하실 수 있습니다. 이제 코드만 작성하시면 됩니다!
LTE에 연결하고 인터넷에 접속하기
월터를 인터넷에 연결해 봅시다! 이 섹션에서는 내장된 Sequans Monarch 2 모뎀을 사용하여 셀룰러 네트워크에 연결하고, 모바일 네트워크에서 시간을 가져온 다음, HTTP를 통해 간단한 테스트 쿼리를 실행하여 모든 것이 제대로 작동하는지 확인합니다.
먼저 Walter에 새 파일 두 개를 생성하세요.
- main.py - 모뎀을 제어하고 데모를 실행하는 핵심 코드입니다.
- config.py - 모든 APN 및 SIM 설정이 저장되어 있습니다.
다음 내용을 config.py 파일에 붙여넣고 Walter에 저장하세요. 아래 코드는 저희가 Walter에서 실행한 코드와 동일합니다. 지역이나 통신사에서 수동으로 지정해야 하는 경우가 아니면 해당 섹션은 비워 두셔도 됩니다. 대부분의 SIM 카드는 자동 APN 감지 기능을 통해 문제없이 작동하지만, 통신사에서 사용자 지정 APN 이름, 사용자 이름 또는 비밀번호를 사용하는 경우 여기에 추가할 수 있습니다. 또한 조회할 사이트를 변경할 수도 있습니다. 이 예시에서는 시간과 날짜만 사용했습니다.
from walter_modem.mixins.default_pdp import WalterModemPDPAuthProtocol
# Cellular connection settings
CELL_APN = "" # leave blank for automatic APN detection
APN_USERNAME = "" # leave blank unless operator requires one
APN_PASSWORD = ""
AUTHENTICATION_PROTOCOL = WalterModemPDPAuthProtocol.NONE
SIM_PIN = None # set if your SIM needs a PIN
# HTTP demo target
HTTP_ADDRESS = "worldtimeapi.org" # free, public JSON time API
HTTP_PORT = 80 # 443 if you later add TLS
HTTP_URI = "/api/timezone/Etc/UTC"
다음 코드를 main.py 파일에 붙여넣고 Walter에도 저장하세요. main.py로 저장되면 Walter 전원이 켜질 때마다 이 코드가 자동으로 실행됩니다.
import micropython
micropython.opt_level(1)
import asyncio, sys
from walter_modem import Modem
from walter_modem.mixins.default_sim_network import *
from walter_modem.mixins.default_pdp import *
from walter_modem.mixins.http import *
from walter_modem.coreEnums import *
from walter_modem.coreStructs import *
from walter_modem.mixins.http import HTTPMixin
import config
# Instantiate modem with the HTTP mixin for querying sites
modem = Modem(HTTPMixin, load_default_power_saving_mixin=False)
modem_rsp = WalterModemRsp()
# --- Helper: wait for registration -----------------------------------------
async def wait_for_network_reg_state(timeout: int, *states: WalterModemNetworkRegState) -> bool:
for _ in range(timeout):
if modem.get_network_reg_state() in states:
return True
await asyncio.sleep(1)
return False
# --- LTE connect logic ------------------------------------------------------
async def lte_connect(_retry: bool = False) -> bool:
"""Bring up LTE link, automatically toggling RAT if needed."""
# Already connected?
if modem.get_network_reg_state() in (
WalterModemNetworkRegState.REGISTERED_HOME,
WalterModemNetworkRegState.REGISTERED_ROAMING,
):
return True
# Full operation mode
if not await modem.set_op_state(WalterModemOpState.FULL):
print("Failed to set operational state to FULL")
return False
if not await modem.set_network_selection_mode(WalterModemNetworkSelMode.AUTOMATIC):
print("Failed to set selection mode")
return False
print("Waiting for network...")
if not await wait_for_network_reg_state(
180,
WalterModemNetworkRegState.REGISTERED_HOME,
WalterModemNetworkRegState.REGISTERED_ROAMING,
):
# Retry with opposite RAT once
if await modem.get_rat(rsp=modem_rsp):
next_rat = (
WalterModemRat.NBIOT
if modem_rsp.rat == WalterModemRat.LTEM
else WalterModemRat.LTEM
)
print(f"Switching RAT and retrying ({WalterModemRat.get_value_name(next_rat)})")
await modem.set_rat(next_rat)
await modem.reset()
return await lte_connect(_retry=True)
return False
return True
# --- Setup ------------------------------------------------------------------
async def setup():
print("Walter Demo 1: Hello Internet\n----------------------------")
await modem.begin()
# Basic modem check
if not await modem.check_comm():
print("Modem communication failed")
return False
# Unlock SIM if a PIN is defined (unlikely but worth checking)
if config.SIM_PIN and not await modem.unlock_sim(pin=config.SIM_PIN):
print("Could not unlock SIM")
return False
# Create PDP context (APN)
if not await modem.create_PDP_context(apn=config.CELL_APN):
print("Failed to create PDP")
return False
if config.APN_USERNAME:
await modem.set_PDP_auth_params(
protocol=config.AUTHENTICATION_PROTOCOL,
user_id=config.APN_USERNAME,
password=config.APN_PASSWORD,
)
# Attach and connect to LTE
print("Connecting to LTE...")
if not await lte_connect():
print("LTE connection failed")
return False
print("Connected to network\n")
return True
# --- Demo logic -------------------------------------------------------------
async def demo_http_and_time():
"""Fetch modem time, then query external API."""
# Get network time
print("Fetching modem clock...")
if await modem.get_clock(rsp=modem_rsp):
print(f"Network time: {modem_rsp.clock}")
else:
print("Could not read time")
# Configure an HTTP profile (no TLS)
print(f"Setting up HTTP profile for {config.HTTP_ADDRESS}")
if not await modem.http_config_profile(
profile_id=1,
server_address=config.HTTP_ADDRESS,
port=config.HTTP_PORT,
):
print("Failed to config HTTP profile")
return
# Perform GET request
print(f"Requesting {config.HTTP_URI} ...")
if await modem.http_query(profile_id=1, uri=config.HTTP_URI):
print("HTTP GET sent, waiting for response...")
while True:
if await modem.http_did_ring(profile_id=1, rsp=modem_rsp):
if modem_rsp.http_response:
hr = modem_rsp.http_response
print(f"\nHTTP status: {hr.http_status}")
print(f"Content type: {hr.content_type}")
print("Snippet of response:")
# first 200 bytes preview (avoid full JSON blob flood)
print(hr.data[:200])
break
await asyncio.sleep(1)
else:
print("HTTP query failed")
# --- Entrypoint -------------------------------------------------------------
async def main():
try:
if not await setup():
raise RuntimeError("Setup failed")
await demo_http_and_time()
print("\nDemo complete.")
except Exception as err:
print("ERROR:")
sys.print_exception(err)
asyncio.run(main())
이 코드를 실행하면 셸에 다음과 같은 내용이 표시될 것입니다.
MPY: soft reboot
Walter Demo 1: Hello Internet
----------------------------
Connecting to LTE...
Waiting for network...
Connected to network
Fetching modem clock...
Network time: 1763082923
Setting up HTTP profile for worldtimeapi.org
Requesting /api/timezone/Etc/UTC ...
HTTP GET sent, waiting for response...
HTTP status: 200
Content type: "application/json; charset=utf-8"
Snippet of response:
bytearray(b'{"utc_offset":"+00:00","timezone":"Etc/UTC","day_of_week":5,"day_of_year":318,"datetime":"2025-11-14T01:15:25.092797+00:00","utc_datetime":"2025-11-
Demo complete.
축하합니다! Walter가 방금 인터넷에 연결하여 모바일 네트워크에서 시간(1970년 1월 1일 이후 경과된 초 수를 나타내는 에포크 시간)을 가져온 다음 웹사이트에 접속하여 시간과 날짜를 반환받았습니다. 작동 방식을 이해하지 못하면 데모 스크립트를 실행하는 것은 의미가 없으므로, 어떤 일이 일어나는지 자세히 살펴보겠습니다.
먼저 MicroPython을 임포트하고 최적화 수준을 설정합니다. 이는 인터프리터에게 일부 디버그 검사를 건너뛰도록 지시하는 것으로, 코드가 조금 더 빠르게 실행되도록 합니다. 특히 비동기 작업에 유용합니다.
asyncio는 MicroPython의 비동기 프레임워크(여러 코드 조각을 동시에 실행할 수 있게 해 줍니다)이고, sys는 나중에 깔끔한 오류 출력을 위해 사용됩니다.
import micropython
micropython.opt_level(1)
import asyncio, sys
다음으로 Walter 모델 라이브러리와 여러 믹스인을 가져옵니다.
믹스인은 특정 기능 집합에 대한 명령 묶음을 추가합니다. 예를 들면 다음과 같습니다.
- efault_sim_network에는 모뎀을 네트워크에 등록하는 함수가 포함되어 있습니다.
- default_pdp는 PDP 컨텍스트(데이터 연결)를 관리하기 위한 명령어를 추가합니다.
- http는 연결되면 HTTP 요청을 수행하는 기능을 추가합니다.
이러한 파일들을 임포트함으로써, 우리 코드는 모뎀에 AT 명령어를 수동으로 전송하는 대신 고수준의 헬퍼 함수에 접근할 수 있습니다. 또한, 우리가 만든 설정 파일도 임포트하여 그 안에 있는 정보에 접근할 수 있도록 합니다.
import asyncio, sys
from walter_modem import Modem
from walter_modem.mixins.default_sim_network import *
from walter_modem.mixins.default_pdp import *
from walter_modem.mixins.http import *
from walter_modem.coreEnums import *
from walter_modem.coreStructs import *
from walter_modem.mixins.http import HTTPMixin
import config
여기서는 모뎀 객체를 초기화하고 HTTPMixin 을 추가하여 나중에 웹 요청을 수행할 수 있도록 합니다. WalterModemRsp() 객체는 현재 네트워크 상태 또는 HTTP 응답 데이터와 같은 모뎀의 구조화된 응답을 저장하는 데 사용되는 컨테이너입니다.
modem = Modem ( HTTPMixin , load_default_power_saving_mixin = False )
modem_rsp = WalterModemRsp ()
셀룰러 기기는 데이터에 접속하기 전에 근처 기지국에 등록해야 합니다. 이 도우미 함수는 타임아웃 시간 동안 매초 모뎀의 네트워크 등록 상태를 확인하고 , 모뎀 이 등록되었다고 보고하면(홈 네트워크 또는 로밍 파트너 네트워크에) True를 반환합니다 .
async def wait_for_network_reg_state(timeout: int, *states: WalterModemNetworkRegState) -> bool:
for _ in range(timeout):
if modem.get_network_reg_state() in states:
return True
await asyncio.sleep(1)
return False
다음으로 LTE 연결 로직을 관리하는 더 큰 헬퍼 함수가 있습니다. 여기서 몇 가지 작업이 수행됩니다. 먼저 이미 연결되어 있는지 확인하고, 모뎀이 이미 등록되어 있으면 모든 설정 과정을 건너뜁니다.
async def lte_connect(_retry: bool = False) -> bool:
if modem.get_network_reg_state() in (
WalterModemNetworkRegState.REGISTERED_HOME,
WalterModemNetworkRegState.REGISTERED_ROAMING,
):
return True
그런 다음 모뎀을 최대 전력/작동 상태로 전환하려고 시도합니다. 이는 주로 모뎀이 절전 모드 등에 있지 않은지 확인하는 과정입니다.
if not await modem.set_op_state(WalterModemOpState.FULL):
print("Failed to set operational state to FULL")
return False
모뎀 의 작동 상태를 FULL로 설정 하는 데 실패 하면 "작동 상태를 FULL로 설정하는 데 실패했습니다 . " 라는 메시지를 출력하고 False 를 반환합니다 .
런 다음 모뎀에게 최적의 네트워크를 자동으로 선택하도록 지시합니다. 아마도 가장 강력한 네트워크를 선택할 가능성이 높을 것입니다.
if not await modem.set_network_selection_mode(WalterModemNetworkSelMode.AUTOMATIC):
print("Failed to set selection mode")
return False
모뎀이 네트워크 선택 모드를 설정할 때 까지 기다리지 않으면 " 선택 모드 설정에 실패했습니다 . " 를 출력 하고 False를 반환합니다 .
그런 다음 모뎀이 기지국에 연결되는 데 최대 3분이 소요됩니다. 네트워크 등록이 완료되면 네트워크와 통신을 시작할 수 있습니다.
print("Waiting for network...")
if not await wait_for_network_reg_state(
180,
WalterModemNetworkRegState.REGISTERED_HOME,
WalterModemNetworkRegState.REGISTERED_ROAMING,
):
만약 그것도 실패하면 다른 RAT(무선 접속 기술)로 다시 시도합니다. LTE가 실패하면 NB-IoT를 시도하도록 코드에 지시하고, 반대로 NB-IoT가 실패하면 LTE를 시도하도록 지시하는 것입니다. 이는 우리 경우에는 다소 중복될 수 있지만, 안정성을 높이는 좋은 방법입니다.
# Retry with opposite RAT once
if await modem.get_rat(rsp=modem_rsp):
next_rat = (
WalterModemRat.NBIOT
if modem_rsp.rat == WalterModemRat.LTEM
else WalterModemRat.LTEM
)
print(f"Switching RAT and retrying ({WalterModemRat.get_value_name(next_rat)})")
await modem.set_rat(next_rat)
await modem.reset()
return await lte_connect(_retry=True)
return False
return True
그리고 이 기능을 이용하면 이제 셀룰러 연결을 설정할 수 있는 방법이 생겼습니다!
다음으로, 모뎀을 완전히 활성화하고 SIM 카드를 확인하는 또 다른 함수가 있습니다. 이 함수는 modem.begin()으로 시작하는데, 이 함수는 모뎀을 활성화하고 ESP32와 Sequans 칩 간의 직렬 인터페이스를 열어 두 장치가 함께 작동할 수 있도록 합니다.
async def setup ( ):
await modem.begin ( )
그다음에는 `modem.check_comm()`을 실행하여 ESP32가 모뎀과 통신할 수 있는지 확인합니다. 이는 사용하기 전에 마지막으로 한 번 더 확인하는 과정입니다.
if not await modem.check_comm():
print("Modem communication failed")
return False
모뎀이 연결될 때 까지 기다리지 않으면 " 모뎀 통신 실패" 를 출력하고 False 를 반환합니다 .
다음으로 라이브러리를 사용하여 SIM 카드 잠금 해제가 필요한지 확인합니다. SIM 카드의 95%는 잠금 해제가 필요하지 않지만, 그래도 확인하는 것이 좋습니다.
if config.SIM_PIN and not await modem.unlock_sim(pin=config.SIM_PIN):
print("Could not unlock SIM")
return False
설정 파일 ( config.SIM_PIN ) 이 설정되어 있고 모뎀 의 ` unlock_sim` 메서드 호출을 기다리지 않은 경우 , " SIM 잠금 해제에 실패했습니다." 를 출력하고 False를 반환합니다 .
다음으로 PDP 컨텍스트를 생성합니다. 이 단계가 실제로 인터넷 데이터 흐름을 가능하게 하는 단계입니다. PDP 컨텍스트(패킷 데이터 프로토콜)는 기기와 이동통신사 네트워크 간의 "세션"을 정의합니다. 이 컨텍스트는 APN(액세스 포인트 이름)으로 구성되며, APN은 이동통신사에 어떤 네트워크 서비스를 제공해야 하는지 알려줍니다. SIM 요금제에 자격 증명이 필요한 경우, 스크립트는 우리가 생성한 구성 파일에서 자격 증명을 설정합니다.
if config.APN_USERNAME:
await modem.set_PDP_auth_params(
protocol=config.AUTHENTICATION_PROTOCOL,
user_id=config.APN_USERNAME,
password=config.APN_PASSWORD,
)
다음으로 LTE에 연결하는 설정 스크립트를 완료합니다. 이 스크립트는 앞서 작성한 lte_connect() 헬퍼 함수를 호출하고 모뎀이 유효한 데이터 연결을 보고할 때까지 기다립니다. 이 단계가 성공하면 모뎀에 IP 주소가 할당되고 Walter는 공식적으로 인터넷에 연결되어 사용할 준비가 됩니다.
print("Connecting to LTE...")
if not await lte_connect():
print("LTE connection failed")
return False
print("Connected to network\n")
return True
다음으로 시간을 가져오고 우리가 추적하는 웹 요청을 수행하는 함수를 하나 더 생성합니다. 먼저 modem.get_clock(rsp=modem_rsp)로 네트워크 에포크 시간을 가져옵니다. 이는 이동통신사의 네트워크 시계(NITZ 서비스)를 조회하므로 GPS 없이도 정확한 타임스탬프를 얻을 수 있습니다.
async def demo_http_and_time():
print("Fetching modem clock...")
if await modem.get_clock(rsp=modem_rsp):
print(f"Network time: {modem_rsp.clock}")
else:
print("Could not read time")
그런 다음 모뎀에 저장된 HTTP 세션을 설정하고(프로필 1 사용), 구성 파일에 설정한 주소와 포트를 지정합니다.
print(f"Setting up HTTP profile for {config.HTTP_ADDRESS}")
if not await modem.http_config_profile(
profile_id=1,
server_address=config.HTTP_ADDRESS,
port=config.HTTP_PORT,
):
print("Failed to config HTTP profile")
return
그다음으로는 매우 중요한 부분인 사이트에 GET 요청을 보내는 단계입니다. 이는 실제로 사이트에 연결을 시도하는 단계입니다.
print(f"Requesting {config.HTTP_URI} ...")
if await modem.http_query(profile_id=1, uri=config.HTTP_URI):
그다음에는 응답을 기다립니다. 라이브러리는 작은 "링" 이벤트로 수신되는 HTTP 데이터를 알립니다. 응답이 도착하면 코드는 modem_rsp.http_response 에서 구조화된 응답을 읽고 상태 코드, 콘텐츠 유형, 페이로드의 일부(Thonny에 과부하를 주지 않도록 200바이트로 제한됨)와 같은 주요 필드를 출력합니다.
print(f"Requesting {config.HTTP_URI} ...")
if await modem.http_query(profile_id=1, uri=config.HTTP_URI):
print("HTTP GET sent, waiting for response...")
while True:
if await modem.http_did_ring(profile_id=1, rsp=modem_rsp):
if modem_rsp.http_response:
hr = modem_rsp.http_response
print(f"\nHTTP status: {hr.http_status}")
print(f"Content type: {hr.content_type}")
print("Snippet of response:")
# first 200 bytes preview (avoid full JSON blob flood)
print(hr.data[:200])
break
await asyncio.sleep(1)
else:
print("HTTP query failed")
이제 모든 것을 하나로 묶는 main() 함수에 도달했습니다. 이 함수는 setup() 함수를 실행 하여 온라인에 접속한 다음 HTTP 데모를 실행합니다. 만약 오류가 발생하면 예외 처리기가 조용히 멈추는 대신 오류 메시지를 출력합니다. 마지막으로 asyncio.run(main())은 모든 await 호출이 작동하도록 하는 비동기 이벤트 루프를 시작합니다.
웹사이트를 조회하는 간단한 작업임에도 불구하고 상당히 많은 과정이 진행되지만, 각 단계를 이처럼 세밀하게 제어할 수 있다는 것은 프로젝트에서 더 큰 통제력과 오류 처리 능력을 확보하는 데 도움이 됩니다. 게다가 이 코드는 훌륭한 템플릿 역할을 하며, Walter를 사용하는 대부분의 프로젝트는 약간의 수정만 거치면 동일한 핵심 흐름을 따르게 됩니다.
요약하자면, 전체 과정을 통해 우리는 다음과 같은 결과를 얻었습니다.
- 전원을 켜고 모뎀과 통신하십시오.
- (필요한 경우) SIM 카드 잠금을 해제하세요.
- PDP 컨텍스트를 생성합니다(셀룰러 데이터 세션을 정의합니다).
- LTE-M 또는 NB-IoT에 연결하여 IP 주소를 받으세요.
- 네트워크의 시계를 읽으세요.
- 테스트용 HTTP 요청을 보내세요.
보시다시피, 대부분의 핵심 작업은 Walter 라이브러리 믹스인이 처리합니다. create_PDP_context()는 데이터 세션을 설정하고, lte_connect() 는 기지국에 연결하며, get_clock()은 통신 사업자의 시간을 읽고, HTTPMixin 은 모뎀에 AT 명령어를 직접 전송할 필요 없이 웹 요청을 처리합니다.
MQTT GPS 추적기 데모
이 데모에서는 Walter를 GPS 추적기로 만들어 MQTT 브로커(이 경우에는 Adafruit IO)에 위치 정보를 전송하도록 합니다. MQTT는 IoT를 위해 설계된 경량 프로토콜로 설정과 사용이 간편합니다. 약간의 지연 시간(약 2초)이 있지만, 인터넷에 연결된 장치에서 짧은 메시지를 전송하는 가장 쉬운 방법입니다. Adafruit IO에는 내장 GPS 지도가 있어 이를 통해 데이터를 시각화할 수 있습니다. 이 과정을 통해 Walter의 위치를 추적할 수 있는 개인 시스템을 구축할 수 있습니다.
다음 내용이 붙여넣어진 설정 파일이 다시 필요합니다.
from walter_modem.mixins.default_pdp import WalterModemPDPAuthProtocol
CELL_APN = ""
APN_USERNAME = ""
APN_PASSWORD = ""
AUTHENTICATION_PROTOCOL = WalterModemPDPAuthProtocol.NONE
SIM_PIN = None
# --- MQTT credentials for Adafruit IO ---
MQTT_SERVER_ADDRESS = "io.adafruit.com" # or change to what ever MQTT broker you are using
MQTT_PORT = 1883 # or 8883 if you enable TLS
MQTT_USERNAME = "" # enter your MQTT username
MQTT_PASSWORD = "" # enter your MQTT password
MQTT_TOPIC = f"{MQTT_USERNAME}/feeds/walter-gps" #assuming you created a feed called walter-gps
MAX_GNSS_CONFIDENCE = 80 # if the accuracy is greater than this, it won't send the location
PUBLISH_INTERVAL = 5 # seconds between publishes (sorta). It takes about 8-10 seconds to connect and send the message, this adds to that time.
그런 다음 다음 내용을 main.py로 저장하세요.
# walter_mqtt_gps_csv.py
import micropython
micropython.opt_level(1)
import asyncio, sys
import network
from walter_modem import Modem
from walter_modem.mixins.default_sim_network import *
from walter_modem.mixins.default_pdp import *
from walter_modem.mixins.mqtt import *
from walter_modem.mixins.gnss import *
from walter_modem.coreEnums import *
from walter_modem.coreStructs import *
import config
# ---------------------------------------------------------------------------
# Adafruit IO topic in CSV format.
AIO_CSV_TOPIC = f"{config.MQTT_USERNAME}/feeds/walter-gps/csv"
modem = Modem(MQTTMixin, GNSSMixin, load_default_power_saving_mixin=False)
modem_rsp = WalterModemRsp()
# ---------------------------------------------------------------------------
async def wait_for_network_reg_state(timeout: int, *states: WalterModemNetworkRegState):
for _ in range(timeout):
if modem.get_network_reg_state() in states:
return True
await asyncio.sleep(1)
return False
# ---------------------------------------------------------------------------
async def lte_connect(_retry=False):
"""Connect to LTE, retry once toggling RAT if needed."""
if modem.get_network_reg_state() in (
WalterModemNetworkRegState.REGISTERED_HOME,
WalterModemNetworkRegState.REGISTERED_ROAMING,
):
return True
if not await modem.set_op_state(WalterModemOpState.FULL):
print("Failed to set op state FULL")
return False
if not await modem.set_network_selection_mode(WalterModemNetworkSelMode.AUTOMATIC):
print("Failed to set network selection mode")
return False
print("Waiting for network registration ...")
if not await wait_for_network_reg_state(
180,
WalterModemNetworkRegState.REGISTERED_HOME,
WalterModemNetworkRegState.REGISTERED_ROAMING,
):
if await modem.get_rat(rsp=modem_rsp):
next_rat = (
WalterModemRat.NBIOT
if modem_rsp.rat == WalterModemRat.LTEM
else WalterModemRat.LTEM
)
if not _retry:
print(f"Retrying with RAT {WalterModemRat.get_value_name(next_rat)}")
await modem.set_rat(next_rat)
await modem.reset()
return await lte_connect(True)
return False
return True
# ---------------------------------------------------------------------------
async def lte_disconnect():
"""Turn off RF to allow GNSS to get a fix."""
ok = await modem.set_op_state(WalterModemOpState.MINIMUM)
await asyncio.sleep(3)
return ok
# ---------------------------------------------------------------------------
async def gnss_assistance_update():
"""Check and update almanac/ephemeris if required."""
if not await modem.get_clock(rsp=modem_rsp):
if not await lte_connect():
return False
await modem.get_clock(rsp=modem_rsp)
if not await modem.gnss_assistance_get_status(rsp=modem_rsp):
print("Failed to retrieve GNSS assistance status")
return False
alm = modem_rsp.gnss_assistance.almanac
eph = modem_rsp.gnss_assistance.realtime_ephemeris
if (not alm.available) or (alm.time_to_update <= 0):
await lte_connect()
await modem.gnss_assistance_update(WalterModemGNSSAssistanceType.ALMANAC)
if (not eph.available) or (eph.time_to_update <= 0):
await lte_connect()
await modem.gnss_assistance_update(WalterModemGNSSAssistanceType.REALTIME_EPHEMERIS)
return True
# ---------------------------------------------------------------------------
async def get_gnss_fix(max_tries=5):
"""Get a single valid GNSS fix."""
print("Requesting GNSS fix:")
gnss_fix = None
for i in range(max_tries):
print(f" Try {i+1}/{max_tries}")
await lte_disconnect()
ok = await modem.gnss_perform_action(
action=WalterModemGNSSAction.GET_SINGLE_FIX, rsp=modem_rsp
)
if not ok:
print(" Failed to request fix")
await asyncio.sleep(3)
continue
gnss_fix = await modem.gnss_wait_for_fix()
if gnss_fix and gnss_fix.estimated_confidence <= config.MAX_GNSS_CONFIDENCE:
print(f" ✅ Fix ({gnss_fix.latitude:.6f}, {gnss_fix.longitude:.6f})")
return gnss_fix
await asyncio.sleep(5)
return gnss_fix
# ---------------------------------------------------------------------------
async def setup():
print("\n=== Walter MQTT + GNSS CSV Publisher ===")
await modem.begin()
if not await modem.check_comm():
print("Modem communication failed")
return False
if config.SIM_PIN and not await modem.unlock_sim(pin=config.SIM_PIN):
print("SIM unlock failed")
return False
if not await modem.create_PDP_context(apn=config.CELL_APN):
print("Failed PDP context")
return False
if config.APN_USERNAME:
await modem.set_PDP_auth_params(
protocol=config.AUTHENTICATION_PROTOCOL,
user_id=config.APN_USERNAME,
password=config.APN_PASSWORD,
)
if not await lte_connect():
print("LTE attach failed")
return False
if not await modem.gnss_config():
print("Couldn't configure GNSS")
return False
await gnss_assistance_update()
return True
# ---------------------------------------------------------------------------
async def publish_csv_location(lat: float, lon: float, alt: float = 0.0):
"""Publish a CSV-formatted location for Adafruit IO Map block."""
payload = f"0,{lat:.6f},{lon:.6f},{alt:.1f}"
print("Publishing CSV:", payload)
if not await modem.mqtt_config(
user_name=config.MQTT_USERNAME,
password=config.MQTT_PASSWORD,
):
print("MQTT config failed")
return
if not await modem.mqtt_connect(
server_name=config.MQTT_SERVER_ADDRESS,
port=config.MQTT_PORT,
):
print("MQTT connect failed")
return
ok = await modem.mqtt_publish(
topic=AIO_CSV_TOPIC,
data=payload,
qos=1,
rsp=modem_rsp,
)
print("Publish result:", "OK" if ok else "FAIL")
await modem.mqtt_disconnect(rsp=modem_rsp)
# ---------------------------------------------------------------------------
async def main():
try:
if not await setup():
raise RuntimeError("Setup failed")
while True:
fix = await get_gnss_fix()
if fix:
await lte_connect()
altitude = getattr(fix, "altitude", 0.0) or 0.0
await publish_csv_location(fix.latitude, fix.longitude, altitude)
else:
print("No valid fix.")
print(f"Sleeping {config.PUBLISH_INTERVAL}s …\n")
await asyncio.sleep(config.PUBLISH_INTERVAL)
except Exception as err:
print("ERROR:")
sys.print_exception(err)
await asyncio.sleep(10)
# ---------------------------------------------------------------------------
asyncio.run(main())
이 코드의 대부분은 지난번과 동일하며, Adafruit IO 설정으로 넘어가기 전에 간단히 코드를 살펴보고 여기서 어떤 작업을 더 수행하는지 확인해 보겠습니다.
저희 임포트 섹션에서는 GNNS와 MQTT용 믹스인을 임포트합니다. 훌륭한 MQTT 라이브러리가 많이 있지만, Walter에 자체적으로 잘 만들어진 라이브러리가 포함되어 있다는 점이 좋습니다.
import micropython
micropython.opt_level(1)
import asyncio, sys
import network
from walter_modem import Modem
from walter_modem.mixins.default_sim_network import *
from walter_modem.mixins.default_pdp import *
from walter_modem.mixins.mqtt import *
from walter_modem.mixins.gnss import *
from walter_modem.coreEnums import *
from walter_modem.coreStructs import *
import config
다음으로 모뎀 객체를 초기화하는데, 이번에는 MQTT와 GNNS 믹스인을 포함시킵니다.
modem = Modem(MQTTMixin, GNSSMixin, load_default_power_saving_mixin=False)
modem_rsp = WalterModemRsp()
다음으로, 이전 스크립트와 동일하게 Walter를 온라인 상태로 만드는 데 도움이 되는 wait_for_network_reg_state() 및 lte_connect() 도우미 함수가 있습니다.
async def wait_for_network_reg_state(timeout: int, *states: WalterModemNetworkRegState):
for _ in range(timeout):
if modem.get_network_reg_state() in states:
return True
await asyncio.sleep(1)
return False
# ---------------------------------------------------------------------------
async def lte_connect(_retry=False):
"""Connect to LTE, retry once toggling RAT if needed."""
if modem.get_network_reg_state() in (
WalterModemNetworkRegState.REGISTERED_HOME,
WalterModemNetworkRegState.REGISTERED_ROAMING,
):
return True
if not await modem.set_op_state(WalterModemOpState.FULL):
print("Failed to set op state FULL")
return False
if not await modem.set_network_selection_mode(WalterModemNetworkSelMode.AUTOMATIC):
print("Failed to set network selection mode")
return False
print("Waiting for network registration ...")
if not await wait_for_network_reg_state(
180,
WalterModemNetworkRegState.REGISTERED_HOME,
WalterModemNetworkRegState.REGISTERED_ROAMING,
):
if await modem.get_rat(rsp=modem_rsp):
next_rat = (
WalterModemRat.NBIOT
if modem_rsp.rat == WalterModemRat.LTEM
else WalterModemRat.LTEM
)
if not _retry:
print(f"Retrying with RAT {WalterModemRat.get_value_name(next_rat)}")
await modem.set_rat(next_rat)
await modem.reset()
return await lte_connect(True)
return False
return True
하지만 새로운 헬퍼 함수인 lte_disconnect()가 있습니다. 이 함수는 GPS가 위치 정보를 수집할 때 LTE 무선 기능을 꺼야 하므로 LTE 무선 기능을 일시적으로 차단합니다.
async def lte_disconnect():
ok = await modem.set_op_state(WalterModemOpState.MINIMUM)
await asyncio.sleep(3)
return ok
또한 GPS 모듈의 천체력 및 궤도력 데이터를 최신 상태로 유지하는 새로운 기능이 추가되었습니다. GPS 칩은 이 두 데이터 세트를 사용하여 위성의 위치를 파악하고 더 빠르게 신호를 수신합니다. 먼저 GNSS 타이밍이 시스템 시계에 의존하기 때문에 get_clock( ) 함수를 사용하여 시스템 시계가 정확한지 확인합니다. 그런 다음 modem.gnss_assistance_get_status() 함수를 호출하여 천체력 또는 궤도력 데이터가 최신 상태인지 확인합니다. 데이터가 최신 상태가 아니면 LTE에 다시 연결하고 각 데이터 유형에 대해 modem.gnss_assistance_update() 함수 를 실행합니다 . 이 모든 과정은 GPS 신호 수신 시간을 단축하기 위한 것입니다. 그렇지 않으면 콜드 스타트 후 첫 번째 신호 수신에 몇 분이 걸릴 수 있습니다.
async def gnss_assistance_update():
"""Check and update almanac/ephemeris if required."""
if not await modem.get_clock(rsp=modem_rsp):
if not await lte_connect():
return False
await modem.get_clock(rsp=modem_rsp)
if not await modem.gnss_assistance_get_status(rsp=modem_rsp):
print("Failed to retrieve GNSS assistance status")
return False
alm = modem_rsp.gnss_assistance.almanac
eph = modem_rsp.gnss_assistance.realtime_ephemeris
if (not alm.available) or (alm.time_to_update <= 0):
await lte_connect()
await modem.gnss_assistance_update(WalterModemGNSSAssistanceType.ALMANAC)
if (not eph.available) or (eph.time_to_update <= 0):
await lte_connect()
await modem.gnss_assistance_update(WalterModemGNSSAssistanceType.REALTIME_EPHEMERIS)
return True
다음으로 GPS 신호를 수신하고 실제 위치 정보를 제공하는 함수로 넘어갑니다. 이 함수는 먼저 `lte_disconnect()`를 호출하여 LTE 통신을 일시적으로 차단합니다. 헬퍼 함수가 (프로그래밍된 대로) 반환되면 모뎀에 위치 정보 수신을 요청합니다. 위치 정보 수신에 실패하면 오류 메시지를 출력하고 다음 단계로 넘어갑니다. 이 모든 과정은 반복문 안에 포함되어 있어 지정된 횟수(기본값은 5회) 동안 GPS 위치 정보 수신을 시도할 수 있습니다.
async def get_gnss_fix(max_tries=5):
print("Requesting GNSS fix:")
gnss_fix = None
for i in range(max_tries):
print(f" Try {i+1}/{max_tries}")
await lte_disconnect()
ok = await modem.gnss_perform_action(
action=WalterModemGNSSAction.GET_SINGLE_FIX, rsp=modem_rsp
)
if not ok:
print(" Failed to request fix")
await asyncio.sleep(3)
continue
LTE 연결이 성공적으로 종료되면 GPS가 위치를 잡을 때까지 기다립니다. 위치를 잡으면 설정 파일에 만들어 둔 최대 신뢰도 값과 비교합니다. 최대 신뢰도 값이 더 크면 해당 위치는 무시합니다. 위치를 잡지 못한 경우, 5초간의 대기 시간 동안 GPS는 다시 시도합니다.
gnss_fix = await modem.gnss_wait_for_fix()
if gnss_fix and gnss_fix.estimated_confidence <= config.MAX_GNSS_CONFIDENCE:
print(f" ✅ Fix ({gnss_fix.latitude:.6f}, {gnss_fix.longitude:.6f})")
return gnss_fix
await asyncio.sleep(5)
return gnss_fix
이제 다시 setup() 함수 로 돌아갑니다 . 이 함수의 첫 부분은 지난 데모와 동일합니다.
async def setup():
print("\n=== Walter MQTT + GNSS CSV Publisher ===")
await modem.begin()
if not await modem.check_comm():
print("Modem communication failed")
return False
if config.SIM_PIN and not await modem.unlock_sim(pin=config.SIM_PIN):
print("SIM unlock failed")
return False
if not await modem.create_PDP_context(apn=config.CELL_APN):
print("Failed PDP context")
return False
if config.APN_USERNAME:
await modem.set_PDP_auth_params(
protocol=config.AUTHENTICATION_PROTOCOL,
user_id=config.APN_USERNAME,
password=config.APN_PASSWORD,
)
if not await lte_connect():
print("LTE attach failed")
return False
하지만 새로운 섹션이 두 개 추가되었습니다. 하나는 GNSS를 구성하여 하드웨어 자체를 초기화하는 부분이고, 다른 하나는 위에서 설명한 어시스턴트 업데이트 기능을 실행하는 부분입니다. 이는 GNSS를 사용하기 전에 일종의 "준비 작업"을 하는 것입니다. 위성 정보를 모르는 상태에서는 콜드 스타트에 몇 분 정도 소요될 수 있습니다.
if not await modem.gnss_config():
print("Couldn't configure GNSS")
return False
await gnss_assistance_update()
return True
모뎀이 gnss_config ()를 호출 하지 않으면 " GNSS를 구성할 수 없습니다." 라는 메시지 를 출력하고 False를 반환합니다 .
새로운 주요 기능이 하나 더 추가되었습니다. 이 기능은 GPS 측정값을 Adafruit IO로 전송하는 역할을 합니다. 먼저 페이로드(전송할 메시지)를 준비하고, Adafruit IO의 지도 블록에서 요구하는 형식으로 변환합니다.
async def publish_csv_location(lat: float, lon: float, alt: float = 0.0):
payload = f"0,{lat:.6f},{lon:.6f},{alt:.1f}"
다음으로 라이브러리를 사용하여 MQTT 자격 증명을 구성하고 MQTT 브로커에 연결합니다.
if not await modem.mqtt_config(
user_name=config.MQTT_USERNAME,
password=config.MQTT_PASSWORD,
):
print("MQTT config failed")
return
if not await modem.mqtt_connect(
server_name=config.MQTT_SERVER_ADDRESS,
port=config.MQTT_PORT,
):
이 모든 과정이 성공적으로 완료되면, 라이브러리를 사용하여 데이터를 브로커에 게시하기만 하면 됩니다.
ok = await modem.mqtt_publish(
topic=AIO_CSV_TOPIC,
data=payload,
qos=1,
rsp=modem_rsp,
)
print("Publish result:", "OK" if ok else "FAIL")
그다음, 이 모든 과정을 마친 후 브로커 연결을 해제합니다. 다른 GNSS 위치 정보를 얻으려면 LTE를 꺼야 하는데, 이는 연결 문제 발생을 방지하는 중요한 단계입니다 (이 가이드를 작성하는 과정에서 이 부분이 다소 골칫거리였습니다).
await modem.mqtt_disconnect(rsp=modem_rsp)
이렇게 해서 GPS 데이터를 입력받아 MQTT 브로커로 전송하는 함수를 만들 수 있게 되었습니다. 정말 편리하죠!
마지막으로, 모든 것을 조율하는 main() 루프 에 도달합니다 . 먼저 setup() 함수 를 실행하여 모든 것을 준비합니다.
async def main():
try:
if not await setup():
raise RuntimeError("Setup failed")
그리고 이 루프의 나머지 부분은 마치 소형 원격 측정 파이프라인처럼 작동합니다. GNNS 위치 정보를 얻기 위해 여러 함수를 사용하고, get_gnns_fix() 함수로 꺼진 LTE를 다시 켠 다음, publish_csv_location() 함수를 통해 해당 데이터를 Adafruit IO로 전송합니다 . 이 세 단계를 반복하면 Walter가 셀룰러 통신 범위 내에 있는 한 어디를 가든 추적할 수 있습니다!
while True:
fix = await get_gnss_fix()
if fix:
await lte_connect()
altitude = getattr(fix, "altitude", 0.0) or 0.0
await publish_csv_location(fix.latitude, fix.longitude, altitude)
else:
print("No valid fix.")
print(f"Sleeping {config.PUBLISH_INTERVAL}s …\n")
await asyncio.sleep(config.PUBLISH_INTERVAL)
이렇게 해서 MQTT 기반 GPS 추적기가 완성되었습니다! 단계별 과정을 간단히 요약하면 다음과 같습니다.
- 모뎀을 초기화하고 ESP32-S3와 Sequans 칩 간의 통신을 확인합니다.
- (필요한 경우) SIM 카드의 잠금을 해제하고 모뎀이 데이터 트래픽을 전송할 수 있도록 PDP 컨텍스트를 생성합니다.
- 이전과 마찬가지로 LTE-M 또는 NB-IoT에 연결되지만, 이제 GNSS 수신기를 구성하고 전원을 공급하는 기능도 추가되었습니다.
- GPS 수신 속도를 높이기 위해 천체력 및 천체력 지원 데이터를 업데이트합니다.
- LTE 라디오를 일시적으로 비활성화하고 gnss_perform_action() 을 사용하여 깨끗한 단일 GPS 위치 정보를 요청합니다 .
- LTE 네트워크에 다시 연결하고 내장된 MQTT 믹스인을 사용하여 MQTT 세션을 엽니다.
- 지도 대시보드에 필요한 CSV 형식으로 좌표를 Adafruit IO 피드에 게시합니다.
- MQTT 브로커와의 연결을 깔끔하게 끊고 몇 초간 기다린 후 5단계부터의 과정을 반복합니다.
이제 Adafruit IO 설정을 해볼까요! Adafruit IO 계정이 없으시다면 io.adafruit.com 에서 만드실 수 있습니다 . 무료 플랜은 분당 최대 30개의 데이터 포인트를 10개의 피드에 걸쳐 전송할 수 있고, 데이터를 30일 동안 보관할 수 있어 대부분의 가정용 프로젝트에 적합합니다. config.py 파일 에 Adafruit IO 정보를 업데이트하는 것도 잊지 마세요!

로그인 후에는 walter-gps 라는 이름의 새 피드를 생성해야 합니다 . 저희 코드가 이 피드를 찾고 있기 때문입니다. 다른 피드를 생성한 경우에는 코드를 수정해야 합니다. 이제 Walter에서 Adafruit IO로 데이터를 전송하면 이 피드에 정보가 표시되는 것을 확인할 수 있습니다.
이제 새 대시보드를 만들거나 기존 대시보드를 수정해 보세요. "새 블록" 버튼을 클릭하고 지도를 선택합니다. 소스 피드로 walter-gps 피드를 선택합니다. 그런 다음 원하는 경우 이름과 지도 유형을 설정합니다. 여기서 가장 중요한 설정은 표시할 GPS 기록 시간(시간 단위)을 지정하는 것입니다.
대시보드로 가서 Walter 시동을 걸면 GPS 추적기 설치가 완료됩니다!
Walter를 저전력 모드로 실행
Walter는 강력한 성능을 자랑하지만, 4G 무선 통신이 가능한 ESP32를 항상 온라인 상태로 유지하는 것은 배터리 소모가 심합니다. 따라서 전력 소모를 줄이기 위해 딥 슬립 기능을 도입하는 방법을 살펴보겠습니다! 지난 섹션의 데모 코드를 활용하여 Walter가 원하는 시간 동안 잠을 잘 수 있도록 딥 슬립 코드를 구현해 보겠습니다.
이 코드를 사용하려면 config.py 파일을 업데이트하세요. 이전과 동일하지만 Walter가 얼마나 오랫동안 잠을 자야 하는지를 지정하는 변수가 추가되었습니다.
from walter_modem.mixins.default_pdp import WalterModemPDPAuthProtocol
CELL_APN = ""
APN_USERNAME = ""
APN_PASSWORD = ""
AUTHENTICATION_PROTOCOL = WalterModemPDPAuthProtocol.NONE
SIM_PIN = None
# --- MQTT credentials for Adafruit IO ---
MQTT_SERVER_ADDRESS = "io.adafruit.com"
MQTT_PORT = 1883 # or 8883 if you enable TLS
MQTT_USERNAME = ""
MQTT_PASSWORD = ""
MQTT_TOPIC = f"{MQTT_USERNAME}/feeds/walter-gps"
MAX_GNSS_CONFIDENCE = 80 # or similar
SLEEP_TIME = 20 # seconds the ESP32-S3 stays in deep sleep
그런 다음 main.py 코드를 다음과 같이 업데이트하세요.
import micropython
micropython.opt_level(1)
import asyncio, sys
from walter_modem import Modem
from walter_modem.mixins.default_sim_network import *
from walter_modem.mixins.default_pdp import *
from walter_modem.mixins.mqtt import *
from walter_modem.mixins.gnss import *
from walter_modem.coreEnums import *
from walter_modem.coreStructs import *
import config
# ---------------------------------------------------------------------------
AIO_CSV_TOPIC = f"{config.MQTT_USERNAME}/feeds/walter-gps/csv"
modem = Modem(MQTTMixin, GNSSMixin, load_default_power_saving_mixin=False)
modem_rsp = WalterModemRsp()
# ---------------------------------------------------------------------------
async def wait_for_network_reg_state(timeout: int, *states: WalterModemNetworkRegState):
for _ in range(timeout):
if modem.get_network_reg_state() in states:
return True
await asyncio.sleep(1)
return False
# ---------------------------------------------------------------------------
async def lte_connect(_retry=False):
if modem.get_network_reg_state() in (
WalterModemNetworkRegState.REGISTERED_HOME,
WalterModemNetworkRegState.REGISTERED_ROAMING,
):
return True
if not await modem.set_op_state(WalterModemOpState.FULL):
print("Failed to set op state FULL"); return False
if not await modem.set_network_selection_mode(WalterModemNetworkSelMode.AUTOMATIC):
print("Failed network selection mode"); return False
print("Waiting for network registration …")
if not await wait_for_network_reg_state(
180,
WalterModemNetworkRegState.REGISTERED_HOME,
WalterModemNetworkRegState.REGISTERED_ROAMING,
):
if await modem.get_rat(rsp=modem_rsp):
next_rat = (WalterModemRat.NBIOT
if modem_rsp.rat == WalterModemRat.LTEM
else WalterModemRat.LTEM)
if not _retry:
print(f"Retrying with RAT {WalterModemRat.get_value_name(next_rat)}")
await modem.set_rat(next_rat)
await modem.reset()
return await lte_connect(True)
return False
return True
# ---------------------------------------------------------------------------
async def lte_disconnect():
"""Turn off RF to allow GNSS and battery savings."""
ok = await modem.set_op_state(WalterModemOpState.MINIMUM)
await asyncio.sleep(3)
return ok
# ---------------------------------------------------------------------------
async def gnss_assistance_update():
if not await modem.get_clock(rsp=modem_rsp):
if not await lte_connect():
return False
await modem.get_clock(rsp=modem_rsp)
if not await modem.gnss_assistance_get_status(rsp=modem_rsp):
print("Failed to retrieve GNSS assistance status")
return False
alm = modem_rsp.gnss_assistance.almanac
eph = modem_rsp.gnss_assistance.realtime_ephemeris
if (not alm.available) or (alm.time_to_update <= 0):
await lte_connect()
await modem.gnss_assistance_update(WalterModemGNSSAssistanceType.ALMANAC)
if (not eph.available) or (eph.time_to_update <= 0):
await lte_connect()
await modem.gnss_assistance_update(WalterModemGNSSAssistanceType.REALTIME_EPHEMERIS)
return True
# ---------------------------------------------------------------------------
async def get_gnss_fix(max_tries=5):
print("Requesting GNSS fix …")
gnss_fix = None
for i in range(max_tries):
print(f" Try {i+1}/{max_tries}")
await lte_disconnect()
ok = await modem.gnss_perform_action(
action=WalterModemGNSSAction.GET_SINGLE_FIX, rsp=modem_rsp
)
if not ok:
print(" Failed to request fix")
await asyncio.sleep(3)
continue
gnss_fix = await modem.gnss_wait_for_fix()
if gnss_fix and gnss_fix.estimated_confidence <= config.MAX_GNSS_CONFIDENCE:
print(f" ✅ Fix ({gnss_fix.latitude:.6f}, {gnss_fix.longitude:.6f})")
return gnss_fix
await asyncio.sleep(5)
return gnss_fix
# ---------------------------------------------------------------------------
async def setup():
print("\n=== Walter MQTT + GNSS CSV Publisher (Sleep edition) ===")
await modem.begin()
if not await modem.check_comm():
print("Modem communication failed"); return False
if config.SIM_PIN and not await modem.unlock_sim(pin=config.SIM_PIN):
print("SIM unlock failed"); return False
if not await modem.create_PDP_context(apn=config.CELL_APN):
print("Failed PDP context"); return False
if config.APN_USERNAME:
await modem.set_PDP_auth_params(
protocol=config.AUTHENTICATION_PROTOCOL,
user_id=config.APN_USERNAME,
password=config.APN_PASSWORD,
)
if not await lte_connect():
print("LTE attach failed"); return False
if not await modem.gnss_config():
print("Couldn't configure GNSS"); return False
await gnss_assistance_update()
return True
# ---------------------------------------------------------------------------
async def publish_csv_location(lat: float, lon: float, alt: float = 0.0):
payload = f"0,{lat:.6f},{lon:.6f},{alt:.1f}"
print("Publishing CSV:", payload)
if not await modem.mqtt_config(
user_name=config.MQTT_USERNAME,
password=config.MQTT_PASSWORD,
):
print("MQTT config failed"); return
if not await modem.mqtt_connect(
server_name=config.MQTT_SERVER_ADDRESS,
port=config.MQTT_PORT,
):
print("MQTT connect failed"); return
ok = await modem.mqtt_publish(
topic=AIO_CSV_TOPIC,
data=payload,
qos=1,
rsp=modem_rsp,
)
print("Publish result:", "OK" if ok else "FAIL")
await modem.mqtt_disconnect(rsp=modem_rsp)
# ---------------------------------------------------------------------------
async def main():
try:
if not await setup():
raise RuntimeError("Setup failed")
# Single publish cycle ------------------------------------------------
fix = await get_gnss_fix()
if fix:
await lte_connect()
altitude = getattr(fix, "altitude", 0.0) or 0.0
await publish_csv_location(fix.latitude, fix.longitude, altitude)
else:
print("No valid GNSS fix this round.")
# --------------------------------------------------------------------
print(f"\nEntering deep sleep for {config.SLEEP_TIME}s …")
await modem.set_op_state(WalterModemOpState.MINIMUM)
await modem.sleep(
sleep_time_ms=int(config.SLEEP_TIME * 1000),
light_sleep=False
)
# Never returns from deep sleep; resumes from reboot
# --------------------------------------------------------------------
except Exception as err:
print("ERROR:")
sys.print_exception(err)
await asyncio.sleep(10)
# ---------------------------------------------------------------------------
asyncio.run(main())
이 코드의 거의 모든 기능은 이전 데모와 완전히 동일합니다. 실제 변경 사항은 main() 루프에서 발생합니다. GPS 데이터를 수집하고 전송한 직후에 두 줄이 새로 추가되었습니다.
await modem.set_op_state(WalterModemOpState.MINIMUM)
await modem.sleep(
sleep_time_ms=int(config.SLEEP_TIME * 1000),
light_sleep=False
)
첫 번째 줄은 모뎀을 가장 낮은 작동 상태로 전환하고, 다음 줄은 모뎀과 ESP32를 지정된 시간 동안 절전 모드로 전환합니다. 이게 전부입니다!
우리가 제대로 잠을 잤는지 확인하기 위해, 매우 정확한 전력 측정 장비인 Otii Arc를 작동시켰습니다 . 오른쪽 이미지는 USB를 통해 5볼트로 전원을 공급했을 때의 한 주기 동안의 모습을 보여줍니다.

- 이야기는 월터가 GPS 신호를 잡으려고 시도하는 것으로 시작하는데, 이 과정에서 약 60mA의 전류가 소모됩니다.
- 그런 다음 LTE를 켜고 해당 데이터를 약 100mA의 전류를 소모하는 MQTT 브로커로 전송합니다.
- 일단 전송이 완료되면, 기기는 딥 슬립 모드로 전환되어 전류 소모량을 100μA 미만으로 낮춘 다음, 이 과정을 다시 시작합니다.
앞으로 어떻게 해야 할까요?
이제 우리는 Walter를 단순한 ESP32 보드에서 완벽하게 연결된 셀룰러 IoT 기기로 발전시켰습니다. Walter는 자체 GPS 위치를 파악하고, 4G를 통해 데이터를 업로드하며, 딥 슬립 모드 덕분에 배터리로 며칠 또는 몇 주 동안 작동할 수도 있습니다. 이 작은 보드 하나에 이 모든 기능을 담아낸 것은 정말 놀라운 일이며, 메이커 수준에서 사용 가능한 하드웨어의 발전상을 보여주는 증거입니다.
여기서 살펴본 예제들은 Walter와 그 라이브러리들이 할 수 있는 일의 극히 일부분에 불과합니다. Walter 공식 문서를 자세히 살펴보는 것이 좋습니다. 더 많은 기능을 실험해 보고 싶다면 라이브러리에서 제공하는 CoAP(Constrained Application Protocol) 기능을 확인해 보세요. CoAP는 MQTT와 유사한 프로토콜이지만 TCP 대신 UDP를 사용하며, 리소스가 제한적이거나 저전력 장치에 적합하도록 설계된 간소화된 메시지 구조를 가지고 있습니다. 실제로 CoAP는 더 빠르고 데이터 사용량도 적어, 대규모 또는 장기적인 IoT 환경을 구축할 때 자연스러운 업그레이드 경로가 될 수 있습니다.
이것으로 이번 가이드를 마무리하겠습니다. Walter를 이용해 멋진 것(추적기, 원격 센서 또는 4G 네트워크를 통해 작동하는 기타 장치)을 만드셨다면 저희에게 보여주세요! 또한 이 가이드에서 다룬 내용에 대해 도움이 필요하시면 아래 링크된 포럼에 글을 남겨주세요.
다음 시간까지, 즐거운 제작 시간 되세요!
튜토리얼의 원문을 참고하세요.
달리기 기록 출석 장치에 넣으려고 보고있다. 어렵다.
'ESP32' 카테고리의 다른 글
| ESP32-C6 RGB LED 제어 (0) | 2026.01.01 |
|---|---|
| CrowPanel ESP32 Display 3.5인치 모듈 터치 기능 동작하지 않을 때 (0) | 2026.01.01 |
| ESP32 CrowPanel 3.5인치 디스플레이 디지털 시계 (0) | 2026.01.01 |
| ESP32 3.5inch TFT-LCD 개발 환경 설정 - 문서로 제공할 것 (0) | 2026.01.01 |
| ESP32 및 ESP8266을 이용한 MicroPython 프로그래밍 (0) | 2025.12.29 |
| DS3231 모듈을 사용한 ESP32 실시간 시계 (1) | 2025.12.26 |
| ESP32와 OLED 디스플레이를 이용한 인터넷 시계 (0) | 2025.12.26 |
| ESP-IDF Programming Guide (0) | 2025.12.24 |
취업, 창업의 막막함, 외주 관리, 제품 부재!
당신의 고민은 무엇입니까? 현실과 동떨어진 교육, 실패만 반복하는 외주 계약,
아이디어는 있지만 구현할 기술이 없는 막막함.
우리는 알고 있습니다. 문제의 원인은 '명확한 학습, 실전 경험과 신뢰할 수 있는 기술력의 부재'에서 시작됩니다.
이제 고민을 멈추고, 캐어랩을 만나세요!
코딩(펌웨어), 전자부품과 디지털 회로설계, PCB 설계 제작, 고객(시장/수출) 발굴과 마케팅 전략으로 당신을 지원합니다.
제품 설계의 고수는 성공이 만든 게 아니라 실패가 만듭니다. 아이디어를 양산 가능한 제품으로!
귀사의 제품을 만드세요. 교육과 개발 실적으로 신뢰할 수 있는 파트너를 확보하세요.
캐어랩