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.

Jumat, 03 April 2026

JAM ANALOG NTP (INTERNET TIME) dengan NodeMCU ESP8266 & OLED 128x64

Jam Analog NTP dengan NodeMCU ESP8266 & OLED - Dua Versi (Dengan & Tanpa TimeLib)

🕐 JAM ANALOG NTP (INTERNET TIME)
dengan NodeMCU ESP8266 & OLED 128x64

🎯 Apa Itu Jam Analog NTP?

Jam Analog NTP (Network Time Protocol) adalah proyek jam dinding digital yang menampilkan waktu dalam format analog (jarum jam) menggunakan layar OLED. Yang membedakan dari jam biasa adalah sumber waktunya yang diambil langsung dari internet melalui server NTP, sehingga waktu selalu akurat dan tidak perlu disetel ulang meskipun listrik padam.

💡 Keunggulan Proyek Ini:
✅ Waktu selalu akurat (sinkron dengan server waktu global)
✅ Tidak perlu setting ulang setelah mati listrik
✅ Tampilan analog klasik dengan sentuhan modern
✅ Menampilkan tanggal dan hari secara otomatis
✅ Biaya komponen sangat murah (sekitar Rp 150.000)
✅ Bisa dipakai sebagai pajangan meja atau hadiah unik

📊 8 Manfaat Membuat Jam Analog NTP

NoManfaatKeterangan
1Belajar IoTMemahami koneksi WiFi dan pengambilan data dari internet
2Praktik grafis OLEDMenggambar lingkaran, garis, jarum, dan angka di layar
3Pajangan meja unikJam analog dengan tampilan digital yang langka
4Tanpa baterai cadanganWaktu tetap akurat karena ambil dari internet
5Portofolio proyekNilai tambah untuk portofolio IoT Anda
6Hadiah untuk temanBisa dijadikan kado unik buatan sendiri
7Edukasi anakMengajarkan konsep waktu dan elektronika
8Dapat dikembangkanBisa ditambah sensor suhu, alarm, dll

🛠 Alat & Bahan yang Diperlukan

Komponen yang dibutuhkan sangat sederhana dan mudah didapatkan:

KomponenSpesifikasiFungsiEstimasi Harga
NodeMCU ESP8266ESP-12E, CH340/CP2102Otak alat + koneksi WiFiRp 60.000 - 85.000
Layar OLED 0.96"128x64, I2C, SSD1306Menampilkan jam analogRp 40.000 - 65.000
Kabel Jumper F-F4 buahKoneksi komponenRp 5.000 - 10.000
Kabel Micro USBSupport dataPower & upload programRp 15.000 - 30.000
WiFi 2.4 GHzSSID: 7D, Password: forget22Koneksi internet-

Total biaya: Rp 105.000 - 160.000 (jika sudah punya kabel USB)

🔌 Skema Koneksi (Wiring)

Layar OLED menggunakan protokol I2C, hanya membutuhkan 4 kabel:

Layar OLEDNodeMCU ESP8266GPIOKeterangan
VCC3.3V-Tegangan 3.3V (JANGAN 5V!)
GNDGND-Ground / Tanah
SCLD1GPIO5Clock I2C
SDAD2GPIO4Data I2C
⚠️ PERINGATAN PENTING: NodeMCU ESP8266 menggunakan logika 3.3 VOLT. Pastikan layar OLED Anda mendukung 3.3V. Menyambungkan ke pin 5V (misalnya dari Arduino Uno) dapat merusak layar secara permanen!

📚 Instalasi Library yang Diperlukan

Buka Arduino IDE → SketchInclude LibraryManage Libraries..., lalu instal:

LibraryNama PencarianFungsiVersi 1Versi 2
Adafruit GFX"Adafruit GFX Library"Fungsi grafis dasar
Adafruit SSD1306"Adafruit SSD1306"Driver layar OLED
NTPClient"NTPClient" by Fabrice WeinbergAmbil waktu dari internet
TimeLib"Time" by Michael MargolisKonversi waktu & tanggal

📦 VERSI 1: Jam Analog dengan TimeLib

🕐 VERSI 1 - Menggunakan Library TimeLib

Versi ini menggunakan library TimeLib untuk memudahkan konversi waktu dan tanggal. Library ini menyediakan fungsi localtime() yang langsung mengubah epoch time menjadi struktur tanggal (hari, bulan, tahun). Cocok untuk pemula karena lebih sederhana.

✅ Menggunakan TimeLib ✅ Fungsi localtime() siap pakai ✅ Kode lebih sederhana ✅ Mudah dipahami

📝 Kode Program (Dengan TimeLib)

/*
    PROJECT: Jam Analog NTP dengan TimeLib
    BOARD: NodeMCU ESP8266
    DISPLAY: OLED 128x64 SSD1306
    LIBRARY: TimeLib.h, NTPClient.h, Adafruit_SSD1306.h
    
    SSID: 7D
    PASSWORD: forget22
*/

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <ESP8266WiFi.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <TimeLib.h>  // Library untuk konversi waktu

// ========== KONFIGURASI OLED ==========
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET    -1
#define OLED_ADDRESS  0x3C

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// ========== KONFIGURASI WiFi ==========
const char* ssid = "7D";
const char* password = "forget22";

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

// ========== VARIABEL JAM ==========
int centerX = 64;
int centerY = 32;
int radius = 28;

// ========== FUNGSI MENGGAMBAR JARUM JAM ==========
void drawHand(int angle, int length, bool isSecondHand = false) {
    float rad = radians(angle - 90);
    int x = centerX + cos(rad) * length;
    int y = centerY + sin(rad) * length;
    
    if(isSecondHand) {
        display.drawLine(centerX, centerY, x, y, SSD1306_WHITE);
        display.fillCircle(centerX, centerY, 2, SSD1306_WHITE);
    } else {
        display.drawLine(centerX, centerY, x, y, SSD1306_WHITE);
    }
}

// ========== FUNGSI MENGGAMBAR ANGKA JAM ==========
void drawHourNumbers() {
    display.setTextSize(0.7);
    display.setTextColor(SSD1306_WHITE);
    
    int numbers[12] = {12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
    float angleStep = 30;
    
    for(int i = 0; i < 12; i++) {
        float rad = radians(i * angleStep - 90);
        int x = centerX + cos(rad) * (radius - 6);
        int y = centerY + sin(rad) * (radius - 6);
        display.setCursor(x - 4, y - 3);
        display.print(numbers[i]);
    }
}

// ========== FUNGSI MENGGAMBAR TITIK MENIT ==========
void drawMinuteMarks() {
    for(int i = 0; i < 60; i++) {
        float rad = radians(i * 6 - 90);
        int x1 = centerX + cos(rad) * (radius - 3);
        int y1 = centerY + sin(rad) * (radius - 3);
        int x2 = centerX + cos(rad) * (radius - 6);
        int y2 = centerY + sin(rad) * (radius - 6);
        
        if(i % 5 == 0) {
            display.drawLine(x1, y1, x2, y2, SSD1306_WHITE);
        } else {
            display.drawPixel(x1, y1, SSD1306_WHITE);
        }
    }
}

// ========== FUNGSI MENGGAMBAR JAM ANALOG ==========
void drawAnalogClock(int hour, int minute, int second) {
    display.drawCircle(centerX, centerY, radius, SSD1306_WHITE);
    display.drawCircle(centerX, centerY, radius + 1, SSD1306_WHITE);
    drawMinuteMarks();
    drawHourNumbers();
    
    float hourAngle = (hour % 12) * 30 + minute * 0.5;
    float minuteAngle = minute * 6;
    float secondAngle = second * 6;
    
    drawHand(hourAngle, 16, false);
    drawHand(minuteAngle, 22, false);
    drawHand(secondAngle, 25, true);
    
    display.fillCircle(centerX, centerY, 3, SSD1306_WHITE);
    display.fillCircle(centerX, centerY, 1, SSD1306_BLACK);
}

// ========== FUNGSI MENAMPILKAN TANGGAL (DENGAN TimeLib) ==========
void displayDateAndTime() {
    timeClient.update();
    
    int hour = timeClient.getHours();
    int minute = timeClient.getMinutes();
    int second = timeClient.getSeconds();
    unsigned long epochTime = timeClient.getEpochTime();
    
    // Konversi epoch ke waktu lokal menggunakan TimeLib
    time_t rawTime = epochTime;  // Konversi ke tipe time_t
    struct tm *ptm = localtime(&rawTime);
    
    const char* daysOfWeek[] = {"Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu"};
    int wday = ptm->tm_wday;
    int day = ptm->tm_mday;
    int month = ptm->tm_mon + 1;
    int year = ptm->tm_year + 1900;
    
    // Tampilkan tanggal di bagian bawah
    display.setTextSize(0.7);
    display.setCursor(18, 56);
    display.print(daysOfWeek[wday]);
    display.print(", ");
    if(day < 10) display.print("0");
    display.print(day);
    display.print("/");
    if(month < 10) display.print("0");
    display.print(month);
    display.print("/");
    display.print(year);
    
    // Jam digital di pojok kiri
    display.setTextSize(0.8);
    display.setCursor(0, 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);
    
    // Indikator WiFi
    display.setCursor(100, 0);
    display.print("WiFi");
}

// ========== SPLASH SCREEN ==========
void showSplashScreen() {
    display.clearDisplay();
    display.setTextSize(2);
    display.setCursor(10, 15);
    display.println("Analog");
    display.setCursor(10, 35);
    display.println("Clock");
    display.setTextSize(1);
    display.setCursor(20, 55);
    display.println("NTP Time");
    display.display();
    delay(2000);
}

// ========== SETUP ==========
void setup() {
    Serial.begin(115200);
    
    if(!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS)) {
        for(;;);
    }
    
    display.clearDisplay();
    showSplashScreen();
    
    // Koneksi WiFi
    display.clearDisplay();
    display.setCursor(15, 25);
    display.println("Menghubungkan");
    display.setCursor(25, 35);
    display.println("ke WiFi...");
    display.display();
    
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    
    Serial.println("\nWiFi Terhubung!");
    timeClient.begin();
    delay(2000);
    display.clearDisplay();
}

// ========== LOOP ==========
void loop() {
    if(WiFi.status() == WL_CONNECTED) {
        timeClient.update();
        
        int hour = timeClient.getHours();
        int minute = timeClient.getMinutes();
        int second = timeClient.getSeconds();
        
        display.clearDisplay();
        drawAnalogClock(hour, minute, second);
        displayDateAndTime();
        display.display();
    } else {
        display.clearDisplay();
        display.setCursor(15, 25);
        display.println("WiFi Terputus!");
        display.display();
        WiFi.reconnect();
        delay(5000);
    }
    
    delay(100);
}

🔍 Penjelasan Kode Penting (Versi TimeLib)

  • #include <TimeLib.h> - Library yang menyediakan fungsi konversi waktu seperti localtime()
  • time_t rawTime = epochTime; - Konversi tipe data dari unsigned long ke time_t agar kompatibel dengan fungsi localtime
  • struct tm *ptm = localtime(&rawTime); - Fungsi ini mengubah epoch time menjadi struktur tm yang berisi: tm_wday (hari), tm_mday (tanggal), tm_mon (bulan), tm_year (tahun)
  • ptm->tm_mon + 1 - Bulan dimulai dari 0 (Januari), jadi perlu ditambah 1
  • ptm->tm_year + 1900 - Tahun dimulai dari 1900, jadi perlu ditambah 1900

📦 VERSI 2: Jam Analog Tanpa TimeLib

🕐 VERSI 2 - Tanpa Library TimeLib (Manual)

Versi ini tidak menggunakan library TimeLib dan menggantinya dengan fungsi konversi buatan sendiri. Solusi ini berguna jika Anda mengalami masalah instalasi TimeLib atau ingin memahami algoritma konversi epoch ke tanggal secara mendalam.

❌ Tidak perlu TimeLib ✅ Fungsi konversi buatan sendiri ✅ Lebih mandiri (tanpa library tambahan) ✅ Memahami algoritma konversi tanggal

📝 Kode Program (Tanpa TimeLib)

/*
    PROJECT: Jam Analog NTP Tanpa TimeLib
    BOARD: NodeMCU ESP8266
    DISPLAY: OLED 128x64 SSD1306
    NOTE: Menggunakan fungsi konversi manual
    
    SSID: 7D
    PASSWORD: forget22
*/

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

// ========== KONFIGURASI OLED ==========
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET    -1
#define OLED_ADDRESS  0x3C

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// ========== KONFIGURASI WiFi ==========
const char* ssid = "7D";
const char* password = "forget22";

// ========== KONFIGURASI NTP ==========
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org", 25200, 60000);

// ========== VARIABEL JAM ==========
int centerX = 64;
int centerY = 32;
int radius = 28;

// ========== FUNGSI KONVERSI EPOCH KE TANGGAL (MANUAL) ==========
void getDateFromEpoch(unsigned long epochTime, int &year, int &month, int &day, int &wday) {
    // Algoritma Gregorian untuk konversi epoch ke tanggal
    unsigned long days = epochTime / 86400;
    unsigned long daysSince1970 = days + 719468;
    
    unsigned long era = daysSince1970 / 146097;
    unsigned long doe = daysSince1970 - era * 146097;
    unsigned long yoe = (doe - doe/1460 + doe/36524 - doe/146096) / 365;
    year = yoe + era * 400;
    unsigned long doy = doe - (365*yoe + yoe/4 - yoe/100);
    unsigned long mp = (5*doy + 2)/153;
    month = mp + 3;
    day = doy - (153*mp + 2)/5 + 1;
    
    if (month > 12) {
        month -= 12;
        year++;
    }
    
    wday = (days + 4) % 7;
}

// ========== FUNGSI MENGGAMBAR JARUM JAM ==========
void drawHand(int angle, int length, bool isSecondHand = false) {
    float rad = radians(angle - 90);
    int x = centerX + cos(rad) * length;
    int y = centerY + sin(rad) * length;
    
    if(isSecondHand) {
        display.drawLine(centerX, centerY, x, y, SSD1306_WHITE);
        display.fillCircle(centerX, centerY, 2, SSD1306_WHITE);
    } else {
        display.drawLine(centerX, centerY, x, y, SSD1306_WHITE);
    }
}

// ========== FUNGSI MENGGAMBAR ANGKA JAM ==========
void drawHourNumbers() {
    display.setTextSize(0.7);
    display.setTextColor(SSD1306_WHITE);
    
    int numbers[12] = {12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
    float angleStep = 30;
    
    for(int i = 0; i < 12; i++) {
        float rad = radians(i * angleStep - 90);
        int x = centerX + cos(rad) * (radius - 6);
        int y = centerY + sin(rad) * (radius - 6);
        display.setCursor(x - 4, y - 3);
        display.print(numbers[i]);
    }
}

// ========== FUNGSI MENGGAMBAR TITIK MENIT ==========
void drawMinuteMarks() {
    for(int i = 0; i < 60; i++) {
        float rad = radians(i * 6 - 90);
        int x1 = centerX + cos(rad) * (radius - 3);
        int y1 = centerY + sin(rad) * (radius - 3);
        int x2 = centerX + cos(rad) * (radius - 6);
        int y2 = centerY + sin(rad) * (radius - 6);
        
        if(i % 5 == 0) {
            display.drawLine(x1, y1, x2, y2, SSD1306_WHITE);
        } else {
            display.drawPixel(x1, y1, SSD1306_WHITE);
        }
    }
}

// ========== FUNGSI MENGGAMBAR JAM ANALOG ==========
void drawAnalogClock(int hour, int minute, int second) {
    display.drawCircle(centerX, centerY, radius, SSD1306_WHITE);
    display.drawCircle(centerX, centerY, radius + 1, SSD1306_WHITE);
    drawMinuteMarks();
    drawHourNumbers();
    
    float hourAngle = (hour % 12) * 30 + minute * 0.5;
    float minuteAngle = minute * 6;
    float secondAngle = second * 6;
    
    drawHand(hourAngle, 16, false);
    drawHand(minuteAngle, 22, false);
    drawHand(secondAngle, 25, true);
    
    display.fillCircle(centerX, centerY, 3, SSD1306_WHITE);
    display.fillCircle(centerX, centerY, 1, SSD1306_BLACK);
}

// ========== FUNGSI MENAMPILKAN TANGGAL (TANPA TimeLib) ==========
void displayDateAndTime() {
    timeClient.update();
    
    int hour = timeClient.getHours();
    int minute = timeClient.getMinutes();
    int second = timeClient.getSeconds();
    unsigned long epochTime = timeClient.getEpochTime();
    
    // Konversi manual tanpa TimeLib
    int year, month, day, wday;
    getDateFromEpoch(epochTime, year, month, day, wday);
    
    const char* daysOfWeek[] = {"Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu"};
    
    display.setTextSize(0.7);
    display.setCursor(18, 56);
    display.print(daysOfWeek[wday]);
    display.print(", ");
    if(day < 10) display.print("0");
    display.print(day);
    display.print("/");
    if(month < 10) display.print("0");
    display.print(month);
    display.print("/");
    display.print(year);
    
    display.setTextSize(0.8);
    display.setCursor(0, 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);
    
    display.setCursor(100, 0);
    display.print("WiFi");
}

// ========== SPLASH SCREEN ==========
void showSplashScreen() {
    display.clearDisplay();
    display.setTextSize(2);
    display.setCursor(10, 15);
    display.println("Analog");
    display.setCursor(10, 35);
    display.println("Clock");
    display.setTextSize(1);
    display.setCursor(20, 55);
    display.println("NTP Time");
    display.display();
    delay(2000);
}

// ========== SETUP ==========
void setup() {
    Serial.begin(115200);
    
    if(!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS)) {
        for(;;);
    }
    
    display.clearDisplay();
    showSplashScreen();
    
    display.clearDisplay();
    display.setCursor(15, 25);
    display.println("Menghubungkan");
    display.setCursor(25, 35);
    display.println("ke WiFi...");
    display.display();
    
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    
    Serial.println("\nWiFi Terhubung!");
    timeClient.begin();
    delay(2000);
    display.clearDisplay();
}

// ========== LOOP ==========
void loop() {
    if(WiFi.status() == WL_CONNECTED) {
        timeClient.update();
        
        int hour = timeClient.getHours();
        int minute = timeClient.getMinutes();
        int second = timeClient.getSeconds();
        
        display.clearDisplay();
        drawAnalogClock(hour, minute, second);
        displayDateAndTime();
        display.display();
    } else {
        display.clearDisplay();
        display.setCursor(15, 25);
        display.println("WiFi Terputus!");
        display.display();
        WiFi.reconnect();
        delay(5000);
    }
    
    delay(100);
}

🔍 Penjelasan Kode Penting (Versi Tanpa TimeLib)

  • void getDateFromEpoch(...) - Fungsi buatan sendiri untuk mengkonversi epoch time (detik sejak 1970) menjadi tanggal (tahun, bulan, hari, dan hari dalam minggu)
  • days = epochTime / 86400 - Menghitung jumlah hari sejak 1970 (86400 = 24 jam x 3600 detik)
  • wday = (days + 4) % 7 - Menghitung hari dalam minggu (0 = Minggu, 1 = Senin, dst). Angka 4 karena 1 Jan 1970 adalah hari Kamis
  • epochTime / 86400 - Konversi epoch (detik) ke jumlah hari

📊 Perbandingan Kedua Versi

.\] Saya akan melanjutkan tabel perbandingan dan sisa artikel: ```html
AspekVersi 1 (Dengan TimeLib)Versi 2 (Tanpa TimeLib)
Library TambahanTimeLib (perlu instalasi)Tidak perlu
Kode ComplexitySederhanaSedang (ada fungsi manual)
Ukuran KodeLebih kecilSedikit lebih besar
Kemudahan ModifikasiMudahPerlu memahami algoritma
RAM UsageLebih hematSama saja
Rekomendasi untukPemulaYang ingin fleksibel tanpa library

📖 Penjelasan Detail Fungsi-Fungsi Penting

1. Fungsi drawHand() - Menggambar Jarum Jam

void drawHand(int angle, int length, bool isSecondHand = false) {
    float rad = radians(angle - 90);  // Konversi sudut ke radian (offset -90 karena 0 derajat = arah kanan)
    int x = centerX + cos(rad) * length;  // Hitung posisi X ujung jarum
    int y = centerY + sin(rad) * length;  // Hitung posisi Y ujung jarum
    
    if(isSecondHand) {
        display.drawLine(centerX, centerY, x, y, SSD1306_WHITE);  // Jarum detik
        display.fillCircle(centerX, centerY, 2, SSD1306_WHITE);   // Lingkaran tengah
    } else {
        display.drawLine(centerX, centerY, x, y, SSD1306_WHITE);  // Jarum jam/menit
    }
}

Penjelasan: Fungsi ini menghitung posisi ujung jarum berdasarkan sudut dan panjangnya menggunakan rumus trigonometri (sinus dan cosinus). radians() mengubah derajat ke radian, cos() dan sin() menghitung koordinat.

2. Fungsi drawAnalogClock() - Menggambar Seluruh Jam

void drawAnalogClock(int hour, int minute, int second) {
    display.drawCircle(centerX, centerY, radius, SSD1306_WHITE);  // Lingkaran luar
    drawMinuteMarks();   // 60 titik menit
    drawHourNumbers();   // Angka 1-12
    
    float hourAngle = (hour % 12) * 30 + minute * 0.5;   // Jarum jam (30 derajat per jam + 0.5 per menit)
    float minuteAngle = minute * 6;                       // Jarum menit (6 derajat per menit)
    float secondAngle = second * 6;                       // Jarum detik (6 derajat per detik)
    
    drawHand(hourAngle, 16, false);   // Panjang jarum jam = 16px
    drawHand(minuteAngle, 22, false); // Panjang jarum menit = 22px
    drawHand(secondAngle, 25, true);  // Panjang jarum detik = 25px
}

Penjelasan: Fungsi ini menghitung sudut setiap jarum. Jarum jam: 360° / 12 jam = 30° per jam. Jarum menit/detik: 360° / 60 menit = 6° per menit/detik.

3. Fungsi getDateFromEpoch() (Versi Tanpa TimeLib)

void getDateFromEpoch(unsigned long epochTime, int &year, int &month, int &day, int &wday) {
    unsigned long days = epochTime / 86400;                    // Konversi detik ke hari
    unsigned long daysSince1970 = days + 719468;              // Hari sejak 1 Jan 0001
    
    // Algoritma Gregorian untuk konversi
    unsigned long era = daysSince1970 / 146097;
    unsigned long doe = daysSince1970 - era * 146097;
    unsigned long yoe = (doe - doe/1460 + doe/36524 - doe/146096) / 365;
    year = yoe + era * 400;
    // ... dan seterusnya
}

Penjelasan: Fungsi ini mengimplementasikan algoritma Gregorian untuk mengkonversi jumlah hari sejak 1970 menjadi tanggal yang bisa dibaca manusia. Algoritma ini memperhitungkan tahun kabisat dan panjang bulan yang berbeda-beda.

🐛 Troubleshooting (8 Masalah & Solusi)

NoMasalahPenyebabSolusi
1OLED tidak menyalaKoneksi salah atau alamat I2CCek VCC ke 3.3V, SCL ke D1, SDA ke D2. Coba ganti alamat 0x3C ke 0x3D
2Error "TimeLib.h: No such file"Library TimeLib belum terinstalLibrary Manager → cari "Time" by Michael Margolis → Install
3Error "cannot convert 'long unsigned int*' to 'const time_t*'"Konversi tipe data ke time_tGunakan time_t rawTime = epochTime; sebelum localtime()
4Gagal upload ke NodeMCUDriver atau kabel USBTekan FLASH saat "Connecting...". Ganti kabel USB (support data)
5Waktu tidak sinkronNTP server tidak bisa diaksesPastikan WiFi bisa akses internet. Coba ganti pool.ntp.org ke id.pool.ntp.org
6Tanggal salah (beda 1 tahun)Lupa menambah 1900 pada tahunGunakan ptm->tm_year + 1900
7Port COM tidak terdeteksiDriver CH340/CP2102 belum terinstalDownload dan instal driver CH340/CP2102
8Jam berkedip/berbayangRefresh rate terlalu cepatTambah delay(100) di loop() atau kurangi kecepatan refresh

🚀 10 Ide Pengembangan Lebih Lanjut

Setelah jam analog dasar berhasil, Anda bisa mengembangkannya menjadi lebih canggih:

  1. 🌡️ Menambahkan Sensor Suhu DHT11 - Tampilkan suhu ruangan di bawah jam
  2. 🔊 Alarm dengan Buzzer - Setel alarm pada jam tertentu menggunakan Buzzer
  3. 💡 LED Indikator - LED berkedip setiap detak detik
  4. 📱 Kontrol via Smartphone - Gunakan Blynk atau Firebase untuk setting alarm jarak jauh
  5. 🖼️ Tampilan Wajah - Tambahkan karakter imut (Mo-chan/Pikachu) yang berkedip
  6. 🌙 Mode Malam - Kurangi kecerahan OLED pada malam hari (22.00 - 06.00)
  7. 🔋 Monitor Baterai - Tampilkan level baterai jika menggunakan power bank
  8. 📅 Kalender Hijriah - Tambahkan tampilan tanggal Hijriah (perlu API eksternal)
  9. 🎨 Desain Casing 3D - Buat casing kayu/akrilik/3D print agar terlihat profesional
  10. 📈 Grafik Suhu - Tampilkan grafik suhu 24 jam terakhir di OLED
💡 Tips Pengembangan: Mulailah dari fitur termudah (LED indikator atau buzzer) terlebih dahulu, baru ke fitur yang lebih kompleks. Ini akan membantu Anda memahami alur program secara bertahap.

❓ FAQ (Pertanyaan yang Sering Diajukan)

PertanyaanJawaban
Apakah jam ini bisa tanpa internet?Tanpa internet, jam tidak bisa sinkron waktu. Tapi jika pernah sync, RTC internal ESP8266 akan tetap berjalan meski WiFi putus (akurasi ±5 menit per hari)
Berapa akurasi waktunya?Sangat akurat karena sinkron dengan server atomik. Selisih < 1 detik dari waktu sebenarnya
Bisa pakai Arduino Uno?Bisa, tapi perlu pin A4 (SDA) dan A5 (SCL) untuk I2C. Kode tetap sama
Zona waktu bagaimana?Offset 25200 = WIB (UTC+7), 28800 = WITA, 32400 = WIT. Ubah di timeClient(ntpUDP, "pool.ntp.org", OFFSET, 60000)
Apakah library TimeLib wajib?Tidak wajib. Versi 2 sudah menyediakan fungsi konversi manual tanpa TimeLib

📦 Kesimpulan

Proyek Jam Analog NTP ini adalah solusi sempurna untuk Anda yang ingin memiliki jam dinding digital dengan akurasi tinggi tanpa perlu repot menyetel waktu. Dengan biaya kurang dari Rp 200.000, Anda bisa membuat jam yang:

  • Menampilkan waktu dalam format analog klasik
  • Waktu selalu akurat karena sinkron dengan internet
  • Menampilkan tanggal dan hari otomatis
  • Bisa dikembangkan dengan berbagai fitur tambahan
✅ Ringkasan Proyek:
☐ Jam analog dengan 3 jarum (jam, menit, detik)
☐ Sumber waktu dari server NTP (internet)
☐ Dua versi kode (dengan & tanpa TimeLib)
☐ Menampilkan tanggal dan hari
☐ Indikator koneksi WiFi
☐ Auto reconnect jika WiFi terputus

Pilih versi yang paling sesuai dengan kebutuhan Anda. Jika Anda ingin praktis dan mudah, gunakan Versi 1 (dengan TimeLib). Jika Anda ingin fleksibel tanpa library tambahan, gunakan Versi 2 (tanpa TimeLib). Keduanya menghasilkan tampilan jam analog yang sama persis!

Selamat mencoba dan semoga proyek ini bermanfaat untuk Anda. Jangan lupa bagikan artikel ini jika dirasa membantu! 🕐

© 2024 - Tutorial Jam Analog NTP | NodeMCU ESP8266 + OLED 128x64

Dibuat dengan ❤️ untuk komunitas IoT Indonesia. Silakan share jika bermanfaat!

Membuat News Ticker IoT dengan ESP8266 dan OLED Display

Membuat News Ticker IoT dengan ESP8266 dan OLED Display | Tutorial Lengkap

📰 Membuat News Ticker IoT dengan ESP8266 dan OLED Display

📋 Pendahuluan

Internet of Things (IoT) telah membuka peluang tak terbatas untuk menciptakan perangkat pintar yang dapat menampilkan informasi real-time. Salah satu proyek menarik yang bisa Anda buat adalah News Ticker - sebuah perangkat yang menampilkan berita bergulir secara otomatis menggunakan mikrokontroler ESP8266 dan layar OLED.

Artikel ini akan membahas secara lengkap bagaimana membuat News Ticker yang menampilkan berita terkini dalam bahasa Indonesia. Proyek ini cocok untuk pemula hingga menengah yang ingin belajar tentang konektivitas WiFi, tampilan grafis, dan pengambilan data dari internet.

✅ Keunggulan Proyek Ini:
• Menampilkan 20+ berita terbaru bahasa Indonesia
• Efek scrolling (berjalan) seperti running text
• Mode online dan offline otomatis
• Update berita setiap 10 menit
• Hemat memori dan daya

🛠️ Komponen yang Dibutuhkan

KomponenSpesifikasiFungsi
NodeMCU ESP8266ESP-12E ModuleOtak dari sistem, mengontrol semua fungsi
OLED Display128x64 SSD1306Menampilkan berita dan antarmuka
Kabel JumperFemale to FemaleMenghubungkan komponen
Power SupplyUSB 5V / Micro USBSumber daya

🔌 Skema Rangkaian

Berikut adalah koneksi antara NodeMCU ESP8266 dengan OLED Display SSD1306 (I2C):

NodeMCU ESP8266OLED SSD1306
3.3VVCC
GNDGND
D1 (GPIO5)SCL
D2 (GPIO4)SDA
⚠️ Catatan Penting: Pastikan koneksi I2C benar karena OLED menggunakan protokol I2C untuk komunikasi data. Periksa kembali koneksi sebelum menyalakan perangkat.

📦 Instalasi Library yang Diperlukan

Sebelum meng-upload kode, Anda perlu menginstal library berikut melalui Arduino IDE:

Cara Instalasi:

1. Buka Arduino IDE → Sketch → Include Library → Manage Libraries
2. Cari dan instal satu per satu:

- Adafruit GFX Library by Adafruit
- Adafruit SSD1306 by Adafruit  
- ArduinoJson by Benoit Blanchon
- ESP8266WiFi by ESP8266 Community
- ESP8266HTTPClient by ESP8266 Community

💻 Kode Program Lengkap

Berikut adalah kode lengkap untuk proyek News Ticker. Copy dan paste ke Arduino IDE:

/*
    PROJECT: News Ticker (Berita Bergulir dari Internet)
    BOARD: NodeMCU ESP8266
    DISPLAY: OLED 128x64 SSD1306
    BAHASA: Indonesia
*/

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClientSecure.h>
#include <ArduinoJson.h>

// ========== KONFIGURASI ==========
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET    -1
#define OLED_ADDRESS  0x3C

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// Ganti dengan SSID dan Password WiFi Anda
const char* ssid = "7G";
const char* password = "forget22";

// ========== DATA BERITA INDONESIA (LENGKAP) ==========
String newsTitles[20];
int totalNews = 0;
String currentNews = "";
int scrollPosition = SCREEN_WIDTH;
int newsIndex = 0;
unsigned long lastUpdate = 0;
unsigned long lastScroll = 0;
bool wifiConnected = false;

// ========== FUNGSI MEMUAT BERITA INDONESIA ==========
void loadIndonesianNews() {
    newsTitles[0] = "Presiden Jokowi Resmikan Ibu Kota Nusantara (IKN) di Kalimantan Timur";
    newsTitles[1] = "BMKG: Waspada Cuaca Ekstrem dan Hujan Lebat di Jabodetabek";
    newsTitles[2] = "Harga BBM Pertalite dan Biosolar Resmi Naik per 1 Oktober 2024";
    newsTitles[3] = "Timnas Indonesia Kalahkan Arab Saudi 2-1 di Kualifikasi Piala Dunia";
    newsTitles[4] = "GoTo Catat Laba Bersih Pertama Kali Sepanjang Berdirinya Perusahaan";
    newsTitles[5] = "Pemerintah Salurkan Bantuan Langsung Tunai El Nino Rp 400 Ribu";
    newsTitles[6] = "Apple Resmi Buka Apple Developer Center Pertama di Asia Tenggara";
    newsTitles[7] = "Kereta Cepat Jakarta-Bandung Whoosh Mulai Beroperasi Penuh";
    newsTitles[8] = "Polisi Tangkap 50 Tersangka Judi Online Internasional di Bali";
    newsTitles[9] = "Alfamart Buka 1000 Gerai Baru dan Lowongan 5000 Karyawan";
    newsTitles[10] = "Jalan Tol Trans Sumatera Tersambung dari Lampung hingga Palembang";
    newsTitles[11] = "KAI Tambah 20 Kereta Api Baru untuk Liburan Natal dan Tahun Baru";
    newsTitles[12] = "Prabowo Siapkan Program Makan Siang Gratis untuk Pelajar";
    newsTitles[13] = "BPJS Kesehatan Naik Kelas, Ini Penjelasan Pemerintah";
    newsTitles[14] = "Shopee dan Tokopedia Gelar Harbolnas 12.12 Diskon Besar";
    newsTitles[15] = "Jokowi Teken Aturan Baru Bikin Izin Usaha Makin Mudah";
    newsTitles[16] = "BRI Catat Laba Rp 30 Triliun Sepanjang Tahun 2023";
    newsTitles[17] = "Free Fire dan Mobile Legends Jadi E-Sports Asian Games";
    newsTitles[18] = "Pembangunan LRT Jabodebek Resmi Beroperasi Penuh";
    newsTitles[19] = "Pertamina Pastikan Stok BBM Aman Selama Libur Nataru";
    totalNews = 20;
    
    Serial.println("✓ Berita Indonesia dimuat (20 judul)");
}

// ========== CONNECT WIFI ==========
bool connectWiFi() {
    display.clearDisplay();
    display.setTextSize(1);
    display.setCursor(15, 25);
    display.println("Menghubungkan");
    display.setCursor(25, 38);
    display.println("ke WiFi...");
    display.display();
    
    WiFi.begin(ssid, password);
    int attempts = 0;
    
    while (WiFi.status() != WL_CONNECTED && attempts < 20) {
        delay(500);
        Serial.print(".");
        attempts++;
    }
    
    Serial.println();
    
    if (WiFi.status() == WL_CONNECTED) {
        Serial.println("✓ WiFi Terhubung!");
        Serial.print("  IP Address: ");
        Serial.println(WiFi.localIP());
        
        display.clearDisplay();
        display.setCursor(20, 25);
        display.println("WiFi OK!");
        display.setCursor(10, 38);
        display.print(WiFi.localIP());
        display.display();
        delay(1500);
        return true;
    } else {
        Serial.println("✗ WiFi Gagal Terhubung!");
        display.clearDisplay();
        display.setCursor(15, 25);
        display.println("WiFi Gagal!");
        display.setCursor(15, 38);
        display.println("Mode Offline");
        display.display();
        delay(1500);
        return false;
    }
}

// ========== COBA AMBIL BERITA ONLINE ==========
void tryFetchOnlineNews() {
    if (WiFi.status() != WL_CONNECTED) {
        Serial.println("Mode offline - menggunakan berita lokal");
        return;
    }
    
    Serial.println("Mencoba mengambil berita online...");
    
    WiFiClientSecure client;
    client.setInsecure();
    HTTPClient https;
    
    String rssUrl = "https://api.rss2json.com/v1/api.json?rss_url=https://rss.tempo.co/nasional";
    https.begin(client, rssUrl);
    https.setTimeout(5000);
    
    int httpCode = https.GET();
    if (httpCode == HTTP_CODE_OK) {
        String payload = https.getString();
        DynamicJsonDocument doc(4096);
        DeserializationError error = deserializeJson(doc, payload);
        
        if (!error && doc.containsKey("items")) {
            JsonArray items = doc["items"];
            int count = 0;
            
            for (JsonObject item : items) {
                if (count < 15) {
                    const char* title = item["title"];
                    if (title && strlen(title) > 10) {
                        String cleanTitle = String(title);
                        cleanTitle.replace(""", "\"");
                        cleanTitle.replace("'", "'");
                        cleanTitle.replace("&", "&");
                        newsTitles[count] = cleanTitle;
                        count++;
                    }
                }
            }
            
            if (count > 0) {
                totalNews = count;
                Serial.print("✓ Berhasil mengambil ");
                Serial.print(totalNews);
                Serial.println(" berita online dari Tempo");
                return;
            }
        }
    }
    https.end();
    
    Serial.println("Gagal mengambil berita online, pakai berita lokal");
}

// ========== TAMPILAN NEWS TICKER (SCROLLING) ==========
void displayNewsTicker() {
    int tickerY = 52;
    
    display.fillRect(0, tickerY - 2, SCREEN_WIDTH, 13, SSD1306_BLACK);
    display.drawRect(0, tickerY - 2, SCREEN_WIDTH, 13, SSD1306_WHITE);
    
    if (totalNews > 0 && currentNews.length() > 0) {
        int textWidth = currentNews.length() * 6;
        
        display.setTextSize(1);
        display.setTextColor(SSD1306_WHITE);
        display.setCursor(scrollPosition, tickerY);
        display.print(currentNews);
        
        if (scrollPosition <= -textWidth) {
            scrollPosition = SCREEN_WIDTH;
            newsIndex++;
            if (newsIndex >= totalNews) {
                newsIndex = 0;
            }
            currentNews = newsTitles[newsIndex];
        }
    } else {
        display.setCursor(10, tickerY);
        display.print("Memuat berita...");
    }
}

// ========== HEADER DISPLAY ==========
void displayHeader() {
    display.drawLine(0, 14, SCREEN_WIDTH, 14, SSD1306_WHITE);
    
    display.setTextSize(1);
    display.setTextColor(SSD1306_WHITE);
    display.setCursor(2, 2);
    display.print("NEWS TICKER");
    
    display.setCursor(SCREEN_WIDTH - 42, 2);
    if (WiFi.status() == WL_CONNECTED) {
        display.print("ONLINE");
    } else {
        display.print("OFFLINE");
    }
    
    display.setCursor(2, 22);
    display.setTextSize(0.6);
    display.print("Update: ");
    unsigned long minutes = (millis() - lastUpdate) / 60000;
    display.print(minutes);
    display.print("m lalu");
    
    if (totalNews > 0) {
        display.setCursor(SCREEN_WIDTH - 55, 22);
        display.print("Berita ");
        display.print(newsIndex + 1);
        display.print("/");
        display.print(totalNews);
    }
}

// ========== HEADLINE UTAMA ==========
void displayHeadline() {
    if (totalNews > 0) {
        display.setTextSize(0.7);
        display.setTextColor(SSD1306_WHITE);
        
        String headline = newsTitles[0];
        int yPos = 28;
        int maxChars = 22;
        int len = headline.length();
        
        display.fillRect(0, 24, SCREEN_WIDTH, 28, SSD1306_BLACK);
        
        for (int i = 0; i < len && yPos < 52; i += maxChars) {
            int endIdx = i + maxChars;
            if (endIdx > len) {
                endIdx = len;
            }
            String line = headline.substring(i, endIdx);
            display.setCursor(2, yPos);
            display.print(line);
            yPos += 8;
        }
        
        display.setTextSize(0.5);
        display.setCursor(SCREEN_WIDTH - 42, 25);
        display.print("HEADLINE");
    } else {
        display.fillRect(0, 24, SCREEN_WIDTH, 28, SSD1306_BLACK);
        display.setCursor(15, 35);
        display.setTextSize(0.8);
        display.print("Tidak ada berita");
    }
}

// ========== SPLASH SCREEN ==========
void showSplash() {
    display.clearDisplay();
    display.setTextSize(2);
    display.setCursor(10, 15);
    display.println("News");
    display.setCursor(10, 35);
    display.println("Ticker");
    display.setTextSize(1);
    display.setCursor(2, 55);
    display.println("Berita Indonesia");
    display.display();
    delay(2000);
}

// ========== SETUP ==========
void setup() {
    Serial.begin(115200);
    Serial.println("\n========================================");
    Serial.println("📰 NEWS TICKER - BERITA INDONESIA");
    Serial.println("========================================\n");
    
    if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS)) {
        Serial.println("OLED gagal diinisialisasi!");
        for (;;);
    }
    
    showSplash();
    
    wifiConnected = connectWiFi();
    loadIndonesianNews();
    
    if (wifiConnected) {
        tryFetchOnlineNews();
    }
    
    newsIndex = 0;
    scrollPosition = SCREEN_WIDTH;
    if (totalNews > 0) {
        currentNews = newsTitles[0];
    }
    
    lastUpdate = millis();
    display.clearDisplay();
    
    Serial.print("✓ Total ");
    Serial.print(totalNews);
    Serial.println(" berita siap ditampilkan");
}

// ========== LOOP UTAMA ==========
void loop() {
    if (wifiConnected && (millis() - lastUpdate > 600000)) {
        tryFetchOnlineNews();
        lastUpdate = millis();
    }
    
    if (millis() - lastScroll > 40) {
        scrollPosition -= 2;
        lastScroll = millis();
    }
    
    display.clearDisplay();
    displayHeader();
    displayHeadline();
    displayNewsTicker();
    display.display();
    
    delay(20);
}

⚙️ Cara Kerja Sistem

Berikut diagram alur kerja sistem:

Start → Inisialisasi OLED → Tampilkan Splash → Koneksi WiFi
                                                    ↓
                                            [Sukses?]
                                                ↓
                                    Yes → Ambil Berita Online
                                    No  → Gunakan Berita Offline
                                                ↓
                                    Tampilkan Header + Headline
                                                ↓
                                    Jalankan Scrolling News Ticker
                                                ↓
                                    Update setiap 10 menit (jika online)

Penjelasan Fungsi Penting:

  • loadIndonesianNews() - Memuat 20 berita offline cadangan
  • connectWiFi() - Menghubungkan ke jaringan WiFi
  • tryFetchOnlineNews() - Mengambil berita dari RSS Tempo
  • displayNewsTicker() - Membuat efek scrolling berita
  • displayHeadline() - Menampilkan berita utama di tengah

⚠️ Troubleshooting Umum

MasalahPenyebabSolusi
Layar OLED kosongKoneksi salahPeriksa kabel SDA/SCL
WiFi tidak konekSSID/password salahPeriksa kembali kredensial
Teks tidak berjalanDelay terlalu besarKurangi nilai delay di loop
Berita tidak updateKoneksi internet bermasalahCek koneksi router
Compile errorLibrary tidak lengkapInstal semua library yang diperlukan

🚀 Pengembangan Lebih Lanjut

Proyek ini dapat dikembangkan dengan fitur-fitur berikut:

  • Menambahkan efek suara - Buzzer untuk notifikasi berita baru
  • Kontrol jarak jauh - Menggunakan Blynk atau Firebase
  • Multiple display - Menampilkan berita di beberapa layar
  • Penyimpanan SD Card - Menyimpan berita untuk mode offline
  • Sensor gerak - Menyalakan display hanya saat ada orang
  • Mode malam - Kecerahan otomatis menyesuaikan waktu
🎉 Selamat! Anda telah berhasil membuat News Ticker IoT sendiri. Jangan lupa untuk menyesuaikan SSID dan password WiFi dengan jaringan Anda sebelum upload kode.

Panduan Lengkap NodeMCU ESP8266 dan OLED 0.96" untuk Pemula

NodeMCU ESP8266 dan OLED 0.96" - Panduan Lengkap untuk Pemula 📟 NODEMCU ESP8266 DAN OLED 0.96" Panduan Lengkap untuk...