Dự án thú vị này kết hợp cảm biến màu sắc TCS3200D/TCS230 với nhân vật Minion trên nền web. Arduino UNO R4 WiFi đọc màu sắc từ cảm biến và gửi màu đã nhận diện đến trình duyệt web qua WebSocket. Minion trên trang web thay đổi màu da theo thời gian thực để khớp với bất kỳ màu sắc nào bạn đặt trước cảm biến! Để dễ dàng xây dựng giao diện web và xử lý giao tiếp thời gian thực, chúng ta sẽ sử dụng Arduino Custom WebApp Ví Dụ - Hướng Dẫn Giao Diện Web Đơn Giản Cho Người Mới Bắt Đầu.
Chúng tôi cũng cung cấp video hướng dẫn từng bước ở cuối tutorial này.
| 1 | × | Arduino UNO R4 WiFi | | |
| 1 | × | Alternatively, DIYables STEM V4 IoT | | |
| 1 | × | (Tùy chọn) DIYables STEM V4 IoT | | |
| 1 | × | Cáp USB Type-C | | |
| 1 | × | Module Cảm Biến Nhận Diện Màu Sắc TCS3200D/TCS230 | | |
| 1 | × | Dây Nối Jumper | | |
| 1 | × | (Khuyến nghị) Screw Terminal Block Shield for Arduino UNO R4 | | |
| 1 | × | (Khuyến nghị) Breadboard Shield for Arduino UNO R4 | | |
| 1 | × | (Khuyến nghị) Enclosure for Arduino UNO R4 | | |
| 1 | × | (Khuyến nghị) Power Splitter for Arduino UNO R4 | | |
| 1 | × | (Khuyến nghị) Prototyping Base Plate & Breadboard Kit for Arduino UNO | | |
Or you can buy the following kits:
| 1 | × | DIYables STEM V4 IoT Starter Kit (Arduino included) | | |
| 1 | × | DIYables Sensor Kit (30 sensors/displays) | | |
| 1 | × | DIYables Sensor Kit (18 sensors/displays) | | |
Hình này cho thấy cách kết nối cảm biến màu sắc TCS3200 với Arduino UNO R4 WiFi:
| Cảm Biến Màu Sắc TCS3200 | Arduino UNO R4 |
| VCC | 5V |
| GND | GND |
| S0 | Pin 4 |
| S1 | Pin 3 |
| S2 | Pin 6 |
| S3 | Pin 5 |
| OUT | Pin 7 |

This image is created using Fritzing. Click to enlarge image
Mã bao gồm 4 file:
ColorSensor.ino - Sketch Arduino chính: đọc cảm biến màu sắc và gửi màu sắc đến trang web
CustomWebApp.h - File header: định nghĩa lớp trang ứng dụng web tùy chỉnh
CustomWebApp.cpp - File triển khai: xử lý giao tiếp WebSocket với định danh "Color Sensor:"
custom_page_html.h - Trang web: chứa HTML/CSS/JavaScript của Minion hoạt hình nhận màu sắc và cập nhật màu da Minion
ColorSensor.ino
#include <DIYablesWebApps.h>
#include "CustomWebApp.h"
const char WIFI_SSID[] = "YOUR_WIFI_SSID";
const char WIFI_PASSWORD[] = "YOUR_WIFI_PASSWORD";
int status = WL_IDLE_STATUS;
const int S0 = 4, S1 = 3, S2 = 6, S3 = 5, sensorOut = 7;
UnoR4ServerFactory serverFactory;
DIYablesWebAppServer webAppsServer(serverFactory, 80, 81);
DIYablesHomePage homePage;
CustomWebAppPage customPage;
unsigned long lastColorRead = 0;
void setup() {
Serial.begin(9600);
pinMode(S0, OUTPUT);
pinMode(S1, OUTPUT);
pinMode(S2, OUTPUT);
pinMode(S3, OUTPUT);
pinMode(sensorOut, INPUT);
digitalWrite(S0, HIGH);
digitalWrite(S1, LOW);
webAppsServer.addApp(&homePage);
webAppsServer.addApp(&customPage);
if (WiFi.status() == WL_NO_MODULE) {
Serial.println("Communication with WiFi module failed!");
while (true);
}
String fv = WiFi.firmwareVersion();
if (fv < WIFI_FIRMWARE_LATEST_VERSION) {
Serial.println("Please upgrade the firmware");
}
while (status != WL_CONNECTED) {
status = WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
delay(10000);
}
webAppsServer.begin();
Serial.println("Color Web Server Ready!");
}
void loop() {
webAppsServer.loop();
if (millis() - lastColorRead > 1000) {
digitalWrite(S2, LOW);
digitalWrite(S3, LOW);
int r = map(pulseIn(sensorOut, LOW), 31, 150, 255, 0);
digitalWrite(S2, HIGH);
digitalWrite(S3, HIGH);
int g = map(pulseIn(sensorOut, LOW), 35, 180, 255, 0);
digitalWrite(S2, LOW);
digitalWrite(S3, HIGH);
int b = map(pulseIn(sensorOut, LOW), 30, 150, 255, 0);
char hexColor[8];
sprintf(hexColor, "#%02X%02X%02X", constrain(r, 0, 255), constrain(g, 0, 255), constrain(b, 0, 255));
customPage.sendToWeb(String(hexColor));
Serial.println("Sent to Minion: " + String(hexColor));
lastColorRead = millis();
}
}
CustomWebApp.h
#ifndef CUSTOM_WEBAPP_H
#define CUSTOM_WEBAPP_H
#include <DIYablesWebApps.h>
class CustomWebAppPage : public DIYablesWebAppPageBase {
private:
static const String APP_IDENTIFIER;
public:
CustomWebAppPage();
void handleHTTPRequest(IWebClient& client) override;
void handleWebSocketMessage(IWebSocket& ws, const char* message, uint16_t length) override;
const char* getPageInfo() const override;
String getNavigationInfo() const override;
void onCustomMessageReceived(void (*callback)(const String& payload));
void sendToWeb(const String& message);
};
#endif
CustomWebApp.cpp
#include "CustomWebApp.h"
#include "custom_page_html.h"
const String CustomWebAppPage::APP_IDENTIFIER = "Color Sensor:";
void (*customMessageCallback)(const String& payload) = nullptr;
CustomWebAppPage::CustomWebAppPage() : DIYablesWebAppPageBase("/custom") {
}
void CustomWebAppPage::handleHTTPRequest(IWebClient& client) {
sendHTTPHeader(client);
client.print(CUSTOM_PAGE_HTML);
}
void CustomWebAppPage::handleWebSocketMessage(IWebSocket& ws, const char* message, uint16_t length) {
String messageStr = String(message, length);
Serial.print("Color Sensor WebApp received: ");
Serial.println(messageStr);
if (messageStr.startsWith(APP_IDENTIFIER)) {
String payload = messageStr.substring(APP_IDENTIFIER.length());
if (customMessageCallback) {
customMessageCallback(payload);
}
}
}
void CustomWebAppPage::onCustomMessageReceived(void (*callback)(const String& payload)) {
customMessageCallback = callback;
}
void CustomWebAppPage::sendToWeb(const String& message) {
String fullMessage = APP_IDENTIFIER + message;
broadcastToAllClients(fullMessage.c_str());
}
const char* CustomWebAppPage::getPageInfo() const {
return "🔧 Color Sensor WebApp";
}
String CustomWebAppPage::getNavigationInfo() const {
String result = "<a href=\"";
result += getPagePath();
result += "\" class=\"app-card custom\" style=\"background: linear-gradient(135deg, #007bff 0%, #0056b3 100%);\">";
result += "<h3>🔧 Color Sensor WebApp</h3>";
result += "<p>Simple template for your own apps</p>";
result += "</a>";
return result;
}
custom_page_html.h
#ifndef CUSTOM_PAGE_HTML_H
#define CUSTOM_PAGE_HTML_H
const char CUSTOM_PAGE_HTML[] PROGMEM = R"HTML_WRAPPER(
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Mobile Laughing Minion</title>
<style>
body { margin: 0; padding: 20px; box-sizing: border-box; display: flex; flex-direction: column; justify-content: flex-start; align-items: center; background-color: #f0f8ff; font-family: sans-serif; overflow-x: hidden; }
.text { font-size: clamp(16px, 5vw, 24px); font-weight: bold; color: #333; margin-bottom: 20px; text-align: center; z-index: 10; }
.scale-wrapper { transform-origin: top center; display: flex; justify-content: center; align-items: flex-start; }
.minion-container { position: relative; width: 200px; height: 400px; }
.body { position: absolute; top: 20px; left: 25px; width: 150px; height: 300px; background-color: #FFD90F; border-radius: 75px; box-shadow: inset -10px -10px 20px rgba(0,0,0,0.1); overflow: hidden; z-index: 2; transition: background-color 0.5s; }
.overalls { position: absolute; bottom: 0; width: 100%; height: 90px; background-color: #225A94; border-radius: 0 0 75px 75px; box-shadow: inset -10px -10px 20px rgba(0,0,0,0.2); }
.pocket { position: absolute; bottom: 30px; left: 50px; width: 50px; height: 40px; background-color: #1A4674; border-radius: 10px 10px 20px 20px; border: 2px dashed #fce144; }
.strap { position: absolute; top: 65px; left: 0; width: 100%; height: 25px; background-color: #333; z-index: 1; }
.goggles-wrapper { position: absolute; top: 50px; left: -5px; width: 160px; display: flex; justify-content: center; z-index: 3; }
.goggle { position: relative; width: 50px; height: 50px; background-color: white; border: 12px solid #999; border-radius: 50%; box-shadow: 3px 3px 8px rgba(0,0,0,0.2), inset 3px 3px 8px rgba(0,0,0,0.1); margin: 0 -2px; overflow: hidden; }
.pupil { position: absolute; top: 50%; left: 50%; width: 20px; height: 20px; background-color: #4B3621; border-radius: 50%; transform: translate(-50%, -50%); transition: transform 0.2s ease-out; }
.pupil::after { content: ''; position: absolute; top: 4px; left: 4px; width: 6px; height: 6px; background-color: black; border-radius: 50%; }
.catchlight { position: absolute; top: 2px; right: 4px; width: 4px; height: 4px; background-color: white; border-radius: 50%; z-index: 4; }
.eyelid { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: #FFD90F; border-bottom: 3px solid #D4B200; transform-origin: top; transform: scaleY(0); z-index: 5; animation: blink 4s infinite; }
.mouth { position: absolute; top: 145px; left: 35px; width: 80px; height: 45px; background-color: #3E2723; border-radius: 10px 10px 60px 60px; overflow: hidden; z-index: 3; box-shadow: inset 0 5px 10px rgba(0,0,0,0.5); animation: laugh 0.2s infinite alternate ease-in-out; }
.teeth { position: absolute; top: 0; left: 0; width: 100%; height: 14px; background-color: #fff; border-radius: 0 0 5px 5px; }
.tongue { position: absolute; bottom: -5px; left: 20px; width: 40px; height: 25px; background-color: #FF5252; border-radius: 50%; animation: wag 0.2s infinite alternate ease-in-out; }
.arm { position: absolute; top: 140px; width: 25px; height: 80px; background-color: #FFD90F; border-radius: 12px; z-index: 1; transition: background-color 0.5s; }
.arm.left { left: 10px; transform: rotate(35deg); }
.arm.right { right: 15px; transform: rotate(-35deg); }
.glove { position: absolute; bottom: -15px; left: -5px; width: 35px; height: 35px; background-color: #333; border-radius: 50%; }
.leg { position: absolute; bottom: 50px; width: 25px; height: 40px; background-color: #225A94; z-index: 1; }
.leg.left { left: 60px; }
.leg.right { left: 115px; }
.shoe { position: absolute; bottom: -15px; left: -10px; width: 45px; height: 20px; background-color: #222; border-radius: 20px 20px 5px 5px; border-bottom: 5px solid #111; }
@keyframes blink { 0%, 94%, 100% { transform: scaleY(0); } 97% { transform: scaleY(1); } }
@keyframes laugh { 0% { height: 40px; transform: scaleX(1); } 100% { height: 55px; transform: scaleX(1.05); } }
@keyframes wag { 0% { transform: translateY(0); } 100% { transform: translateY(-3px); } }
</style>
</head>
<body>
<div class="text" id="status-text">Just watch him look around! 👀</div>
<div class="scale-wrapper" id="minionWrapper">
<div class="minion-container">
<div class="arm left" id="armL"><div class="glove"></div></div>
<div class="arm right" id="armR"><div class="glove"></div></div>
<div class="leg left"><div class="shoe"></div></div>
<div class="leg right"><div class="shoe"></div></div>
<div class="body" id="minionBody">
<div class="overalls">
<div class="pocket"></div>
</div>
<div class="strap"></div>
<div class="goggles-wrapper">
<div class="goggle"><div class="pupil"><div class="catchlight"></div></div><div class="eyelid" id="eyelidL"></div></div>
<div class="goggle"><div class="pupil"><div class="catchlight"></div></div><div class="eyelid" id="eyelidR"></div></div>
</div>
<div class="mouth">
<div class="teeth"></div>
<div class="tongue"></div>
</div>
</div>
</div>
</div>
<script>
const APP_IDENTIFIER = 'Color Sensor:';
let ws = null;
function connectWebSocket() {
ws = new WebSocket('ws:
ws.onopen = () => document.getElementById('status-text').textContent = "Arduino Uno R4 - Color Sensor";
ws.onclose = () => setTimeout(connectWebSocket, 2000);
ws.onmessage = (event) => {
if (event.data.startsWith(APP_IDENTIFIER)) {
let color = event.data.substring(APP_IDENTIFIER.length);
document.getElementById('minionBody').style.backgroundColor = color;
document.getElementById('armL').style.backgroundColor = color;
document.getElementById('armR').style.backgroundColor = color;
document.getElementById('eyelidL').style.backgroundColor = color;
document.getElementById('eyelidR').style.backgroundColor = color;
document.getElementById('status-text').style.color = color;
}
};
}
function resizeMinion() {
const wrapper = document.getElementById('minionWrapper');
const availableWidth = window.innerWidth - 40;
const minionTrueWidth = 260;
const minionHeight = 400;
let scaleFactor = availableWidth / minionTrueWidth;
if (scaleFactor > 1.5) scaleFactor = 1.5;
wrapper.style.transform = `scale(${scaleFactor})`;
wrapper.style.height = `${minionHeight * scaleFactor}px`;
}
window.addEventListener('resize', resizeMinion);
resizeMinion();
connectWebSocket();
const pupils = document.querySelectorAll('.pupil');
function moveEyesAutomatically() {
const angle = Math.random() * Math.PI * 2;
const distance = Math.random() * 15;
const pupilX = Math.cos(angle) * distance;
const pupilY = Math.sin(angle) * distance;
pupils.forEach(pupil => {
pupil.style.transform = `translate(calc(-50% + ${pupilX}px), calc(-50% + ${pupilY}px))`;
});
}
setInterval(moveEyesAutomatically, 600);
</script>
</body>
</html>
)HTML_WRAPPER";
#endif
Làm theo hướng dẫn từng bước sau:
Kết nối các linh kiện theo sơ đồ kết nối ở trên.
Kết nối bo mạch Arduino Uno R4 WiFi với máy tính bằng cáp USB.
Khởi động Arduino IDE trên máy tính.
Chọn bo mạch Arduino Uno R4 phù hợp (ví dụ: Arduino Uno R4 WiFi) và cổng COM.
Điều hướng đến biểu tượng Libraries ở thanh bên trái của Arduino IDE.
Tìm kiếm "DIYables WebApps", sau đó tìm thư viện DIYables WebApps của DIYables.
Nhấp nút Install để cài đặt thư viện.
Bạn sẽ được yêu cầu cài đặt một số thư viện phụ thuộc khác.
Nhấp nút Install All để cài đặt tất cả thư viện phụ thuộc.
const char WIFI_SSID[] = "YOUR_WIFI_NAME";
const char WIFI_PASSWORD[] = "YOUR_WIFI_PASSWORD";
Cập nhật các giá trị hiệu chuẩn trong các hàm map() bên trong loop() với giá trị hiệu chuẩn của riêng bạn từ bước hiệu chuẩn. Ví dụ, nếu hiệu chuẩn của bạn cho redMin = 42, redMax = 210, greenMin = 55, greenMax = 185, blueMin = 60, blueMax = 172, thay đổi các dòng thành:
int r = map(pulseIn(sensorOut, LOW), 42, 210, 255, 0);
int g = map(pulseIn(sensorOut, LOW), 55, 185, 255, 0);
int b = map(pulseIn(sensorOut, LOW), 60, 172, 255, 0);
Color Web Server Ready!
INFO: Added app /
INFO: Added app /custom
DIYables WebApp Library
Platform: Arduino Uno R4 WiFi
Network connected!
IP address: 192.168.0.2
HTTP server started on port 80
WebSocket server started on port 81
==========================================
DIYables WebApp Ready!
==========================================
📱 Web Interface: http://192.168.0.2
🔗 WebSocket: ws://192.168.0.2:81
📋 Available Applications:
🏠 Home Page: http://192.168.0.2/
🔧 Color Sensor WebApp: http://192.168.0.2/custom
==========================================
Sent to Minion: #FFD200
Sent to Minion: #00C832
Sent to Minion: #0028FF
Nếu bạn không thấy gì, hãy khởi động lại bo mạch Arduino.
Ghi lại địa chỉ IP được hiển thị và nhập địa chỉ này vào thanh địa chỉ của trình duyệt web trên điện thoại thông minh hoặc PC.
Ví dụ: http://192.168.0.2
Bạn sẽ thấy trang chủ. Nhấp vào liên kết Color Sensor WebApp.
Hoặc bạn có thể truy cập trực tiếp trang Minion bằng địa chỉ IP theo sau là /custom. Ví dụ: http://192.168.0.2/custom
Bạn sẽ thấy nhân vật Minion hoạt hình trên trang web.
Đặt một vật có màu trước cảm biến TCS3200 — màu da của Minion sẽ thay đổi theo thời gian thực để khớp với màu sắc được phát hiện!
Bạn có thể xem hướng dẫn từng bước trong video dưới đây.
Sketch chính thực hiện những việc sau:
Khởi tạo các chân cảm biến TCS3200: S0, S1 được đặt cho tần số scale 20%. S2, S3 được sử dụng để chọn bộ lọc màu.
Đọc màu sắc mỗi 1 giây: Trong loop(), Arduino chọn bộ lọc đỏ, xanh lá cây và xanh dương lần lượt, đọc độ rộng xung bằng pulseIn(), và ánh xạ các giá trị thô thành giá trị RGB 0-255 sử dụng số hiệu chuẩn của bạn.
Chuyển đổi sang HEX: Các giá trị RGB được định dạng thành chuỗi màu HEX như #FF8000 sử dụng sprintf().
Gửi đến trình duyệt web: Chuỗi HEX được gửi đến tất cả các client web đã kết nối qua customPage.sendToWeb().
Trang HTML chứa:
Nhân vật Minion hoạt hình được xây dựng hoàn toàn bằng CSS — bao gồm mắt chớp mắt, miệng cười và con ngươi di chuyển ngẫu nhiên.
Kết nối WebSocket: JavaScript kết nối với WebSocket server của Arduino (cổng 81) và lắng nghe các thông điệp màu sắc đến.
Cập nhật màu sắc: Khi nhận được thông điệp như #FF8000, cơ thể, cánh tay và mí mắt của Minion chuyển đổi mượt mà sang màu mới sử dụng CSS transition.
Tự động kết nối lại: Nếu kết nối WebSocket bị ngắt, trang tự động thử kết nối lại mỗi 2 giây.
Thiết kế responsive: Minion tự động scale để phù hợp với các kích thước màn hình khác nhau (điện thoại, máy tính bảng, desktop).
Dự án này sử dụng framework ứng dụng web tùy chỉnh DIYables WebApps với định danh "Color Sensor:":
Để biết thêm chi tiết về giao thức giao tiếp và cách tùy chỉnh ứng dụng web, xem Arduino Custom WebApp Ví Dụ - Hướng Dẫn Giao Diện Web Đơn Giản Cho Người Mới Bắt Đầu.