
지난 여름, 친구들이 예고도 없이 우리 집 문 앞에 나타나 물총으로 나를 기습했다. 이번 여름에는 미리 대비를 해두기로 했다.
물총은 아이들에게 빼놓을 수 없는 여름철 필수품입니다. 많은 사람들이 잔디밭을 뛰어다니며 친구들에게 가차 없이 물을 뿌리던 추억을 간직하고 있습니다. 전장을 가로지르는 끊임없는 물줄기 덕분에 무더운 여름날이 훨씬 더 즐겁고 상쾌해집니다.
하지만 공대를 다니는 대학생인 지금은 예전만큼 여기저기 뛰어다닐 힘이 별로 없어요. 그래서 물총 로봇이라는 아이디어를 떠올렸죠. 바로 원격 조종이 가능한 로봇으로, 제가 대신 친구들(그리고 식물들)에게 물을 뿌려줄 수 있는 로봇이에요.
STEP 1. 준비물

전자 제품:
구동 모터 2개 Amazon
서보 모터 Amazon
ESP32 Amazon (참고: 코드를 약간 수정하면 어떤 ESP32를 사용해도 됩니다)
워터 펌프 Amazon
배터리 Amazon
벅 컨버터 Amazon
H 브리지 Amazon
방수 스위치 Amazon
전기 부품:
브레드보드 Amazon
전선 Amazon
전기 커넥터 Amazon
전원 분배 보드 Amazon
납땜용 납 Amazon
로봇 본체:
워터 튜브 Amazon
핫 글루건 Amazon
덕트 테이프 Amazon
너트와 볼트 Amazon
식품 용기 (주방에서 구함) Amazon
플라스틱 물병 (주방에서 구함)
3D 프린터 (선택 사항, DIY 대안은 2단계를 참조)
소프트웨어
Arduino IDE arduino.cc/en/software/
STEP 2. 전자 부품 연결하기


각 전자 부품은 물을 펌핑하거나 신호를 수신하는 등 간단한 작업을 수행합니다. 이러한 모든 전자 부품을 하나로 통합하면 정교한 로봇을 만들 수 있습니다.
이러한 전자 부품들을 연결하기 위해 XT 30 커넥터를 사용했습니다. 이 커넥터는 조립이 간편하고 안정적으로 연결할 수 있게 해 줍니다. 전선이 미리 연결된 커넥터를 구입하거나 직접 납땜할 수도 있습니다.
물과 전기는 당연히 섞이지 않으므로, 나중에 이 부품들을 방수 식품 용기에 넣을 것입니다.
연결 1: 배터리에서 스위치로
스위치는 배터리를 나머지 전자 부품들과 연결합니다. 스위치를 전환하면 로봇의 전원이 켜지거나 꺼집니다.
연결 2: 스위치에서 전원 분배 보드로
이 보드는 나머지 전자 부품들에 전원을 안정적으로 분배합니다.
연결 3: 전원 분배 보드에서 L298N (x2)으로
L298N 모터 드라이버의 12V와 GND를 전원 분배 보드에 연결하십시오. 그런 다음 나사 단자를 사용하여 모터 드라이버를 펌프 및 구동 모터에 연결하십시오.
연결 4: 전원 분배 보드에서 벅 컨버터로
ESP32와 서보는 5볼트 정격입니다. 배터리는 약 12볼트이므로, 벅 컨버터를 사용하여 5볼트를 효율적이고 안전하게 출력합니다.
연결 5: 벅 컨버터에서 서보로 연결
서보의 전원 핀과 접지 핀을 벅 컨버터의 출력 측에 연결하십시오.
연결 6: 벅 컨버터에서 ESP32로
벅 컨버터의 출력을 ESP32의 5V 및 GND 핀에 연결하십시오.
STEP 3. ESP32 배선하기

ESP32는 모든 모터에 신호를 전송합니다. ESP32에는 많은 핀이 있지만, 그중 일부는 다른 기능을 수행하므로 사용 중인 ESP32 보드의 핀 배열도를 확인하는 것이 중요합니다. 간단한 전기 연결을 위해 ESP32를 브레드보드에 배치했습니다.
이 단계에서는 ESP32의 핀을 모터 드라이버와 서보에 연결합니다. 점퍼선을 사용하면 브레드보드와 전자 부품을 쉽게 연결할 수 있습니다.
다음 핀을 사용했습니다:
// ------------------------------------------------------------
// LEFT MOTOR
// ------------------------------------------------------------
const int LEFT_IN1 = 4;
const int LEFT_IN2 = 5;
const int LEFT_PWM = 21;
// ------------------------------------------------------------
// RIGHT MOTOR
// ------------------------------------------------------------
const int RIGHT_IN1 = 6;
const int RIGHT_IN2 = 7;
const int RIGHT_PWM = 8;
// ------------------------------------------------------------
// PUMP
// ------------------------------------------------------------
const int PUMP_IN1 = 41;
const int PUMP_IN2 = 42;
const int PUMP_PWM = 2;
// ------------------------------------------------------------
// SERVO
// ------------------------------------------------------------
const int SERVO_PIN = 1;
STEP 4. 플랫폼 구축

이 프로젝트를 진행하던 중 3D 프린터가 고장 나서, 우리는 Instructables 특유의 꺾이지 않는 DIY 정신을 발휘해 집안 곳곳에 있는 물건들을 활용하여 3D 모델링 부품 대부분을 대체하기로 했습니다. 3D 프린터를 사용할 수 없는 경우를 위한 일반적인 대체 재료도 함께 목록으로 제시했습니다. 그렇지 않다면 3D 모델은 이 단계 하단에 첨부되어 있습니다.
로봇 본체 (1개)
로봇의 나머지 부분을 지지하고 모터와 바퀴를 부착할 지점을 제공하는 플랫폼.
DIY 대체품: 골판지 상자 - 음식 용기와 크기가 비슷한 골판지 상자를 찾아 본체 모델에 맞게 구멍을 자릅니다.
바퀴 (4개)
D자형 축에 맞는 구멍이 있는 원형 부품입니다. 아래에는 바퀴 CAD 파일이 두 개 있습니다. 하나는 모터에 연결되는 바퀴용이고, 다른 하나는 피동 바퀴(모터가 없는 바퀴)용입니다.
DIY 대체품: 병뚜껑 - 축이 들어갈 수 있도록 뚜껑 중앙에 구멍을 잘라냅니다. 그런 다음 핫멜트 접착제로 뚜껑을 축에 연결합니다.
축 (2개)
자유롭게 회전하면서 피동 바퀴를 부착할 수 있게 합니다.
DIY 대체품: 너트와 볼트 - 피동 바퀴와 본체에 볼트를 관통시킵니다. 그런 다음 반대편에 너트를 조입니다.
모터 홀더 (2개)
모터를 본체에 고정합니다. 너트와 볼트를 끼울 수 있는 구멍이 있습니다.
DIY 대체품: 케이블 타이와 핫멜트 접착제 - 모터 주변의 구멍에 케이블 타이를 끼워 고정합니다. 그런 다음 핫멜트 접착제로 연결 부위를 안정시킵니다.
참고: 이 부품들은 마찰 끼워 맞춤 방식으로 설계되었습니다. 부품이 너무 크게 출력된 경우 약간의 사포질이 필요할 수 있습니다.
STEP 5. 프로그래밍 및 제어

3. 프로그래밍 및 제어
Arduino IDE를 사용하여 C++로 모터, 서보 및 펌프 제어 프로그램을 작성했습니다. 이 프로그램을 직접 사용하려면 아래 코드를 다운로드한 후 Wi-Fi 인증 정보와 핀 번호를 변경하세요.
PC가 ESP로 명령을 전송할 수 있도록 웹 서버를 사용했으며, ESP는 이 명령에 따라 이동하고 물총을 조정하거나 물을 분사하는 등의 동작을 수행합니다. 이를 위해서는 Wifi 및 WebServer 라이브러리를 사용해야 하며, 이 라이브러리에는 네트워크 정보가 필요합니다.
참고: ssid와 password를 자신의 네트워크 인증 정보에 맞게 변경해야 합니다.
#include <WiFi.h>
#include <WebServer.h>
#include <ESP32Servo.h>
// ============================================================
// WIFI
// ============================================================
const char* ssid = "*********";
const char* password = "*********";
부품의 핀 번호를 확인한 후, 로봇이 실행할 함수(예: moveForward, setPumpPower)를 만들었습니다.
핀 번호를 전역 상수로 지정
참고: 자신의 배선에 맞게 이 값을 변경하세요.
// ------------------------------------------------------------
// LEFT MOTOR
// ------------------------------------------------------------
const int LEFT_IN1 = 4;
const int LEFT_IN2 = 5;
const int LEFT_PWM = 21;
// ------------------------------------------------------------
// RIGHT MOTOR
// ------------------------------------------------------------
const int RIGHT_IN1 = 6;
const int RIGHT_IN2 = 7;
const int RIGHT_PWM = 8;
// ------------------------------------------------------------
// PUMP
// ------------------------------------------------------------
const int PUMP_IN1 = 41;
const int PUMP_IN2 = 42;
const int PUMP_PWM = 2;
// ------------------------------------------------------------
// SERVO
// ------------------------------------------------------------
const int SERVO_PIN = 1;
함수 예시(모든 함수는 코드를 참조하세요):
void moveForward() {
// Left motor forward
digitalWrite(LEFT_IN1, HIGH);
digitalWrite(LEFT_IN2, LOW);
// Right motor forward
digitalWrite(RIGHT_IN1, LOW);
digitalWrite(RIGHT_IN2, HIGH);
ledcWrite(LEFT_PWM, motor_speed);
ledcWrite(RIGHT_PWM, motor_speed);
}
void setPumpPower(int percent) {
pumpPowerPercent = constrain(
percent,
0,
100
);
if (pumpFiring) {
int duty = map(
pumpPowerPercent,
0,
100,
0,
255
);
ledcWrite(PUMP_PWM, duty);
}
}
컴퓨터에서 전송된 명령을 처리하는 함수를 작성하세요.
void handleCommandRequest() {
if (!server.hasArg("c")) {
server.send(
400,
"text/plain",
"Missing command"
);
return;
}
String command = server.arg("c");
handleCommand(command);
server.send(
200,
"text/plain",
"OK"
);
}
// ============================================================
// STATUS
// ============================================================
void handleStatus() {
String response = "";
response += "{";
response += "\"motor_speed\":";
response += String(motor_speed);
response += ",";
response += "\"pump_power\":";
response += String(pumpPowerPercent);
response += ",";
response += "\"pump_firing\":";
response += pumpFiring ? "true" : "false";
response += ",";
response += "\"ip\":\"";
response += WiFi.localIP().toString();
response += "\"";
response += "}";
server.send(
200,
"application/json",
response
);
}
각 명령을 함수와 연결하는 if-else if 문 체인을 만드세요.
// ============================================================
// COMMAND PROCESSING
// ============================================================
void handleCommand(String cmd) {
cmd.trim();
Serial.print("Command: ");
Serial.println(cmd);
// ----------------------------------------------------------
// MOVEMENT
// ----------------------------------------------------------
if (cmd == "F") {
moveForward();
}
else if (cmd == "B") {
moveBackward();
}
else if (cmd == "L") {
turnLeft();
}
else if (cmd == "R") {
turnRight();
}
else if (cmd == "STOP") {
stopMotors();
}
// ----------------------------------------------------------
// SERVO
// ----------------------------------------------------------
else if (cmd.startsWith("SERVO:")) {
int angle = cmd.substring(6).toInt();
angle = constrain(
angle,
0,
180
);
gunServo.write(angle);
Serial.print("Servo: ");
Serial.println(angle);
}
// ----------------------------------------------------------
// MOTOR SPEED
// ----------------------------------------------------------
else if (cmd.startsWith("SPEED:")) {
int percent = cmd.substring(6).toInt();
setMotorSpeed(percent);
Serial.print("Motor speed: ");
Serial.println(percent);
}
// ----------------------------------------------------------
// PUMP
// ----------------------------------------------------------
else if (cmd == "FIRE:START") {
fire(true);
}
else if (cmd == "FIRE:STOP") {
fire(false);
}
else if (cmd.startsWith("PUMP:")) {
int percent = cmd.substring(5).toInt();
setPumpPower(percent);
Serial.print("Pump power: ");
Serial.println(percent);
}
else {
Serial.println("Unknown command");
}
}
제어 페이지 웹사이트 만들기
// ============================================================
// WEB CONTROL PAGE
// ============================================================
void handleRoot() {
String html = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta name="viewport"
content="width=device-width, initial-scale=1">
<title>ESP32-S3 Tank</title>
<style>
body {
font-family: Arial;
text-align: center;
background: #222;
color: white;
}
button {
width: 120px;
height: 70px;
margin: 8px;
font-size: 20px;
}
input {
width: 300px;
}
</style>
</head>
<body>
<h1>ESP32-S3 Tank</h1>
<h2>Movement</h2>
<div>
<button onclick="cmd('F')">
Forward
</button>
</div>
<div>
<button onclick="cmd('L')">
Left
</button>
<button onclick="cmd('STOP')">
STOP
</button>
<button onclick="cmd('R')">
Right
</button>
</div>
<div>
<button onclick="cmd('B')">
Backward
</button>
</div>
<h2>Motor Speed</h2>
<input
type="range"
min="0"
max="100"
value="50"
oninput="speed(this.value)"
>
<p id="speedValue">50%</p>
<h2>Servo</h2>
<input
type="range"
min="0"
max="180"
value="90"
oninput="servo(this.value)"
>
<p id="servoValue">90°</p>
<h2>Pump</h2>
<input
type="range"
min="0"
max="100"
value="50"
oninput="pumpPower(this.value)"
>
<p id="pumpValue">50%</p>
<button onclick="cmd('FIRE:START')">
FIRE
</button>
<button onclick="cmd('FIRE:STOP')">
PUMP OFF
</button>
<script>
function cmd(command) {
fetch(
'/cmd?c=' +
encodeURIComponent(command)
);
}
function speed(value) {
document.getElementById(
'speedValue'
).innerText = value + '%';
cmd('SPEED:' + value);
}
function servo(value) {
document.getElementById(
'servoValue'
).innerText = value + '°';
cmd('SERVO:' + value);
}
function pumpPower(value) {
document.getElementById(
'pumpValue'
).innerText = value + '%';
cmd('PUMP:' + value);
}
</script>
</body>
</html>
)rawliteral";
server.send(
200,
"text/html",
html
);
}
웹 서버용 HTTP 경로
// ==========================================================
// HTTP ROUTES
// ==========================================================
server.on(
"/",
HTTP_GET,
handleRoot
);
server.on(
"/cmd",
HTTP_GET,
handleCommandRequest
);
server.on(
"/status",
HTTP_GET,
handleStatus
);
server.onNotFound(
handleNotFound
);
모터, 서보, 펌프 및 Wi-Fi 연결을 초기화하고 설정하세요.
// ============================================================
// SETUP
// ============================================================
void setup() {
Serial.begin(115200);
delay(500);
Serial.println();
Serial.println("==============================");
Serial.println("ESP32-S3 TANK CONTROLLER");
Serial.println("==============================");
// ==========================================================
// SERVO
// ==========================================================
gunServo.setPeriodHertz(50);
gunServo.attach(
SERVO_PIN,
500,
2500
);
gunServo.write(90);
Serial.println("Servo initialized");
// ==========================================================
// MOTOR GPIO
// ==========================================================
pinMode(LEFT_IN1, OUTPUT);
pinMode(LEFT_IN2, OUTPUT);
pinMode(RIGHT_IN1, OUTPUT);
pinMode(RIGHT_IN2, OUTPUT);
// ==========================================================
// PUMP GPIO
// ==========================================================
pinMode(PUMP_IN1, OUTPUT);
pinMode(PUMP_IN2, OUTPUT);
// ==========================================================
// PWM
//
// Arduino ESP32 Core 3.x:
//
// ledcAttach(pin, frequency, resolution)
// ==========================================================
ledcAttach(
LEFT_PWM,
1000,
8
);
ledcAttach(
RIGHT_PWM,
1000,
8
);
ledcAttach(
PUMP_PWM,
1000,
8
);
// ==========================================================
// SAFE INITIAL STATE
// ==========================================================
stopMotors();
pumpOff();
setMotorSpeed(50);
// ==========================================================
// WIFI
// ==========================================================
WiFi.mode(WIFI_STA);
WiFi.begin(
ssid,
password
);
Serial.print("Connecting to WiFi");
while (
WiFi.status() != WL_CONNECTED
) {
delay(500);
Serial.print(".");
}
Serial.println();
Serial.println("WiFi connected!");
Serial.print("IP address: ");
Serial.println(
WiFi.localIP()
);
// ==========================================================
// START SERVER
// ==========================================================
server.begin();
Serial.println(
"HTTP server started"
);
Serial.println("==============================");
Serial.println("TANK CONTROLLER READY");
Serial.println("==============================");
Serial.print("Open: http://");
Serial.print(WiFi.localIP());
Serial.println("/");
}
프로그램을 실행하는 루프를 만드세요.
void loop() {
server.handleClient();
}
참고: Arduino IDE의 라이브러리 관리자에서 라이브러리를 설치해야 할 수도 있습니다.
STEP 6. 모든 것을 종합해 보면



ESP32 코드를 다운로드하고 수정한 후, USB-C 케이블을 통해 프로그램을 ESP32에 업로드하세요. 그 다음, 방수 식품 용기 안에 배터리, 전원 분배 보드, 모터 드라이버, 벅 컨버터, ESP32를 넣으세요. 그런 다음 모터를 장착할 구멍을 뚫고, 용기를 3D 프린팅으로 제작한 본체에 접착제로 고정하세요.
스위치, 서보, 펌프, 물병을 용기 뚜껑에 부착하고, 전선이 통과할 수 있도록 뚜껑에 구멍을 뚫습니다. 전선을 넣은 후 물이 새지 않도록 이 구멍들을 핫멜트 접착제로 메워주세요. 그런 다음 핫멜트 접착제와 덕트 테이프로 부품을 단단히 고정하세요. 작업이 끝나면 뚜껑을 식품 용기에 덮어 모든 것을 밀봉하세요.
그런 다음, 물병에 구멍을 뚫고 물 호스를 이용해 펌프에 연결합니다. 펌프의 출력을 서보 모터에 접착제로 붙이거나 묶어 발사 각도를 조절합니다. 마지막으로 구동 모터를 로봇 본체에 부착하고 바퀴를 장착합니다.
STEP 7. 친구들에게 스프레이 뿌리기 + 결론

물병을 채우고, 배터리를 충전한 다음, 물총 로봇을 보내 친구들(혹은 식물들)에게 물을 뿌려보세요! 이런 로봇을 몇 대만 있으면 뒷마당 전장을 완전히 장악할 수 있어요.
이 인스트럭터블의 끝이 이 프로젝트의 끝은 아닙니다. 다양한 노즐을 만들어 물이 얼마나 멀리 뿜어져 나오는지 확인해 보며 물총 로봇을 나만의 스타일로 꾸며보세요. 다양한 구동 시스템을 적용하거나, 나만의 프로그램을 만들거나, 무시무시한 함대를 구성하는 등 계속해서 실험해 볼 수 있습니다.
이 프로젝트를 통해 특히 두드러진 점은 공학의 다재다능함이었습니다. 3D 프린터가 고장 났을 때, 저는 이 프로젝트가 끝날 수도 있겠다고 생각했습니다. 하지만 재빨리 머리를 굴려 집 주변에 있는 이것저것으로 대신할 수 있는 물건을 찾아냈습니다. 창작자로서 우리는 어떤 일이든 해결할 방법을 찾아낼 수 있습니다. 무더운 여름날, (물 분사로) 친구와 가족을 시원하게 해주는 일을 막을 수 있는 건 아무것도 없습니다.
원문: https://www.instructables.com/DIY-Remote-Controlled-Water-Gun-Robot/