Sabtu, 04 April 2026

Proyek IoT: Mata Robot Mo-chan Ekspresif - Berbasis Waktu dengan OLED & NodeMCU

Proyek IoT: Mata Robot Mo-chan Ekspresif Berbasis Waktu dengan OLED & NodeMCU

🤖 Proyek IoT: Mata Robot Mo-chan Ekspresif
Berbasis Waktu dengan OLED & NodeMCU

Pernahkah Anda ingin membuat robot kecil yang bisa menunjukkan ekspresi sesuai dengan waktu sepanjang hari? Pada tutorial kali ini, kita akan membuat sebuah proyek IoT yang menarik: mata robot bergaya Mo-chan (anime) yang bisa berkedip, melihat ke kiri/kanan, dan berganti ekspresi berdasarkan waktu (pagi, siang, sore, malam) menggunakan layar OLED 0.96" dan board NodeMCU ESP8266. Proyek ini sangat cocok untuk pajangan meja kerja, jam digital unik, atau bagian dari proyek robotika Anda berikutnya!

🎯 Hasil Akhir: Layar OLED akan menampilkan sepasang mata dalam kotak persegi yang berubah ekspresi secara otomatis sesuai jam dari internet. Mulai dari mata bahagia ^^, terkejut O.O, marah, tidur dengan Zzz, hingga mata berbentuk hati.

🛠 Alat & Bahan yang Diperlukan

Pastikan Anda sudah menyiapkan komponen berikut. Total biaya relatif murah dan mudah didapatkan di toko elektronik atau online.

Komponen Spesifikasi / Model Fungsi
Board NodeMCU NodeMCU ESP8266 (CP2102/CH340) Mikrokontroler utama dengan WiFi
Layar OLED 0.96 inch, 128x64 pixel, I2C (SSD1306) Menampilkan gambar mata robot
Kabel Jumper Female to Female (F-F) 4 buah Menghubungkan OLED ke NodeMCU
Kabel USB Micro USB (yang mendukung data) Memprogram dan memberi daya
Jaringan WiFi 2.4 GHz (ESP8266 tidak support 5GHz) Mengambil waktu dari internet (NTP)

🔌 Skema Koneksi (Wiring)

Layar OLED menggunakan protokol I2C, hanya butuh 4 kabel. Hubungkan dengan NodeMCU sesuai tabel berikut:

Layar OLED NodeMCU ESP8266 Keterangan
VCC 3.3V Power (jangan 5V!)
GND GND Ground
SCL D1 (GPIO5) Clock I2C
SDA D2 (GPIO4) Data I2C
⚠️ Perhatian: Board NodeMCU menggunakan logika 3.3V. Pastikan OLED Anda juga mendukung 3.3V (kebanyakan OLED 0.96" sudah support). Jangan sampai menyambung ke pin 5V Arduino karena bisa merusak layar.

📚 Instalasi Library yang Diperlukan

Sebelum mengupload kode, Anda harus menginstal library berikut melalui Arduino IDE (Sketch → Include Library → Manage Libraries).

Nama Library Pencarian di Library Manager Fungsi
Adafruit GFX "Adafruit GFX Library" by Adafruit Fungsi grafis dasar (lingkaran, garis, dll)
Adafruit SSD1306 "Adafruit SSD1306" by Adafruit Driver untuk layar OLED SSD1306
NTPClient "NTPClient" by Fabrice Weinberg Mengambil waktu dari server internet

Tambahan: Untuk board ESP8266, pastikan Anda sudah menginstal board ESP8266 melalui Boards Manager dengan URL: https://arduino.esp8266.com/stable/package_esp8266com_index.json. Kemudian pilih board "NodeMCU 1.0 (ESP-12E Module)".

💻 Kode Program Lengkap

Berikut adalah kode lengkap yang perlu Anda upload ke NodeMCU. Jangan lupa ganti ssid dan password dengan jaringan WiFi Anda sendiri. Kode ini dapat di-scroll jika terlalu panjang.

/*
    Project: Mo-chan Expressive Eyes with Time-based Emotions
    Board: NodeMCU ESP8266
    OLED: 128x64 I2C SSD1306
    Author: Tutorial Blog
    Description: Menampilkan mata robot yang berubah ekspresi sesuai waktu (NTP)
*/

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include <NTPClient.h>

// ========== KONFIGURASI WiFi ==========
const char* ssid = "NAMA_WIFI_ANDA";       // Ganti dengan SSID WiFi Anda
const char* password = "PASSWORD_WIFI_ANDA"; // Ganti password WiFi Anda

// ========== KONFIGURASI OLED ==========
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET    -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// ========== KONFIGURASI NTP (Waktu) ==========
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org", 25200, 60000); // 25200 = UTC+7 (WIB)

// ========== VARIABEL ANIMASI MATA ==========
unsigned long lastBlink = 0;
unsigned long lastEyeMove = 0;
bool eyesOpen = true;
int eyeDirection = 0;     // 0:center, 1:kiri, 2:kanan, 3:atas, 4:bawah
int expression = 0;       // 0:normal, 1:happy, 2:surprised, 3:sleep, 4:angry, 5:love

// ========== FUNGSI UNTUK MENENTUKAN EKSPRESI BERDASARKAN JAM ==========
int getExpressionByTime() {
  if (WiFi.status() != WL_CONNECTED) return 0; // Jika offline, tampilkan normal
  
  timeClient.update();
  int hour = timeClient.getHours();
  int minute = timeClient.getMinutes();
  int currentTime = hour * 100 + minute; // Format HHMM, contoh 0630 = jam 6:30
  
  // Logika jadwal emosi
  if (currentTime < 615) return 3;        // 00:00 - 06:14 -> Tidur (Zzz)
  else if (currentTime < 900) return 1;   // 06:15 - 08:59 -> Bahagia (^ ^)
  else if (currentTime < 1200) return 0;  // 09:00 - 11:59 -> Normal (o o)
  else if (currentTime < 1300) return 5;  // 12:00 - 12:59 -> Love (Hati)
  else if (currentTime < 1500) return 2;  // 13:00 - 14:59 -> Terkejut (O O)
  else if (currentTime < 1800) return 0;  // 15:00 - 17:59 -> Normal
  else if (currentTime < 1900) return 4;  // 18:00 - 18:59 -> Marah (/\)
  else if (currentTime < 2200) return 1;  // 19:00 - 21:59 -> Bahagia
  else return 3;                          // 22:00 - 23:59 -> Tidur
}

// ========== FUNGSI MENGGAMBAR MATA MO-CHAN DALAM KOTAK ==========
void drawMoChanEyesInBox() {
  int leftEyeX = 44, rightEyeX = 84, eyeY = 32;
  int pupilOffsetX = 0, pupilOffsetY = 0;
  
  // Atur pergeseran pupil berdasarkan arah pandang
  switch(eyeDirection) {
    case 1: pupilOffsetX = -6; break;
    case 2: pupilOffsetX = 6; break;
    case 3: pupilOffsetY = -5; break;
    case 4: pupilOffsetY = 5; break;
  }
  
  // Gambar kotak bingkai mata (efek seperti panel robot)
  display.drawRoundRect(leftEyeX - 16, eyeY - 14, 32, 28, 5, SSD1306_WHITE);
  display.drawRoundRect(rightEyeX - 16, eyeY - 14, 32, 28, 5, SSD1306_WHITE);
  display.drawRect(leftEyeX - 15, eyeY - 13, 30, 26, SSD1306_WHITE);
  display.drawRect(rightEyeX - 15, eyeY - 13, 30, 26, SSD1306_WHITE);
  
  if(eyesOpen) {
    // Gambar berbagai ekspresi berdasarkan variabel 'expression'
    switch(expression) {
      case 1: // Bahagia ^ ^
        display.drawLine(leftEyeX - 10, eyeY - 2, leftEyeX - 4, eyeY - 6, SSD1306_WHITE);
        display.drawLine(leftEyeX - 4, eyeY - 6, leftEyeX + 4, eyeY - 6, SSD1306_WHITE);
        display.drawLine(leftEyeX + 4, eyeY - 6, leftEyeX + 10, eyeY - 2, SSD1306_WHITE);
        display.drawLine(rightEyeX - 10, eyeY - 2, rightEyeX - 4, eyeY - 6, SSD1306_WHITE);
        display.drawLine(rightEyeX - 4, eyeY - 6, rightEyeX + 4, eyeY - 6, SSD1306_WHITE);
        display.drawLine(rightEyeX + 4, eyeY - 6, rightEyeX + 10, eyeY - 2, SSD1306_WHITE);
        break;
      case 2: // Terkejut O O
        display.fillCircle(leftEyeX, eyeY, 12, SSD1306_WHITE);
        display.fillCircle(rightEyeX, eyeY, 12, SSD1306_WHITE);
        display.fillCircle(leftEyeX + pupilOffsetX, eyeY + pupilOffsetY, 5, SSD1306_BLACK);
        display.fillCircle(rightEyeX + pupilOffsetX, eyeY + pupilOffsetY, 5, SSD1306_BLACK);
        display.fillCircle(leftEyeX + pupilOffsetX - 2, eyeY + pupilOffsetY - 2, 2, SSD1306_WHITE);
        display.fillCircle(rightEyeX + pupilOffsetX - 2, eyeY + pupilOffsetY - 2, 2, SSD1306_WHITE);
        break;
      case 3: // Tidur (Zzz)
        display.drawLine(leftEyeX - 10, eyeY - 3, leftEyeX + 10, eyeY - 3, SSD1306_WHITE);
        display.drawLine(rightEyeX - 10, eyeY - 3, rightEyeX + 10, eyeY - 3, SSD1306_WHITE);
        display.setCursor(leftEyeX - 18, eyeY - 8); display.print("z");
        display.setCursor(leftEyeX - 14, eyeY - 12); display.print("Z");
        display.setCursor(leftEyeX - 10, eyeY - 16); display.print("Z");
        break;
      case 4: // Marah (alis turun, bentuk /\)
        display.drawLine(leftEyeX - 10, eyeY - 5, leftEyeX, eyeY + 2, SSD1306_WHITE);
        display.drawLine(leftEyeX, eyeY + 2, leftEyeX + 10, eyeY - 5, SSD1306_WHITE);
        display.drawLine(rightEyeX - 10, eyeY - 5, rightEyeX, eyeY + 2, SSD1306_WHITE);
        display.drawLine(rightEyeX, eyeY + 2, rightEyeX + 10, eyeY - 5, SSD1306_WHITE);
        display.drawLine(leftEyeX - 12, eyeY - 8, leftEyeX + 12, eyeY - 4, SSD1306_WHITE);
        display.drawLine(rightEyeX - 12, eyeY - 4, rightEyeX + 12, eyeY - 8, SSD1306_WHITE);
        break;
      case 5: // Love (pupil berbentuk hati)
        display.fillCircle(leftEyeX, eyeY, 11, SSD1306_WHITE);
        display.fillCircle(rightEyeX, eyeY, 11, SSD1306_WHITE);
        // Hati kiri
        display.fillCircle(leftEyeX - 3, eyeY - 2, 3, SSD1306_BLACK);
        display.fillCircle(leftEyeX + 3, eyeY - 2, 3, SSD1306_BLACK);
        display.fillTriangle(leftEyeX - 5, eyeY - 1, leftEyeX + 5, eyeY - 1, leftEyeX, eyeY + 4, SSD1306_BLACK);
        // Hati kanan
        display.fillCircle(rightEyeX - 3, eyeY - 2, 3, SSD1306_BLACK);
        display.fillCircle(rightEyeX + 3, eyeY - 2, 3, SSD1306_BLACK);
        display.fillTriangle(rightEyeX - 5, eyeY - 1, rightEyeX + 5, eyeY - 1, rightEyeX, eyeY + 4, SSD1306_BLACK);
        break;
      default: // Normal (mata bulat dengan pupil & sorotan)
        display.fillCircle(leftEyeX, eyeY, 11, SSD1306_WHITE);
        display.fillCircle(rightEyeX, eyeY, 11, SSD1306_WHITE);
        display.fillCircle(leftEyeX + pupilOffsetX, eyeY + pupilOffsetY, 7, SSD1306_BLACK);
        display.fillCircle(rightEyeX + pupilOffsetX, eyeY + pupilOffsetY, 7, SSD1306_BLACK);
        display.fillCircle(leftEyeX + pupilOffsetX - 3, eyeY + pupilOffsetY - 3, 3, SSD1306_WHITE);
        display.fillCircle(rightEyeX + pupilOffsetX - 3, eyeY + pupilOffsetY - 3, 3, SSD1306_WHITE);
        display.fillCircle(leftEyeX + pupilOffsetX + 2, eyeY + pupilOffsetY + 2, 1, SSD1306_WHITE);
        display.fillCircle(rightEyeX + pupilOffsetX + 2, eyeY + pupilOffsetY + 2, 1, SSD1306_WHITE);
        break;
    }
  } else {
    // Kondisi mata tertutup (sedang berkedip)
    display.drawLine(leftEyeX - 12, eyeY, leftEyeX + 12, eyeY, SSD1306_WHITE);
    display.drawLine(rightEyeX - 12, eyeY, rightEyeX + 12, eyeY, SSD1306_WHITE);
    for(int i = -10; i <= 10; i+=4) {
      display.drawLine(leftEyeX + i, eyeY, leftEyeX + i, eyeY + 5, SSD1306_WHITE);
      display.drawLine(rightEyeX + i, eyeY, rightEyeX + i, eyeY + 5, SSD1306_WHITE);
    }
  }
  
  // Tampilkan Jam Digital (HH:MM:SS) di bagian atas layar jika WiFi terhubung
  if (WiFi.status() == WL_CONNECTED) {
    timeClient.update();
    int hour = timeClient.getHours();
    int minute = timeClient.getMinutes();
    int second = timeClient.getSeconds();
    display.setTextSize(1);
    display.setTextColor(SSD1306_WHITE);
    display.setCursor(40, 0);
    if(hour < 10) display.print("0");
    display.print(hour); display.print(":");
    if(minute < 10) display.print("0");
    display.print(minute); display.print(":");
    if(second < 10) display.print("0");
    display.print(second);
  }
}

// ========== SETUP ==========
void setup() {
  Serial.begin(115200);
  
  // Inisialisasi OLED
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println("OLED gagal!");
    for(;;);
  }
  display.clearDisplay();
  
  // Koneksi WiFi
  display.setCursor(10, 20);
  display.println("Menghubungkan WiFi...");
  display.display();
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("WiFi Connected!");
  
  // Inisialisasi NTP
  timeClient.begin();
  display.clearDisplay();
}

// ========== LOOP UTAMA ==========
void loop() {
  unsigned long now = millis();
  
  // Update ekspresi setiap 30 detik (sesuai jam)
  static unsigned long lastTimeCheck = 0;
  if (WiFi.status() == WL_CONNECTED && (now - lastTimeCheck > 30000)) {
    int newExpr = getExpressionByTime();
    if (newExpr != expression) {
      expression = newExpr;
      Serial.println("Ekspresi berubah!");
    }
    lastTimeCheck = now;
  }
  
  // Logika Kedipan Mata (lebih lambat saat tidur)
  int blinkInterval = (expression == 3) ? 8000 : 3000;
  if (now - lastBlink > blinkInterval && eyesOpen) {
    eyesOpen = false;
    lastBlink = now;
  }
  if (now - lastBlink > 150 && !eyesOpen) {
    eyesOpen = true;
    lastBlink = now;
  }
  
  // Logika Gerakan Mata (hanya jika tidak tidur)
  int moveInterval = (expression == 3) ? 5000 : 2500;
  if (eyesOpen && expression != 3 && (now - lastEyeMove > moveInterval)) {
    eyeDirection = (eyeDirection + 1) % 5;
    lastEyeMove = now;
  }
  
  // Gambar ulang layar
  display.clearDisplay();
  drawMoChanEyesInBox();
  display.display();
  delay(30);
}

📖 Penjelasan Detail Kode (Baris per Baris)

Berikut adalah penjelasan fungsi dan alur kode agar Anda bisa memodifikasinya nanti.

1. Bagian Header dan Library

#include <Wire.h> : Library untuk komunikasi I2C (komunikasi antara NodeMCU dan OLED).
#include <Adafruit_GFX.h> : Library grafis inti yang menyediakan fungsi menggambar seperti drawCircle(), drawLine(), dll.
#include <Adafruit_SSD1306.h> : Driver spesifik untuk layar OLED SSD1306.
#include <ESP8266WiFi.h> : Library untuk koneksi WiFi pada board ESP8266.
#include <NTPClient.h> : Library untuk sinkronisasi waktu dengan server NTP (Network Time Protocol) via internet.

2. Konfigurasi WiFi dan OLED

const char* ssid = "..." dan password : Isi dengan kredensial hotspot Anda.
#define SCREEN_WIDTH 128 ... : Mendefinisikan resolusi layar OLED (128x64).
Adafruit_SSD1306 display(...) : Membuat objek display untuk mengontrol layar. Parameter terakhir -1 berarti tidak menggunakan pin reset tambahan.

3. Konfigurasi NTP dan Zona Waktu

NTPClient timeClient(ntpUDP, "pool.ntp.org", 25200, 60000);
- pool.ntp.org : alamat server waktu global.
- 25200 : offset dalam detik untuk zona waktu WIB (UTC+7). Untuk WITA gunakan 28800, untuk WIT 32400.
- 60000 : interval update waktu setiap 60 detik.

4. Variabel Animasi Mata

lastBlink, lastEyeMove : menyimpan waktu terakhir (millis) untuk mengatur interval kedipan dan gerakan.
eyesOpen : status boolean (true=terbuka, false=tertutup).
eyeDirection : 0 (tengah), 1 (kiri), 2 (kanan), 3 (atas), 4 (bawah).
expression : 0 (normal), 1 (happy), 2 (surprised), 3 (sleep), 4 (angry), 5 (love).

5. Fungsi getExpressionByTime()

Fungsi ini yang menjadi inti logika waktu. Ia mengambil jam dan menit dari NTP, lalu mengonversi ke format HHMM (misal pukul 07:30 menjadi 730). Kemudian dengan percabangan if-else, fungsi mengembalikan angka ekspresi yang sesuai. Contoh: jika jam kurang dari 6:15 pagi, akan mengembalikan 3 (tidur).

6. Fungsi Gambar drawMoChanEyesInBox()

Ini adalah fungsi paling kompleks. Langkah-langkahnya:
- Menentukan posisi mata kiri (x=44) dan kanan (x=84).
- Menggambar bingkai kotak dengan drawRoundRect() dan drawRect() untuk efek panel robot.
- Kondisi if(eyesOpen) : Jika mata terbuka, akan menggambar 6 macam ekspresi berdasarkan nilai expression menggunakan switch-case. Mulai dari garis lengkung untuk bahagia, lingkaran besar untuk terkejut, hingga bentuk hati untuk love.
- Kondisi else : Jika mata tertutup (kedip), akan menggambar garis horizontal plus bulu mata pendek.
- Terakhir, fungsi menampilkan jam digital di koordinat (40,0) jika WiFi terhubung.

7. Bagian setup()

Menjalankan inisialisasi:
- Serial.begin(115200) untuk debugging.
- Memulai koneksi ke OLED. Alamat I2C umumnya 0x3C. Jika tidak merespon, program akan berhenti (infinite loop).
- Menghubungkan ke WiFi dengan WiFi.begin() dan menunggu hingga terhubung.
- Memulai client NTP dengan timeClient.begin().

8. Bagian loop()

Ini adalah jantung animasi yang berjalan terus menerus:
- Setiap 30 detik, program memanggil getExpressionByTime() untuk mengecek perubahan ekspresi.
- Kedipan Mata : Menggunakan millis() non-blocking. Jika waktu sekarang dikurang lastBlink melebihi interval, status mata berubah. Mata akan tertutup selama 150ms.
- Gerakan Lirik : Mirip seperti kedipan, namun mengubah arah pandang setiap 2.5 detik (kecuali saat ekspresi tidur).
- Setiap iterasi, layar dibersihkan (display.clearDisplay()), lalu memanggil fungsi gambar, dan display.display() untuk mengirim buffer ke layar. Delay 30ms memberikan refresh rate yang halus.

🚀 Cara Upload dan Menjalankan

  1. Buka Arduino IDE, pilih board NodeMCU 1.0 (ESP-12E Module) dan port COM yang sesuai.
  2. Copy seluruh kode di atas ke IDE, lalu ganti ssid dan password dengan milik Anda.
  3. Klik tombol Upload (→). Jika gagal koneksi, tekan tombol FLASH pada NodeMCU saat proses "Connecting..." di log.
  4. Setelah upload selesai, buka Serial Monitor (baud 115200) untuk melihat debugging waktu dan perubahan ekspresi.
  5. Dalam beberapa detik, layar OLED akan menampilkan mata robot dan jam digital. Amati bagaimana ekspresi berubah sesuai waktu!
📌 Catatan Penting: Jika layar OLED tidak menampilkan apa-apa, coba ganti alamat I2C dari 0x3C menjadi 0x3D pada baris display.begin(SSD1306_SWITCHCAPVCC, 0x3C). Juga periksa kembali koneksi kabel SDA/SCL.

🎨 Kustomisasi Lebih Lanjut

Anda bisa mengubah jadwal emosi sesuai keinginan. Misalnya menambahkan ekspresi kaget pada jam tertentu, atau membuat mata melotot saat jam makan. Ubah saja nilai currentTime di fungsi getExpressionByTime(). Anda juga bisa mengganti bentuk kotak, menambah animasi kelopak mata, atau menampilkan teks tambahan di layar.

📦 Kesimpulan

Dengan proyek ini, Anda telah berhasil membuat sebuah perangkat IoT yang interaktif dan imut. Mata robot Mo-chan tidak hanya menjadi pajangan statis, tetapi memiliki "kepribadian" yang berubah sepanjang hari. Proyek ini menggabungkan konsep mikrokontroler, display grafis, koneksi internet, dan manajemen waktu. Selamat bereksperimen dan mengembangkan kreasi Anda sendiri! Jangan lupa bagikan hasil karya Anda di media sosial dengan tagar #RobotMoChan.

Bagikan artikel ini jika dirasa membantu teman Anda yang lain.

Tidak ada komentar:

Posting Komentar

Proyek IoT: Mata Robot Mo-chan Ekspresif - Berbasis Waktu dengan OLED & NodeMCU

Proyek IoT: Mata Robot Mo-chan Ekspresif Berbasis Waktu dengan OLED & NodeMCU 🤖 Proyek IoT: Mata Robot Mo-cha...