선이 사라지면 로봇이 달라진다
지금까지의 통신은 대부분 보드와 보드, 또는 보드와 PC 사이를 선으로 연결하는 방식이었다. 시리얼 통신은 PC와 보드를 연결했고, I2C와 SPI는 센서와 디스플레이를 보드 가까이에 붙여 두는 데 유용했다. 그런데 바퀴가 달린 로봇을 생각해 보자. 로봇이 앞으로 움직이는 순간 USB 케이블은 방해물이 된다. 바퀴에 감기거나, 로봇을 책상 아래로 끌어당기거나, 실험할 때마다 사람이 뒤따라가야 한다.
무선 통신은 이 답답함을 끊어 준다. 스마트폰 화면에서 버튼을 누르면 로봇이 앞으로 가고, 브라우저의 슬라이더를 움직이면 속도가 바뀐다. 방 안에서 테스트할 때는 노트북으로 조종하고, 책상 위에서는 스마트폰으로 조종할 수 있다. 이 장에서는 ESP32의 Wi-Fi와 Bluetooth 기능을 이용해 케이블 없이 로봇을 움직이는 방법을 배운다.
ESP32가 로봇 제작에서 사랑받는 이유 중 하나는 Wi-Fi와 Bluetooth가 보드 안에 들어 있다는 점이다. 별도의 무선 모듈을 붙이지 않아도 웹 서버를 열고, 스마트폰과 연결하고, 문자 명령을 받아 모터를 움직일 수 있다. 실제 ESP32 Wi-Fi 로봇 예제들도 웹 브라우저에 전진, 후진, 좌회전, 우회전, 정지 버튼과 속도 슬라이더를 만들고 이를 모터 제어 함수에 연결하는 구조를 자주 사용한다.1
이번 장의 목표는 화려한 앱을 만드는 것이 아니다. 더 중요한 것은 "무선 명령이 들어오면 로봇의 모터 함수가 호출된다"는 구조를 몸에 익히는 것이다. 이 구조만 이해하면 나중에 웹캠 영상, ROS2, AI 판단 결과, 음성 명령도 결국 같은 방식으로 로봇의 움직임에 연결할 수 있다.

Wi-Fi와 Bluetooth, 무엇이 다를까
Wi-Fi와 Bluetooth는 둘 다 선 없이 데이터를 주고받는 기술이지만, 로봇에서 맡기 좋은 역할은 조금 다르다.
Wi-Fi는 로봇을 작은 네트워크 장치처럼 다루게 해 준다. ESP32가 웹 서버가 되고, 스마트폰이나 노트북의 브라우저가 그 서버에 접속한다. 별도 앱을 만들지 않아도 주소창에 IP 주소를 입력하면 조종 화면이 열린다. 같은 네트워크에 있는 여러 장치에서 접속할 수도 있고, 영상 스트리밍이나 상태 대시보드처럼 데이터가 많은 기능으로 확장하기도 쉽다.
Bluetooth는 좀 더 단순한 조종기에 가깝다. 스마트폰과 ESP32를 페어링한 뒤, 터미널 앱이나 간단한 조종 앱에서 F, B, L, R, S 같은 짧은 문자를 보낸다. ESP32는 문자를 하나 읽고 그에 맞는 동작을 실행한다. 웹 페이지를 만들 필요가 없어서 처음 무선 조종을 테스트하기 좋다.
정리하면 Wi-Fi는 "브라우저 기반 조종 화면"에 강하고, Bluetooth는 "짧은 문자 명령 기반 조종"에 강하다. Wi-Fi는 IP 주소와 네트워크 연결을 신경 써야 하지만 화면 구성이 자유롭다. Bluetooth는 화면 구성은 앱에 의존하지만 명령 구조가 단순하다.
이 장에서는 두 방식을 모두 다룬다. 먼저 Wi-Fi로 웹 조종 화면을 만들고, 그다음 Bluetooth Serial로 문자 명령을 받는다. 재미있는 점은 두 방식 모두 마지막에는 같은 모터 함수로 이어진다는 것이다. 즉, forward(), backward(), turnLeft(), turnRight(), stopRobot() 함수만 잘 만들어 두면 Wi-Fi든 Bluetooth든 같은 로봇을 움직일 수 있다.
STA 모드와 AP 모드
ESP32를 Wi-Fi로 연결하는 방식은 크게 두 가지다. 첫 번째는 기존 공유기에 접속하는 STA 모드이고, 두 번째는 ESP32가 직접 Wi-Fi 네트워크를 만드는 AP 모드다.
STA는 Station의 줄임말이다. 스마트폰이나 노트북이 집의 공유기에 접속하듯 ESP32도 공유기에 접속한다. 공유기는 ESP32에게 IP 주소를 하나 배정하고, 같은 공유기에 연결된 스마트폰은 그 IP 주소로 ESP32에 접속한다. 예를 들어 시리얼 모니터에 192.168.0.42가 출력되면 스마트폰 브라우저에서 http://192.168.0.42를 열면 된다.
AP는 Access Point의 줄임말이다. 이때 ESP32는 작은 공유기처럼 동작한다. 스마트폰 Wi-Fi 목록에 ESP32_ROBOT 같은 이름이 나타나고, 사용자는 그 네트워크에 직접 접속한다. ESP32가 AP 모드로 동작할 때는 보통 192.168.4.1 같은 주소로 접속한다. ESP32를 스마트폰과 직접 연결하는 AP 모드는 인터넷이나 집 공유기가 없어도 실습할 수 있다는 장점이 있다.2
| 구분 | STA 모드 | AP 모드 |
|---|---|---|
| 연결 방식 | ESP32가 기존 공유기에 접속 | ESP32가 직접 Wi-Fi 네트워크 생성 |
| 필요한 장비 | 공유기 필요 | 공유기 없어도 가능 |
| 스마트폰 연결 | 같은 공유기에 접속 | 스마트폰이 ESP32 Wi-Fi에 직접 접속 |
| IP 확인 | 시리얼 모니터에서 WiFi.localIP() 확인 | WiFi.softAPIP() 확인, 보통 192.168.4.1 |
| 장점 | 노트북, 스마트폰, 라즈베리파이와 같은 네트워크에서 함께 사용하기 좋음 | 야외나 교실처럼 공유기 환경이 불안정할 때 편함 |
| 단점 | 공유기 비밀번호와 네트워크 상태에 의존 | 스마트폰이 ESP32 AP에 접속하는 동안 인터넷이 끊길 수 있음 |
처음 실습한다면 AP 모드가 편하다. 공유기 비밀번호를 코드에 넣을 필요가 없고, 어디서든 ESP32가 만든 네트워크에 붙으면 된다. 반대로 라즈베리파이, PC, 카메라 스트리밍 서버와 함께 실험한다면 STA 모드가 더 자연스럽다. 같은 공유기 안에서 서로 IP 주소로 찾아갈 수 있기 때문이다.
회로 준비: 무선보다 먼저 전원과 GND
무선 조종이라고 해서 전원 문제가 사라지는 것은 아니다. 오히려 로봇이 자유롭게 움직이기 때문에 전원이 더 중요해진다. USB 케이블을 빼고 배터리로 움직이기 시작하면, 모터가 순간적으로 큰 전류를 요구할 때 ESP32가 재부팅되는 일이 흔하다.
이 장의 예제는 ESP32와 2개의 DC 모터, 그리고 L298N이나 TB6612 같은 2채널 모터 드라이버를 기준으로 설명한다. 핀 이름은 드라이버마다 조금 다르지만 구조는 비슷하다. 방향을 정하는 입력 핀 2개와 속도를 정하는 PWM 핀 1개가 한 모터에 대응된다.
예제 배선은 다음처럼 잡아 보자.
| 역할 | ESP32 GPIO | 모터 드라이버 쪽 이름 예시 |
|---|---|---|
| 왼쪽 모터 방향 1 | 27 | IN1 또는 AIN1 |
| 왼쪽 모터 방향 2 | 26 | IN2 또는 AIN2 |
| 왼쪽 모터 속도 PWM | 14 | ENA 또는 PWMA |
| 오른쪽 모터 방향 1 | 33 | IN3 또는 BIN1 |
| 오른쪽 모터 방향 2 | 25 | IN4 또는 BIN2 |
| 오른쪽 모터 속도 PWM | 32 | ENB 또는 PWMB |
| 공통 기준 전압 | GND | GND |
여기서 가장 중요한 선은 의외로 GND다. ESP32와 모터 드라이버가 서로 다른 배터리에서 전원을 받더라도 GND는 반드시 연결되어야 한다. 그래야 ESP32가 보내는 HIGH와 LOW 신호를 모터 드라이버가 같은 기준으로 이해할 수 있다.
모터 전원은 ESP32의 3.3V 핀에서 가져오면 안 된다. DC 모터는 출발 순간에 큰 전류를 요구한다. ESP32 보드의 3.3V 출력은 센서나 작은 모듈용이지 모터용이 아니다. 모터 전원은 별도 배터리나 충분한 전류를 낼 수 있는 전원에서 공급하고, ESP32는 USB 보조 배터리나 안정적인 5V 입력으로 공급하는 편이 안전하다. 앞의 전원 장에서 배운 것처럼 "모터 전원과 보드 전원 분리, GND 공유"는 무선 로봇에서도 그대로 적용된다.
모터 제어 함수를 먼저 만든다
웹 서버나 Bluetooth 코드를 바로 쓰고 싶겠지만, 먼저 모터 제어 함수를 분리해 두는 편이 좋다. 무선 통신 코드는 명령을 받는 역할만 하고, 실제 움직임은 모터 함수가 맡도록 나누는 것이다.
아래 코드는 ESP32 Arduino Core 3.x 기준으로 ledcAttach()와 ledcWrite()를 사용한다. 만약 사용하는 보드 패키지에서 이 함수가 동작하지 않는다면, 10장에서 배운 PWM 설정 방식에 맞게 ledcSetup(), ledcAttachPin() 또는 analogWrite() 방식으로 바꾸면 된다.
const int L_IN1 = 27;
const int L_IN2 = 26;
const int L_PWM = 14;
const int R_IN1 = 33;
const int R_IN2 = 25;
const int R_PWM = 32;
const int PWM_FREQ = 20000;
const int PWM_RES = 8; // 0~255
int speedPercent = 60; // 기본 속도
float leftTrim = 1.00; // 왼쪽 모터 보정
float rightTrim = 0.95; // 오른쪽 모터 보정
int percentToPwm(int percent) {
percent = constrain(percent, 0, 100);
return map(percent, 0, 100, 0, 255);
}
void writeOneMotor(int pwmPin, int in1, int in2, int power) {
power = constrain(power, -255, 255);
if (power > 0) {
digitalWrite(in1, HIGH);
digitalWrite(in2, LOW);
} else if (power < 0) {
digitalWrite(in1, LOW);
digitalWrite(in2, HIGH);
} else {
digitalWrite(in1, LOW);
digitalWrite(in2, LOW);
}
ledcWrite(pwmPin, abs(power));
}
void drive(int leftPower, int rightPower) {
writeOneMotor(L_PWM, L_IN1, L_IN2, leftPower);
writeOneMotor(R_PWM, R_IN1, R_IN2, rightPower);
}
void stopRobot() {
drive(0, 0);
}
void forward() {
int p = percentToPwm(speedPercent);
drive((int)(p * leftTrim), (int)(p * rightTrim));
}
void backward() {
int p = percentToPwm(speedPercent);
drive((int)(-p * leftTrim), (int)(-p * rightTrim));
}
void turnLeft() {
int p = percentToPwm(speedPercent);
drive((int)(-p * 0.7), (int)(p * 0.7));
}
void turnRight() {
int p = percentToPwm(speedPercent);
drive((int)(p * 0.7), (int)(-p * 0.7));
}
void setupMotors() {
pinMode(L_IN1, OUTPUT);
pinMode(L_IN2, OUTPUT);
pinMode(R_IN1, OUTPUT);
pinMode(R_IN2, OUTPUT);
ledcAttach(L_PWM, PWM_FREQ, PWM_RES);
ledcAttach(R_PWM, PWM_FREQ, PWM_RES);
stopRobot();
}이 코드에서 drive(leftPower, rightPower)는 왼쪽과 오른쪽 모터의 출력을 각각 받는다. 양수면 전진 방향, 음수면 후진 방향, 0이면 정지다. 이 구조를 쓰면 직진, 후진, 회전을 모두 숫자 두 개로 표현할 수 있다.
만약 forward()를 호출했는데 한쪽 바퀴가 반대로 돈다면 하드웨어와 소프트웨어 중 한 곳에서 방향을 뒤집으면 된다. 가장 쉬운 방법은 해당 모터의 두 선을 서로 바꾸는 것이다. 소프트웨어에서 고치고 싶다면 그 모터의 in1, in2 HIGH/LOW 관계를 반대로 바꾸면 된다.
Wi-Fi 웹 서버 만들기
이제 ESP32 안에 작은 웹 서버를 열어 보자. 브라우저가 ESP32에 접속하면 HTML 페이지를 받고, 사용자가 버튼을 누르면 /cmd?v=F 같은 HTTP 요청이 ESP32로 전달된다. ESP32는 F를 전진 명령으로 해석해 forward()를 호출한다.
먼저 Wi-Fi 연결을 시작하는 함수를 만든다. 아래 코드에서 USE_AP_MODE를 true로 두면 AP 모드, false로 두면 STA 모드가 된다.
#include <WiFi.h>
#include <WebServer.h>
WebServer server(80);
const bool USE_AP_MODE = true;
const char* STA_SSID = "YOUR_WIFI_SSID";
const char* STA_PASS = "YOUR_WIFI_PASSWORD";
const char* AP_SSID = "ESP32_ROBOT";
const char* AP_PASS = "12345678"; // AP 비밀번호는 8자 이상
void startWiFi() {
if (USE_AP_MODE) {
WiFi.mode(WIFI_AP);
WiFi.softAP(AP_SSID, AP_PASS);
Serial.print("AP IP: ");
Serial.println(WiFi.softAPIP());
} else {
WiFi.mode(WIFI_STA);
WiFi.begin(STA_SSID, STA_PASS);
Serial.print("Connecting to Wi-Fi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println();
Serial.print("STA IP: ");
Serial.println(WiFi.localIP());
}
}AP 모드로 업로드했다면 스마트폰 Wi-Fi 설정에서 ESP32_ROBOT을 찾아 접속한다. 그다음 브라우저에서 시리얼 모니터에 출력된 AP IP를 연다. STA 모드라면 스마트폰과 ESP32가 같은 공유기에 연결되어 있어야 한다.
버튼 요청을 모터 함수에 연결하기
이제 웹 페이지를 만든다. 화려할 필요는 없다. 전진, 후진, 좌회전, 우회전, 정지 버튼과 속도 슬라이더만 있으면 충분하다.
void handleRoot() {
String html = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: sans-serif; text-align: center; margin-top: 30px; }
button { font-size: 26px; padding: 14px 24px; margin: 6px; }
.stop { background: #333; color: white; }
input { width: 280px; }
</style>
</head>
<body>
<h2>ESP32 Robot Controller</h2>
<p><button onclick="sendCmd('F')">전진</button></p>
<p>
<button onclick="sendCmd('L')">좌회전</button>
<button class="stop" onclick="sendCmd('S')">정지</button>
<button onclick="sendCmd('R')">우회전</button>
</p>
<p><button onclick="sendCmd('B')">후진</button></p>
<p>속도: <span id="speedValue">60</span>%</p>
<input type="range" min="0" max="100" value="60"
oninput="setSpeed(this.value)">
<script>
function sendCmd(cmd) {
fetch('/cmd?v=' + cmd);
}
function setSpeed(value) {
document.getElementById('speedValue').innerText = value;
fetch('/speed?v=' + value);
}
</script>
</body>
</html>
)rawliteral";
server.send(200, "text/html", html);
}버튼을 누르면 브라우저는 ESP32에 HTTP 요청을 보낸다. fetch('/cmd?v=F')는 현재 ESP32 주소의 /cmd 경로로 v=F라는 값을 보내는 코드다. ESP32는 이 값을 읽어 모터 함수를 호출한다.
unsigned long lastCommandTime = 0;
const unsigned long COMMAND_TIMEOUT = 800;
bool timeoutStopped = false;
void markCommandReceived() {
lastCommandTime = millis();
timeoutStopped = false;
}
void runCommand(String cmd) {
markCommandReceived();
if (cmd == "F") {
forward();
} else if (cmd == "B") {
backward();
} else if (cmd == "L") {
turnLeft();
} else if (cmd == "R") {
turnRight();
} else if (cmd == "S") {
stopRobot();
} else {
stopRobot(); // 모르는 명령은 안전하게 정지
}
}
void handleCommand() {
String cmd = server.arg("v");
runCommand(cmd);
server.send(200, "text/plain", "OK " + cmd);
}
void handleSpeed() {
int value = server.arg("v").toInt();
speedPercent = constrain(value, 0, 100);
markCommandReceived();
server.send(200, "text/plain", "SPEED " + String(speedPercent));
}
void setup() {
Serial.begin(115200);
setupMotors();
startWiFi();
server.on("/", handleRoot);
server.on("/cmd", handleCommand);
server.on("/speed", handleSpeed);
server.begin();
lastCommandTime = millis();
stopRobot();
}
void loop() {
server.handleClient();
if (!timeoutStopped && millis() - lastCommandTime > COMMAND_TIMEOUT) {
stopRobot();
timeoutStopped = true;
}
}여기서 눈여겨볼 부분은 runCommand()다. Wi-Fi 요청이 들어오든, 나중에 Bluetooth 문자가 들어오든, 결국 이 함수에 F, B, L, R, S를 넘기면 된다. 통신 방식과 로봇 동작을 분리했기 때문에 코드가 훨씬 깔끔해진다.
HTTP 버튼 방식은 이해하기 쉽고 브라우저만 있으면 동작한다. 다만 버튼을 아주 빠르게 누르거나 조이스틱처럼 부드러운 조작을 하고 싶다면 WebSocket 방식이 더 적합하다. WebSocket은 브라우저와 ESP32 사이에 연결을 유지한 채 명령을 보내므로, 매번 페이지 요청을 새로 처리하는 방식보다 실시간 조종 화면에 유리하다.3 하지만 처음에는 HTTP 요청 방식으로 구조를 익히는 것이 좋다. 로봇이 움직이는 원리는 둘 다 같다.
속도 슬라이더와 좌우 보정
브라우저의 속도 슬라이더는 0부터 100까지의 값을 보낸다. ESP32는 이를 PWM 듀티비 0부터 255로 변환한다. 이 값이 커질수록 모터에 전원이 공급되는 시간이 길어지고, 평균적으로 더 빠르게 회전한다.
하지만 슬라이더를 20%로 둔다고 해서 모터가 반드시 천천히 부드럽게 도는 것은 아니다. DC 모터에는 출발에 필요한 최소 힘이 있다. 너무 작은 PWM에서는 모터가 "윙" 소리만 내고 돌지 않을 수 있다. 이럴 때는 실험을 통해 실제로 바퀴가 돌기 시작하는 최소 속도를 찾아야 한다.
예를 들어 20% 이하는 정지처럼 동작하고 30%부터 안정적으로 돈다면, 코드에서 최소 구동값을 줄 수 있다.
int percentToPwm(int percent) {
percent = constrain(percent, 0, 100);
if (percent == 0) return 0;
int pwm = map(percent, 1, 100, 80, 255);
return pwm;
}이렇게 하면 슬라이더가 1% 이상일 때 PWM이 최소 80부터 시작한다. 단, 로봇이 갑자기 튀어나갈 수 있으므로 처음에는 바퀴를 공중에 띄우고 테스트하자.
좌우 모터 보정도 중요하다. 같은 PWM 값을 줘도 두 모터가 정확히 같은 속도로 돌지는 않는다. 모터의 제조 편차, 바퀴 마찰, 프레임 조립 상태, 배터리 상태가 모두 영향을 준다. 그래서 예제 코드에는 leftTrim과 rightTrim을 넣었다.
로봇이 전진할 때 오른쪽으로 조금씩 휜다면 왼쪽 바퀴가 더 빠르거나 오른쪽 바퀴가 더 느린 것이다. 이때 leftTrim을 0.95로 낮추거나 rightTrim을 1.00으로 올려 보며 조정한다. 반대로 왼쪽으로 휜다면 오른쪽 출력을 줄인다.
정확한 직진은 나중에 엔코더와 PID 제어로 해결할 수 있다. 하지만 무선 조종 단계에서는 좌우 보정값만 잘 잡아도 조작감이 크게 좋아진다. 로봇이 내 손가락을 따라 자연스럽게 움직이기 시작하면 그때부터 실험이 훨씬 재미있어진다.
Bluetooth Serial로 문자 명령 받기
이번에는 Bluetooth를 사용해 보자. ESP32의 Bluetooth Serial은 스마트폰에서 보낸 문자를 시리얼 데이터처럼 읽을 수 있게 해 준다. Arduino IDE의 ESP32 예제에도 Serial과 Bluetooth Serial을 서로 이어 주는 기본 예제가 제공되며, 스마트폰 터미널 앱에서 ESP32 이름을 선택해 문자를 주고받는 식으로 테스트할 수 있다.4
주의할 점이 하나 있다. 여기서 사용하는 BluetoothSerial은 클래식 Bluetooth SPP 방식이다. 일반적인 ESP32-WROOM-32 계열에서는 사용할 수 있지만, ESP32-C3나 ESP32-S3처럼 BLE 중심인 보드에서는 같은 코드가 동작하지 않을 수 있다. 그런 보드는 BLE UART 예제로 접근해야 한다. 이 책의 실습에서는 가장 널리 쓰이는 ESP32 DevKit 계열을 기준으로 설명한다.
Bluetooth 제어는 웹 페이지보다 훨씬 단순하다. 스마트폰 터미널 앱에서 다음 문자를 보낸다고 정하자.
| 문자 | 동작 |
|---|---|
F | 전진 |
B | 후진 |
L | 좌회전 |
R | 우회전 |
S | 정지 |
0~9 | 속도 0~90% 설정 |
이미 runCommand() 함수가 있으므로 Bluetooth 코드는 문자를 읽어 그 함수로 넘기기만 하면 된다.
#include "BluetoothSerial.h"
BluetoothSerial SerialBT;
void setup() {
Serial.begin(115200);
setupMotors();
SerialBT.begin("ESP32_ROBOT_BT");
Serial.println("Bluetooth ready. Pair with ESP32_ROBOT_BT");
lastCommandTime = millis();
stopRobot();
}
void loop() {
if (SerialBT.available()) {
char c = SerialBT.read();
if (c >= '0' && c <= '9') {
speedPercent = (c - '0') * 10;
markCommandReceived();
SerialBT.println("Speed: " + String(speedPercent));
} else {
runCommand(String(c));
SerialBT.println("Command: " + String(c));
}
}
if (!timeoutStopped && millis() - lastCommandTime > COMMAND_TIMEOUT) {
stopRobot();
timeoutStopped = true;
}
}이 코드는 Wi-Fi 서버 코드와 동시에 섞어 쓸 수도 있다. 즉, ESP32가 웹 조종도 받고 Bluetooth 명령도 받게 만들 수 있다. 다만 처음부터 둘을 모두 켜면 디버깅이 어려워질 수 있으니, 먼저 Wi-Fi만 테스트하고 그다음 Bluetooth만 테스트한 뒤 마지막에 합치는 것이 좋다.
스마트폰에서는 "Serial Bluetooth Terminal" 같은 앱을 설치하고, Bluetooth 설정에서 ESP32_ROBOT_BT를 페어링한다. 앱에서 F를 보내면 전진, S를 보내면 정지해야 한다. 여기서도 처음 테스트할 때는 바퀴를 바닥에서 띄워 두자. 무선 로봇은 예상보다 빨리 책상 끝으로 달려간다.
지연, 손실, 끊김을 고려한 안전 로직
유선 버튼은 누르는 순간 바로 신호가 들어간다. 하지만 무선 통신은 항상 완벽하지 않다. 공유기 상태가 나쁘면 Wi-Fi 요청이 늦게 도착할 수 있고, 스마트폰이 절전 모드에 들어가면 명령 전송이 멈출 수 있다. Bluetooth도 거리가 멀어지거나 주변 전파가 복잡하면 끊길 수 있다.
그래서 무선 로봇에는 안전 로직이 반드시 필요하다. 가장 기본은 정지 버튼이다. 웹 화면 중앙에 정지 버튼을 크게 두고, Bluetooth 앱에서도 S를 쉽게 보낼 수 있어야 한다. 하지만 정지 버튼만으로는 부족하다. 사용자가 정지 버튼을 누르기 전에 연결이 끊길 수 있기 때문이다.
그래서 두 번째 안전장치가 타임아웃이다. 마지막 명령을 받은 뒤 일정 시간이 지나도록 새 명령이 없으면 로봇이 자동으로 멈춘다. 앞의 코드에서 COMMAND_TIMEOUT을 800ms로 둔 이유가 이것이다. 0.8초 동안 아무 명령이 없으면 ESP32는 "조종 신호가 사라졌다"고 판단하고 stopRobot()을 호출한다.
세 번째는 시작과 재연결 시 초기 정지다. ESP32가 켜졌을 때 이전 상태를 기억해서 갑자기 전진하면 위험하다. 프로그램 시작 시에는 무조건 stopRobot()을 호출해야 한다. Wi-Fi가 다시 연결되거나 Bluetooth가 다시 페어링되었을 때도 처음 명령을 받기 전까지는 정지 상태가 안전하다.
실전에서는 다음 규칙을 습관처럼 넣자.
모르는 명령을 받으면 무시하지 말고 정지한다.
일정 시간 새 명령이 없으면 자동 정지한다.
부팅 직후와 재연결 직후에는 항상 정지 상태로 시작한다.
속도 기본값은 낮게 두고, 테스트가 끝난 뒤 조금씩 올린다.
로봇 전원선에는 물리 스위치를 넣어 언제든 모터 전원을 끊을 수 있게 한다.
무선 조종에서 가장 무서운 상황은 로봇이 "마지막 명령을 계속 실행하는 것"이다. 전진 명령을 받은 직후 연결이 끊겼는데 로봇이 계속 앞으로 달린다면, 그것은 장난감이 아니라 작은 사고 장치가 된다. 안전한 로봇은 똑똑한 로봇보다 먼저다.
실습 순서: 한 번에 다 하지 않기
무선 로봇을 만들 때 흔한 실수는 회로, 모터, 웹 서버, Bluetooth, 배터리를 한 번에 조립하고 "왜 안 되지?"라고 고민하는 것이다. 로봇은 작은 문제가 여러 개 겹치면 원인을 찾기 어려워진다. 다음 순서로 나누어 테스트하자.
먼저 USB 케이블을 연결한 상태에서 모터 함수만 테스트한다. forward(), backward(), turnLeft(), turnRight(), stopRobot()을 2초 간격으로 호출해 바퀴 방향이 맞는지 확인한다. 이 단계에서 방향이 반대인 모터를 고쳐야 한다.
그다음 Wi-Fi 연결만 테스트한다. AP 모드라면 스마트폰에서 ESP32 네트워크가 보이는지 확인하고, STA 모드라면 시리얼 모니터에 IP 주소가 찍히는지 확인한다. 이 단계에서는 모터를 움직이지 않아도 된다.
세 번째로 웹 버튼을 눌렀을 때 시리얼 모니터에 F, B, L, R, S가 출력되는지 확인한다. 아직 모터를 연결하지 않아도 된다. 명령이 제대로 들어오는지 먼저 보는 것이다.
마지막으로 모터 전원을 연결하고 바퀴를 공중에 띄운 뒤 웹 조종을 테스트한다. 방향이 맞고 정지 버튼이 잘 먹으면 바닥에 내려놓는다. 속도는 30~40%부터 시작해 조금씩 올린다.
Bluetooth도 같은 순서다. 먼저 스마트폰에서 문자가 들어오는지 시리얼 모니터로 확인하고, 그다음 runCommand()에 연결한다. 무선 명령과 모터 구동을 단계별로 붙이면 문제를 훨씬 쉽게 찾을 수 있다.
자주 생기는 문제와 해결 방법
스마트폰에서 ESP32 AP가 보이지 않는다면 먼저 ESP32가 정상 부팅했는지 확인하자. 시리얼 모니터에 AP IP가 출력되어야 한다. AP 비밀번호는 8자 이상이어야 하며, 너무 짧으면 AP 생성이 실패할 수 있다. 보드가 계속 재부팅된다면 모터 전원이 ESP32 전원에 영향을 주고 있을 가능성이 크다.
STA 모드에서 IP 주소가 출력되지 않는다면 SSID와 비밀번호를 다시 확인한다. ESP32는 일반적으로 2.4GHz Wi-Fi를 사용한다. 공유기가 5GHz 전용 네트워크라면 접속되지 않을 수 있다. 스마트폰과 ESP32가 같은 공유기에 붙어 있는지도 확인해야 한다.
웹 페이지는 열리는데 로봇이 움직이지 않는다면 브라우저 버튼이 실제로 요청을 보내는지 시리얼 출력으로 확인한다. 요청은 들어오는데 모터가 돌지 않는다면 모터 드라이버 전원, ENA/ENB PWM 핀, GND 공유를 확인한다. 특히 L298N 계열은 점퍼 설정에 따라 ENA/ENB가 보드의 5V에 고정되어 있거나 외부 PWM 입력을 받도록 되어 있으니 모듈 상태를 살펴봐야 한다.
로봇이 전진 명령에서 제자리 회전을 한다면 한쪽 모터 방향이 반대다. 모터 선을 바꾸거나 소프트웨어의 방향 설정을 반대로 바꾼다. 좌회전과 우회전이 바뀌어 있다면 왼쪽과 오른쪽 모터 정의가 서로 바뀌었을 가능성이 있다.
Bluetooth 장치가 스마트폰에 보이지 않는다면 사용하는 보드가 Bluetooth Classic을 지원하는지 확인한다. ESP32-C3나 ESP32-S3 계열에서는 BluetoothSerial 예제가 맞지 않을 수 있다. 이 경우 BLE 기반 예제를 사용해야 한다.
조종이 늦게 반응한다면 Wi-Fi 신호가 약하거나 HTTP 요청이 너무 자주 쌓이고 있을 수 있다. 버튼을 누를 때마다 명령을 보내는 단순 방식에서는 큰 문제가 없지만, 조이스틱처럼 연속 값을 보내려면 요청 간격을 제한하거나 WebSocket 구조로 바꾸는 편이 좋다.
무선 조종 구조의 핵심
이번 장에서 만든 로봇은 아직 자율 주행 로봇은 아니다. 사람이 버튼을 누르면 그 방향으로 움직이는 원격 조종 로봇이다. 하지만 이 구조는 앞으로 계속 확장된다.
카메라가 색상 물체를 찾았을 때 turnLeft()를 호출할 수 있다. 라즈베리파이가 경로를 계산한 뒤 ESP32에 F 명령을 보낼 수 있다. 웹 화면에 배터리 전압, 거리 센서값, IMU 기울기 값을 표시할 수도 있다. Bluetooth 앱의 버튼 대신 게임패드, 음성 인식, ROS2 토픽이 명령의 출발점이 될 수도 있다.
중요한 것은 통신과 제어를 분리하는 습관이다. Wi-Fi는 명령을 받아오는 길이고, Bluetooth도 명령을 받아오는 길이다. 로봇을 실제로 움직이는 것은 결국 모터 제어 함수다. 이 경계를 잘 나누면 프로젝트가 커져도 코드가 무너지지 않는다.
다음 장에서는 라즈베리파이와 마이크로컨트롤러를 함께 사용하는 구조를 다룬다. 라즈베리파이는 카메라 처리나 고수준 판단을 맡고, ESP32나 아두이노는 모터와 센서를 실시간으로 제어한다. 이번 장에서 만든 무선 명령 구조는 그 연결의 출발점이 된다.
참고
1ESP32 Remote-Controlled Wi-Fi Car Robot (Arduino)
2How to Configure ESP32 with a Smartphone: Ultimate WiFi & Bluetooth Guide (2025)
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.