Bir ESP32 dijital ikiz, dijital ikizin gerçekte ne olduğunu anlamanın en uygulamalı yoludur: masanızdaki bir düğmeyi çevirirsiniz ve tarayıcıdaki 3D model de onunla birlikte döner. Bu başlangıç projesinde bir ESP32, bir potansiyometreyi, bir butonu ve LED durumunu MQTT üzerinden broker.connectmq.com adresine yayınlar; bir Three.js sayfası bunları gerçek zamanlı yansıtır ve sanal LED'e tıklamak gerçek LED'i açıp kapatır. Proje yaklaşık bir saat sürer ve çoğu maker'ın elinde zaten bulunan parçaları kullanır.
Neler Öğreneceksiniz?
Bu proje küçük olsa da daha büyük endüstriyel ikizlerin dayandığı tüm kavramları kapsar:
- Durum yayınlama: cihaz yalnızca olayları değil, ne durumda olduğunu bildirir.
- Ölü bantlar ve hız sınırları: gürültüyü değil, önemli değişiklikleri göndermek.
- Retained mesajlar: açıldığı anda doğru olan bir ikiz.
- Last will: ikiz, cihazın ortadan kaybolduğunu bilir.
- Çift yönlü kontrol: komutlar aşağı iner, onaylanmış durum yukarı çıkar.
Bir ESP32'yi daha önce hiç MQTT'ye bağlamadıysanız önce ESP32 MQTT eğitimimizi tamamlayın; bu makale Arduino IDE'den bir sketch yükleyebildiğinizi varsayar. İkizlerin ardındaki teori için dijital ikiz teknolojisi rehberine bakın.
Parçalar ve Bağlantılar
| Parça | ESP32 pini | Notlar |
|---|---|---|
| 10 k potansiyometre, orta uç | GPIO34 | Dış uçlar 3V3 ve GND'ye. GPIO34 bir ADC1 pinidir ve Wi-Fi etkinken çalışır |
| Buton | GPIO4 | Diğer uç GND'ye; sketch dahili pull-up'ı etkinleştirir |
| LED + 330 ohm direnç | GPIO2 | Birçok DevKit kartında GPIO2'de zaten bir LED bulunur |
| ESP32 DevKit | USB | Herhangi bir klasik ESP32 kartı çalışır |
Potansiyometre için ADC2 pinlerinden (GPIO25 ile GPIO27 arası gibi) kaçının: Wi-Fi çalışırken ADC2 okunamaz; bu, klasik "değer hep sıfır" kafa karışıklığının kaynağıdır. Espressif ADC dokümantasyonu hangi pinlerin hangi ADC'ye ait olduğunu listeler.
Topic Tasarımı
Prefix'te esp32-7f3a gibi benzersiz bir cihaz ID'si kullanın:
| Topic | Yön | Payload | Retained |
|---|---|---|---|
connectmq/demo/esp32-7f3a/state |
ESP32 yayınlar | {"knob":63,"button":false,"led":true,"rssi":-58,"uptime":412} |
Evet |
connectmq/demo/esp32-7f3a/led/set |
İkiz yayınlar | ON, OFF, TOGGLE |
Hayır |
connectmq/demo/esp32-7f3a/status |
ESP32 yayınlar, last will | online / offline |
Evet |
Tek bir birleşik durum nesnesi ikizi tutarlı tutar: potansiyometre, buton ve LED her zaman cihazın aynı anlık görüntüsünden gelir.
Adım 1: ESP32 Sketch'i
Library Manager'dan Nick O'Leary'nin PubSubClient kütüphanesini kurun, ardından Wi-Fi bilgilerinizi ve cihaz ID'nizi doldurduktan sonra bu sketch'i yükleyin.
#include <WiFi.h>
#include <PubSubClient.h>
const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 1883;
const char* DEVICE_ID = "esp32-7f3a"; // change to your own random suffix
const int PIN_KNOB = 34;
const int PIN_BUTTON = 4;
const int PIN_LED = 2;
String topicState, topicLedSet, topicStatus;
WiFiClient net;
PubSubClient mqtt(net);
bool ledOn = false;
int lastKnob = -100;
bool lastButton = false;
unsigned long lastPublish = 0;
unsigned long lastHeartbeat = 0;
bool dirty = true;
void publishState() {
char json[128];
snprintf(json, sizeof(json),
"{\"knob\":%d,\"button\":%s,\"led\":%s,\"rssi\":%d,\"uptime\":%lu}",
lastKnob, lastButton ? "true" : "false", ledOn ? "true" : "false",
WiFi.RSSI(), millis() / 1000);
mqtt.publish(topicState.c_str(), json, true); // retained
lastPublish = millis();
lastHeartbeat = lastPublish;
dirty = false;
}
void onMessage(char* topic, byte* payload, unsigned int length) {
String cmd;
for (unsigned int i = 0; i < length; i++) cmd += (char)payload[i];
cmd.trim();
cmd.toUpperCase();
if (cmd == "ON") ledOn = true;
else if (cmd == "OFF") ledOn = false;
else if (cmd == "TOGGLE") ledOn = !ledOn;
else return;
digitalWrite(PIN_LED, ledOn ? HIGH : LOW);
dirty = true; // report the new real state
}
void connectWifi() {
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
Serial.printf("\nWi-Fi connected, IP %s\n", WiFi.localIP().toString().c_str());
}
void connectMqtt() {
while (!mqtt.connected()) {
Serial.print("Connecting to MQTT... ");
// id, willTopic, willQos, willRetain, willMessage
if (mqtt.connect(DEVICE_ID, topicStatus.c_str(), 1, true, "offline")) {
Serial.println("connected");
mqtt.publish(topicStatus.c_str(), "online", true);
mqtt.subscribe(topicLedSet.c_str(), 1);
dirty = true;
} else {
Serial.printf("failed, rc=%d, retrying in 3 s\n", mqtt.state());
delay(3000);
}
}
}
void setup() {
Serial.begin(115200);
pinMode(PIN_BUTTON, INPUT_PULLUP);
pinMode(PIN_LED, OUTPUT);
analogReadResolution(12);
String prefix = String("connectmq/demo/") + DEVICE_ID;
topicState = prefix + "/state";
topicLedSet = prefix + "/led/set";
topicStatus = prefix + "/status";
connectWifi();
mqtt.setServer(MQTT_HOST, MQTT_PORT);
mqtt.setCallback(onMessage);
mqtt.setKeepAlive(30);
}
void loop() {
if (WiFi.status() != WL_CONNECTED) connectWifi();
if (!mqtt.connected()) connectMqtt();
mqtt.loop();
// Average a few samples and convert to 0..100 %
long sum = 0;
for (int i = 0; i < 8; i++) sum += analogRead(PIN_KNOB);
int knob = map(sum / 8, 0, 4095, 0, 100);
if (abs(knob - lastKnob) >= 2) { lastKnob = knob; dirty = true; } // 2 % deadband
bool button = digitalRead(PIN_BUTTON) == LOW;
if (button != lastButton) { lastButton = button; dirty = true; }
unsigned long now = millis();
bool rateOk = now - lastPublish >= 200; // at most 5 messages/s
bool heartbeat = now - lastHeartbeat >= 10000; // refresh every 10 s anyway
if ((dirty && rateOk) || heartbeat) publishState();
}
Serial Monitor'ü 115200 baud hızında açın. "connected" yazısını gördüğünüzde akışı bilgisayarınızdan kontrol edin:
mosquitto_sub -h broker.connectmq.com -p 1883 -i watch-esp32-7f3a -v -t "connectmq/demo/esp32-7f3a/#"
Potansiyometreyi çevirin ve butona basın; saniyenin çok küçük bir kesrinde yeni bir durum mesajı belirir.
Sketch Neden Bu Şekilde Yayın Yapıyor?
Ölü bant. Analog okumalar, kimse potansiyometreye dokunmasa bile bir iki birim oynar. Yüzde 2'lik ölü bant olmasaydı kart sürekli yayın yapardı. Gerçek sensörler de aynı işlemi gerektirir.
Hız sınırı. Potansiyometreyi hızlı çevirmek çok sayıda değişiklik üretir. Saniyede beş mesajla sınırlamak, anlık hissettirmeye devam ederken cihazı public broker'ın client başına saniyede 10 mesaj sınırının rahatça içinde tutar.
Heartbeat. Değişiklik olmasa bile on saniyede bir gönderilen durum mesajı, ikizin sessiz bir cihazı bozuk bir cihazdan ayırt etmesini sağlar ve RSSI ile çalışma süresini tazeler.
Retained durum ve last will. İkiz yüklendiği anda güncel durumu alır ve kart gücünü kaybederse broker offline duyurusu yapar.
Komut sonrası durum. Bir komut geldiğinde sketch LED'i değiştirir ve ardından yeni durumu bildirir. İkiz tıklamayı değil bu bildirimi gösterir; bu, dijital ikiz durum senkronizasyonunun özüdür.
Adım 2: Tarayıcıdaki 3D İkiz
Bu sayfayı index.html olarak kaydedin ve npx serve . ile sunun. Sayfa; potansiyometre, buton ve LED içeren basitleştirilmiş bir ESP32 kartı oluşturur ve bunları durum topic'ine bağlar.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>ESP32 Digital Twin</title>
<style>
body { margin: 0; overflow: hidden; background: #0f172a; font-family: system-ui, sans-serif; }
#hud { position: absolute; top: 10px; left: 10px; color: #fff; background: rgba(0,0,0,.6);
padding: 8px 12px; border-radius: 6px; font-size: 14px; }
</style>
<script type="importmap">
{ "imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/",
"mqtt": "https://unpkg.com/mqtt@5.10.1/dist/mqtt.esm.js"
} }
</script>
</head>
<body>
<div id="hud">Connecting...</div>
<script type="module">
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import mqtt from "mqtt";
const P = "connectmq/demo/esp32-7f3a";
const hud = document.getElementById("hud");
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0f172a);
const camera = new THREE.PerspectiveCamera(40, innerWidth / innerHeight, 0.1, 50);
camera.position.set(0, 6, 7);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
new OrbitControls(camera, renderer.domElement);
scene.add(new THREE.HemisphereLight(0xffffff, 0x334155, 1.4));
const key = new THREE.DirectionalLight(0xffffff, 1.2); key.position.set(3, 8, 4); scene.add(key);
// Breadboard-style base and ESP32 module
const base = new THREE.Mesh(new THREE.BoxGeometry(7, 0.2, 4), new THREE.MeshStandardMaterial({ color: 0xf1f5f9 }));
const pcbMat = new THREE.MeshStandardMaterial({ color: 0x14532d });
const pcb = new THREE.Mesh(new THREE.BoxGeometry(2.6, 0.1, 1.4), pcbMat);
pcb.position.set(-1.8, 0.15, 0);
const shield = new THREE.Mesh(new THREE.BoxGeometry(1.0, 0.12, 0.9), new THREE.MeshStandardMaterial({ color: 0xcbd5e1, metalness: 0.8, roughness: 0.3 }));
shield.position.set(-1.4, 0.26, 0);
scene.add(base, pcb, shield);
// Potentiometer knob with a pointer
const knob = new THREE.Group();
knob.add(new THREE.Mesh(new THREE.CylinderGeometry(0.45, 0.45, 0.4, 32), new THREE.MeshStandardMaterial({ color: 0x1e3a8a })));
const pointer = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.05, 0.4), new THREE.MeshStandardMaterial({ color: 0xffffff }));
pointer.position.set(0, 0.22, -0.2);
knob.add(pointer);
knob.position.set(1.0, 0.3, 0);
scene.add(knob);
// Push button (cap moves down when pressed)
const btnBase = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.2, 0.6), new THREE.MeshStandardMaterial({ color: 0x111827 }));
btnBase.position.set(2.4, 0.2, -0.8);
const btnCap = new THREE.Mesh(new THREE.CylinderGeometry(0.18, 0.18, 0.15, 24), new THREE.MeshStandardMaterial({ color: 0xdc2626 }));
btnCap.position.set(2.4, 0.37, -0.8);
scene.add(btnBase, btnCap);
// LED (clickable)
const ledMat = new THREE.MeshStandardMaterial({ color: 0x7f1d1d, emissive: 0xff2d2d, emissiveIntensity: 0, transparent: true, opacity: 0.9 });
const led = new THREE.Mesh(new THREE.SphereGeometry(0.22, 24, 16), ledMat);
led.position.set(2.4, 0.4, 0.8);
scene.add(led);
// ---------- State and MQTT ----------
const s = { knob: 0, button: false, led: false, rssi: null, online: false, at: 0 };
let knobAngle = 0, glow = 0;
const client = mqtt.connect("wss://broker.connectmq.com:8084/mqtt", {
clientId: "esp32-twin-" + Math.random().toString(16).slice(2, 10),
protocolVersion: 5,
});
const dec = new TextDecoder();
client.on("connect", () => client.subscribe([`${P}/state`, `${P}/status`], { qos: 1 }));
client.on("message", (topic, payload) => {
const text = dec.decode(payload);
if (topic.endsWith("/status")) { s.online = text === "online"; return; }
try { Object.assign(s, JSON.parse(text), { at: performance.now() }); } catch {}
});
// Click the virtual LED to toggle the real one
const ray = new THREE.Raycaster(), ndc = new THREE.Vector2();
renderer.domElement.addEventListener("click", (e) => {
ndc.set((e.clientX / innerWidth) * 2 - 1, -(e.clientY / innerHeight) * 2 + 1);
ray.setFromCamera(ndc, camera);
if (ray.intersectObject(led).length) client.publish(`${P}/led/set`, "TOGGLE", { qos: 1 });
});
const clock = new THREE.Clock();
renderer.setAnimationLoop(() => {
const dt = clock.getDelta();
const k = 1 - Math.exp(-dt * 10);
// 0..100 % maps to -135..+135 degrees, like a real potentiometer
const targetAngle = THREE.MathUtils.degToRad(135 - (s.knob / 100) * 270);
knobAngle += (targetAngle - knobAngle) * k;
knob.rotation.y = knobAngle;
btnCap.position.y = s.button ? 0.31 : 0.37; // no easing for discrete states
glow += ((s.led ? 2 : 0) - glow) * k;
ledMat.emissiveIntensity = glow;
const stale = !s.online || performance.now() - s.at > 15000;
pcbMat.color.setHex(stale ? 0x4b5563 : 0x14532d);
hud.textContent = stale ? "ESP32 offline or no data" :
`knob ${s.knob} % | button ${s.button ? "pressed" : "released"} | LED ${s.led ? "on" : "off"} | RSSI ${s.rssi} dBm`;
renderer.render(scene, camera);
});
addEventListener("resize", () => {
camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
</script>
</body>
</html>
Gerçek potansiyometreyi çevirin, 3D düğme onu takip eder. Butona basın, kırmızı başlık aşağı iner. 3D LED'e tıklayın; bir an sonra hem gerçek LED hem de sanal LED yanar. ESP32'nin fişini çekin; keepalive süresi dolduktan sonra tarayıcıdaki kart griye döner.
ESP32 Dijital İkiz Sorun Giderme
| Sorun | Neden | Çözüm |
|---|---|---|
Seri monitörde sürekli rc=-2 görünüyor |
Ağ veya DNS hatası | Wi-Fi bilgilerini ve 1883 portunun engellenmediğini kontrol edin |
| Potansiyometre hep 0 veya 4095 | Yanlış pin veya bağlantı | GPIO34 (ADC1) kullanın, orta ucu orta bacağa bağlayın |
| İkiz canlı değil, eski değerleri gösteriyor | Topic prefix uyuşmazlığı | Sketch'teki DEVICE_ID ile sayfadaki P değerini karşılaştırın |
| LED'e tıklamak hiçbir şey yapmıyor | Kart yeniden bağlandıktan sonra abone olmuyor | Gösterildiği gibi connectMqtt() içinde abone olun |
| Kart ve ikiz çevrimiçi/çevrimdışı arasında gidip geliyor | Aynı client ID'ye sahip iki kart | Her cihaza benzersiz bir ID verin |
Projeniz büyüdükçe payload tasarımı ve adlandırma hakkında daha fazlası için dijital ikizler için MQTT yazımıza bakın. Basit şekilleri gerçek bir kart modeliyle değiştirmek için IoT verisini bir 3D modele bağlama yazımızı okuyun; daha gelişmiş bir sahne için tank ve motor içeren Three.js dijital ikiz eğitimini deneyin.
Sonraki Adım: Kendi Broker'ınızla Güvenli Hâle Getirin
Public broker bu proje için mükemmeldir, ancak topic'inizi bilen herkes onu okuyabilir ve hatta LED'inizi açıp kapatabilir. Projeyi özel hâle getirmek için bir yönetilen MQTT broker hizmetinde bir broker oluşturun ve sketch'i WiFiClientSecure ve portaldaki kimlik bilgileriyle 8883 portunda TLS'ye geçirin: mqtt.connect("YOUR_CLIENT_ID", "YOUR_USERNAME", "YOUR_PASSWORD", ...). ConnectMQ ile sertifika Let's Encrypt'ten gelir; bu yüzden ISRG Root X1 sertifikasını setCACert() ile WiFiClientSecure içine yükleyebilirsiniz. Tarayıcı ikizine kendi kimlik bilgilerini verin ve bunları mqtt.connect seçeneklerinde kullanın. ESP32 IoT eğitimi eksiksiz TLS kurulumunu adım adım gösteriyor.
ConnectMQ ile Başlayın
ESP32 dijital ikiziniz bugün public broker üzerinde çalışıyor. Onu özel hâle getirmek istediğinizde app.connectmq.com adresinden ücretsiz bir broker oluşturun: Serverless plan kredi kartı gerektirmez, 25 bağlantıya kadar destekler ve kartınızın mesajlarını izlemek için canlı bir topic gezgini ile tarayıcı içi bir MQTT client'ı içerir. Daha fazla deneme için ücretsiz public MQTT broker ile keşfetmeye de devam edebilirsiniz.
Sık Sorulan Sorular
Bu gerçekten bir dijital ikiz mi, yoksa sadece bir panel mi?
Basit bir dijital ikizdir: belirli bir fiziksel cihazın canlı durumunu yansıtan ve geri komut gönderebilen sanal bir model. Endüstriyel ikizler daha fazla veri, geçmiş ve analitik ekler, ancak temel döngü aynıdır.
Arduino yerine MicroPython kullanabilir miyim?
Evet. MicroPython'daki umqtt.simple modülü aynı topic'leri, retained mesajları ve last will'i destekler; bu yüzden tarayıcı ikizi hiç değişmez.
İkizin kartı çevrimdışı göstermesi neden biraz zaman alıyor?
Broker last will mesajını ancak keepalive aralığı iletişim olmadan geçtikten sonra yayınlar; burada bu süre yaklaşık 30 ile 45 saniyedir. Daha kısa bir keepalive, biraz daha fazla trafik pahasına arızaları daha hızlı tespit eder.
İkizi aynı anda birkaç kişi görüntüleyebilir mi?
Evet. Her tarayıcı, kendi client ID'sine sahip ayrı bir MQTT abonesidir. Ücretsiz Serverless planında cihazlar ve izleyicilerin toplamını 25 bağlantı sınırı içinde tutun.



