Lewati ke isi

Koneksi Alternatif IoT dengan Wokwi

Pada pertemuan kali ini akan membahas berbagai protokol komunikasi IoT selain MQTT. Mahasiswa akan mempelajari protokol HTTP/REST API, WebSocket, dan CoAP, beserta implementasi praktis menggunakan ESP32 di Wokwi simulator. Setiap protokol memiliki karakteristik dan use case yang berbeda dalam ekosistem IoT.

Alat dan Bahan

Simulator:

  • Wokwi Online Platform (https://wokwi.com)
  • Akun Wokwi (gratis, login dengan Google/GitHub)

Software:

  • Web Browser (Chrome, Firefox, Edge)
  • Postman atau Thunder Client (untuk test REST API)
  • Python 3.x (untuk mock server)
  • Text Editor untuk mencatat konfigurasi

Server/Backend:

  • HTTP: RequestBin (https://requestbin.com) - untuk test HTTP POST
  • WebSocket: WebSocket.org Echo Server
  • CoAP: CoAP.me Public Server (coap://coap.me)

Network:

  • Koneksi Internet untuk akses Wokwi dan public servers
  • WiFi connection akan di-mock oleh Wokwi simulator

Bagian 1: Pengenalan Protokol IoT

1.1 Perbandingan Protokol IoT

Sebelum mulai praktikum, penting memahami karakteristik masing-masing protokol.

Tabel Perbandingan:

Aspek HTTP/REST MQTT WebSocket CoAP
Arsitektur Request-Response Pub-Sub Full-Duplex Request-Response
Transport TCP TCP TCP UDP
Overhead Tinggi Rendah Sedang Sangat Rendah
Bandwidth Besar Kecil Sedang Sangat Kecil
Real-time ✓✓
Power Consumption Tinggi Rendah Sedang Sangat Rendah
Use Case Web API, CRUD Telemetry, Command Chat, Live Update Constrained Device
Port Default 80/443 1883/8883 80/443 5683/5684

Kapan Menggunakan Protokol Tertentu:

HTTP/REST:
✓ Integration dengan web services
✓ CRUD operations (Create, Read, Update, Delete)
✓ Device yang tidak battery-powered
✗ Real-time monitoring
✗ Constrained devices (low power/bandwidth)

MQTT:
✓ Sensor data streaming
✓ Command and control
✓ Battery-powered devices
✓ Unreliable network
✗ Request-response patterns
✗ Large payload (> 256 MB)

WebSocket:
✓ Bi-directional communication
✓ Real-time updates (chat, dashboard)
✓ Live streaming data
✗ Simple request-response
✗ Low-power devices

CoAP:
✓ Constrained devices (memory, power)
✓ Low bandwidth networks (2G, NB-IoT)
✓ UDP-based communication
✗ Complex state management
✗ High-frequency updates

1.2 Arsitektur Komunikasi

Diagram Perbandingan Arsitektur:

HTTP/REST (Request-Response):
┌──────────┐                  ┌──────────┐
│  Device  │ ─── HTTP POST → │  Server  │
│  (ESP32) │                  │   API    │
│          │ ← HTTP Response ─│          │
└──────────┘                  └──────────┘
Polling: Device request berulang


MQTT (Publish-Subscribe):
┌──────────┐                  ┌──────────┐
│ Device 1 │ ─── Publish ───→ │  Broker  │
└──────────┘                  └────┬─────┘
┌──────────┐                       │
│ Device 2 │ ←──── Subscribe ──────┘
└──────────┘
Push: Broker forward messages


WebSocket (Full-Duplex):
┌──────────┐  ════════════════ ┌──────────┐
│  Device  │  ← Send/Receive → │  Server  │
│  (ESP32) │  ════════════════ │          │
└──────────┘                   └──────────┘
Persistent: Connection always open


CoAP (Request-Response over UDP):
┌──────────┐                  ┌──────────┐
│  Device  │ ─── CoAP GET ──→ │  Server  │
│  (ESP32) │                  │   CoAP   │
│          │ ← CoAP Response ─│          │
└──────────┘                  └──────────┘
UDP: Lightweight, connectionless

1.3 Setup Project Wokwi

Praktikum 1.1: Persiapan Project

Langkah 1: Buat Project Baru

  1. Login ke Wokwi (https://wokwi.com)
  2. Klik "New Project"
  3. Pilih template "ESP32"
  4. Rename project: "IoT Protocols Comparison"

Langkah 2: Setup Hardware

Kita akan gunakan setup sederhana: ESP32 + DHT22 + LED

  1. Klik "+" (Add part)
  2. Tambahkan DHT22 sensor
  3. Tambahkan Red LED
  4. Tambahkan Resistor 220Ω

Wiring:

Component Pin ESP32 Pin
DHT22 VCC 3V3
DHT22 DATA GPIO 15
DHT22 GND GND
LED Anode (+) → Resistor → GPIO 2
LED Cathode (-) GND

Langkah 3: Verify Diagram

Setelah wiring selesai, diagram seharusnya seperti ini:

        DHT22
       ┌─┬─┬─┐
       │ │ │ │
      3V3│ │GND
         15

    ┌─────────┐
    │  ESP32  │
    │         │
    │ 2   GND │
    └─┬───┬───┘
      │   │
    [R220] │
      │   │
    [LED] │
      └───┘

Bagian 2: HTTP/REST API Protocol

HTTP adalah protokol request-response yang paling umum digunakan untuk komunikasi web. Dalam IoT, HTTP digunakan untuk mengirim data sensor ke server atau mengambil konfigurasi dari cloud.

2.1 Pengenalan HTTP/REST

Konsep Dasar HTTP:

  • Request Methods:
  • GET: Mengambil data dari server
  • POST: Mengirim data ke server (paling umum untuk IoT)
  • PUT: Update data di server
  • DELETE: Hapus data di server

  • HTTP Status Codes:

  • 200 OK: Request berhasil
  • 201 Created: Resource dibuat
  • 400 Bad Request: Request invalid
  • 404 Not Found: Resource tidak ditemukan
  • 500 Server Error: Error di server

REST API Structure:

HTTP Method + Endpoint + Headers + Body

Example:
POST /api/sensor/data HTTP/1.1
Host: example.com
Content-Type: application/json

{
  "temperature": 25.5,
  "humidity": 60.2,
  "device_id": "esp32-001"
}

2.2 Implementasi HTTP POST

Praktikum 2.1: Kirim Data Sensor via HTTP

Kita akan implementasi HTTP POST untuk mengirim data sensor ke RequestBin (service untuk test HTTP requests).

Langkah 1: Setup RequestBin

  1. Buka browser, akses: https://requestbin.com
  2. Klik "Create a RequestBin"
  3. Copy URL endpoint yang diberikan (contoh: https://requestbin.com/r/xxxxx)
  4. Catat URL ini untuk digunakan di code

Langkah 2: Code HTTP Client

Buka main.py di Wokwi, hapus isi sebelumnya, dan mulai dengan imports:

# ============================================
# HTTP/REST Protocol - IoT Data Sender
# Platform: Wokwi ESP32 Simulator
# ============================================

import network
import urequests as requests  # HTTP library untuk MicroPython
import ujson as json
import time
from machine import Pin
import dht

print("\n" + "="*50)
print("HTTP/REST Protocol Implementation")
print("Platform: Wokwi ESP32")
print("="*50)

Langkah 3: Konfigurasi

Tambahkan konfigurasi di bawah imports:

# ============================================
# CONFIGURATION
# ============================================

# WiFi Configuration
WIFI_SSID = "Wokwi-GUEST"
WIFI_PASSWORD = ""

# HTTP API Configuration
# Ganti dengan URL RequestBin Anda!
API_ENDPOINT = "https://requestbin.com/r/xxxxx"  # TODO: Update ini!
API_TIMEOUT = 10  # Timeout dalam detik

# Headers untuk HTTP request
HEADERS = {
    'Content-Type': 'application/json',
    'User-Agent': 'ESP32-Wokwi/1.0'
}

# Timing Configuration
SEND_INTERVAL = 10  # Kirim data setiap 10 detik

print("[CONFIG] Configuration loaded")
print(f"[CONFIG] API Endpoint: {API_ENDPOINT}")
print(f"[CONFIG] Send interval: {SEND_INTERVAL}s")

⚠️ PENTING: Ganti API_ENDPOINT dengan URL RequestBin yang Anda dapatkan!

Langkah 4: Hardware Setup

Tambahkan setup hardware:

# ============================================
# HARDWARE SETUP
# ============================================

# LED setup - GPIO 2
led = Pin(2, Pin.OUT)
led.value(0)
print("[HARDWARE] LED initialized on GPIO 2")

# DHT22 setup - GPIO 15
dht_sensor = dht.DHT22(Pin(15))
print("[HARDWARE] DHT22 sensor initialized on GPIO 15")

# State variables
last_send_time = 0
send_count = 0

Langkah 5: WiFi Connection Function

Tambahkan fungsi untuk connect WiFi:

# ============================================
# WIFI CONNECTION
# ============================================

def connect_wifi():
    """Connect to WiFi network"""
    print("\n[WIFI] Connecting to WiFi...")
    print(f"[WIFI] SSID: {WIFI_SSID}")

    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)

    if not wlan.isconnected():
        wlan.connect(WIFI_SSID, WIFI_PASSWORD)
        print("[WIFI] Waiting for connection...")

        max_wait = 20
        while max_wait > 0:
            if wlan.isconnected():
                break
            time.sleep(1)
            max_wait -= 1
            print(".", end="")

        print()  # New line

    if wlan.isconnected():
        print("[WIFI] ✓ Connected successfully!")
        print(f"[WIFI] IP Address: {wlan.ifconfig()[0]}")
        return True
    else:
        print("[WIFI] ✗ Connection failed!")
        return False

Langkah 6: Sensor Reading Function

Tambahkan fungsi untuk baca sensor:

# ============================================
# SENSOR FUNCTIONS
# ============================================

def read_sensor():
    """
    Read temperature and humidity from DHT22
    Returns: dict with sensor data or None if error
    """
    try:
        dht_sensor.measure()
        time.sleep(0.5)

        temp = dht_sensor.temperature()
        hum = dht_sensor.humidity()

        if temp is None or hum is None:
            print("[SENSOR] ✗ Failed to read sensor")
            return None

        data = {
            'temperature': round(temp, 1),
            'humidity': round(hum, 1)
        }

        print(f"[SENSOR] ✓ Read: Temp={data['temperature']}°C, Humidity={data['humidity']}%")
        return data

    except Exception as e:
        print(f"[SENSOR] ✗ Error: {e}")
        return None

Langkah 7: HTTP POST Function

Ini adalah fungsi utama untuk kirim data via HTTP:

# ============================================
# HTTP FUNCTIONS
# ============================================

def send_http_post(sensor_data):
    """
    Send sensor data via HTTP POST
    Args:
        sensor_data: dict with temperature and humidity
    Returns:
        bool: True if success, False if failed
    """
    global send_count

    if sensor_data is None:
        print("[HTTP] ✗ Cannot send - no sensor data")
        return False

    # Blink LED untuk indicate sending
    led.value(1)

    try:
        # Prepare payload
        payload = {
            'device_id': 'esp32-wokwi-001',
            'temperature': sensor_data['temperature'],
            'humidity': sensor_data['humidity'],
            'timestamp': time.time(),
            'count': send_count
        }

        # Convert ke JSON
        payload_json = json.dumps(payload)

        print(f"\n[HTTP] Sending POST request...")
        print(f"[HTTP] Endpoint: {API_ENDPOINT}")
        print(f"[HTTP] Payload: {payload_json}")

        # Send HTTP POST request
        response = requests.post(
            API_ENDPOINT,
            data=payload_json,
            headers=HEADERS,
            timeout=API_TIMEOUT
        )

        # Check response
        print(f"[HTTP] Status Code: {response.status_code}")

        if response.status_code in [200, 201]:
            print("[HTTP] ✓ Data sent successfully!")
            send_count += 1

            # Print response body jika ada
            if response.text:
                print(f"[HTTP] Response: {response.text[:100]}")  # First 100 chars

            success = True
        else:
            print(f"[HTTP] ✗ Request failed with status {response.status_code}")
            success = False

        # Cleanup
        response.close()

        # LED off
        led.value(0)

        return success

    except Exception as e:
        print(f"[HTTP] ✗ Exception: {e}")
        led.value(0)
        return False

Penjelasan Code:

  1. LED Indicator: LED nyala saat sending, mati setelah selesai
  2. Payload Structure: Include device_id, sensor data, timestamp, dan counter
  3. requests.post(): Library urequests untuk HTTP request
  4. Error Handling: Try-catch untuk handle network errors
  5. Response Validation: Check status code untuk verify success

Langkah 8: Main Program

Tambahkan main loop untuk integrate semua:

# ============================================
# MAIN PROGRAM
# ============================================

def main():
    """Main program loop"""
    global last_send_time

    print("\n" + "="*50)
    print("Starting HTTP IoT Application")
    print("="*50)

    # Connect WiFi
    if not connect_wifi():
        print("[ERROR] WiFi failed - stopping")
        return

    time.sleep(2)

    print("\n" + "="*50)
    print("System Ready!")
    print("="*50)
    print(f"[INFO] Sending data every {SEND_INTERVAL} seconds")
    print("[INFO] Press STOP to exit")
    print("="*50 + "\n")

    last_send_time = time.time()
    loop_count = 0

    # Main loop
    while True:
        try:
            loop_count += 1
            current_time = time.time()

            # Check if it's time to send
            if (current_time - last_send_time) >= SEND_INTERVAL:
                print(f"\n{'='*50}")
                print(f"Loop #{loop_count}")
                print(f"{'='*50}")

                # Read sensor
                sensor_data = read_sensor()

                # Send via HTTP
                if sensor_data:
                    success = send_http_post(sensor_data)

                    if success:
                        print("[STATUS] ✓ Cycle completed successfully")
                    else:
                        print("[STATUS] ✗ Cycle failed")

                # Update timing
                last_send_time = current_time

            # Small delay
            time.sleep(0.5)

        except KeyboardInterrupt:
            print("\n[INFO] Program stopped by user")
            break

        except Exception as e:
            print(f"[ERROR] Main loop error: {e}")
            time.sleep(5)

    # Cleanup
    led.value(0)
    print("[INFO] Program ended")

# Entry point
if __name__ == "__main__":
    main()

Langkah 9: Test HTTP POST

  1. Update API_ENDPOINT dengan URL RequestBin Anda
  2. Klik "Play" di Wokwi
  3. Tunggu WiFi connect
  4. Setiap 10 detik, ESP32 akan send data
  5. Lihat Serial Monitor untuk logs

Expected Output:

==================================================
HTTP/REST Protocol Implementation
Platform: Wokwi ESP32
==================================================
[CONFIG] Configuration loaded
[CONFIG] API Endpoint: https://requestbin.com/r/xxxxx
[CONFIG] Send interval: 10s
[HARDWARE] LED initialized on GPIO 2
[HARDWARE] DHT22 sensor initialized on GPIO 15

[WIFI] Connecting to WiFi...
[WIFI] SSID: Wokwi-GUEST
[WIFI] ✓ Connected successfully!
[WIFI] IP Address: 192.168.1.100

==================================================
System Ready!
==================================================
[INFO] Sending data every 10 seconds
[INFO] Press STOP to exit
==================================================

==================================================
Loop #1
==================================================
[SENSOR] ✓ Read: Temp=24.5°C, Humidity=55.3%

[HTTP] Sending POST request...
[HTTP] Endpoint: https://requestbin.com/r/xxxxx
[HTTP] Payload: {"device_id":"esp32-wokwi-001","temperature":24.5,"humidity":55.3,"timestamp":123456,"count":0}
[HTTP] Status Code: 200
[HTTP] ✓ Data sent successfully!
[STATUS] ✓ Cycle completed successfully

Langkah 10: Verify di RequestBin

  1. Buka RequestBin URL di browser
  2. Refresh page
  3. Akan melihat request yang masuk dengan:
  4. Method: POST
  5. Headers: Content-Type, User-Agent
  6. Body: JSON dengan sensor data

Screenshot RequestBin akan show:

POST /r/xxxxx
Headers:
  Content-Type: application/json
  User-Agent: ESP32-Wokwi/1.0

Body:
{
  "device_id": "esp32-wokwi-001",
  "temperature": 24.5,
  "humidity": 55.3,
  "timestamp": 123456,
  "count": 0
}

2.3 HTTP GET Request

Praktikum 2.2: Retrieve Configuration via HTTP GET

Sekarang kita implementasi HTTP GET untuk mengambil konfigurasi dari server.

Use Case: ESP32 request update interval atau threshold dari server.

Langkah 1: Tambahkan Config Endpoint

Tambahkan di section CONFIGURATION:

# GET endpoint untuk ambil config
CONFIG_ENDPOINT = "https://api.github.com/zen"  # Public API untuk test
# Atau bisa pakai JSONPlaceholder: "https://jsonplaceholder.typicode.com/todos/1"

Langkah 2: HTTP GET Function

Tambahkan fungsi di section HTTP FUNCTIONS:

def fetch_config_http():
    """
    Fetch configuration from server via HTTP GET
    Returns: dict with config or None if failed
    """
    try:
        print(f"\n[HTTP] Sending GET request...")
        print(f"[HTTP] Endpoint: {CONFIG_ENDPOINT}")

        # Send GET request
        response = requests.get(
            CONFIG_ENDPOINT,
            headers=HEADERS,
            timeout=API_TIMEOUT
        )

        print(f"[HTTP] Status Code: {response.status_code}")

        if response.status_code == 200:
            print("[HTTP] ✓ Config retrieved successfully!")

            # Parse response
            try:
                config = json.loads(response.text)
                print(f"[HTTP] Config: {config}")
                response.close()
                return config
            except:
                # Jika bukan JSON, return sebagai text
                print(f"[HTTP] Response (text): {response.text}")
                response.close()
                return {'message': response.text}
        else:
            print(f"[HTTP] ✗ Request failed")
            response.close()
            return None

    except Exception as e:
        print(f"[HTTP] ✗ Exception: {e}")
        return None

Langkah 3: Update Main Loop

Modify main() untuk fetch config di awal:

# Dalam main(), setelah WiFi connect, tambahkan:

    # Fetch initial config
    print("\n[CONFIG] Fetching configuration from server...")
    config = fetch_config_http()
    if config:
        print("[CONFIG] ✓ Configuration loaded")
        # Bisa update SEND_INTERVAL based on config
        # SEND_INTERVAL = config.get('interval', SEND_INTERVAL)
    else:
        print("[CONFIG] ⚠ Using default configuration")

Test GET Request:

  1. Run simulator
  2. Setelah WiFi connect, akan fetch config
  3. Lihat response di Serial Monitor

Bagian 3: WebSocket Protocol

WebSocket adalah protokol full-duplex yang memungkinkan komunikasi bi-directional real-time antara client dan server. Berbeda dengan HTTP yang request-response, WebSocket maintain persistent connection.

3.1 Pengenalan WebSocket

Karakteristik WebSocket:

  • Persistent Connection: Connection tetap terbuka, tidak perlu reconnect
  • Bi-directional: Client dan server bisa send message kapan saja
  • Low Latency: Tidak ada overhead HTTP handshake setiap request
  • Real-time: Ideal untuk chat, live updates, streaming

WebSocket Handshake:

Client → Server:
GET /socket HTTP/1.1
Upgrade: websocket
Connection: Upgrade

Server → Client:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade

[Connection upgraded ke WebSocket]
[Both sides can now send messages anytime]

Diagram Communication:

HTTP (Polling):               WebSocket:

Client ─── Request ──→        Client ═══════════ Server
       ←── Response ─          │   Send/Receive   │
       (wait)                  │   ═══════════    │
Client ─── Request ──→         │   Persistent     │
       ←── Response ─          └═════════════════┘
       (repeat)

Overhead: High                Overhead: Low
Latency: High                 Latency: Very Low

3.2 Implementasi WebSocket Client

Praktikum 3.1: WebSocket Communication

Kita akan gunakan WebSocket.org Echo Server untuk test. Server ini akan echo back semua message yang dikirim.

Langkah 1: Create New File

Untuk WebSocket, kita buat file baru agar tidak conflict dengan HTTP code.

  1. Di Wokwi, buat tab baru atau clear main.py
  2. Nama file: websocket_client.py

Langkah 2: Import dan Configuration

# ============================================
# WebSocket Protocol - Real-time Communication
# Platform: Wokwi ESP32 Simulator
# ============================================

import network
import time
import socket
import ubinascii
import uhashlib
import urandom
from machine import Pin
import dht

print("\n" + "="*50)
print("WebSocket Protocol Implementation")
print("Platform: Wokwi ESP32")
print("="*50)

# ============================================
# CONFIGURATION
# ============================================

# WiFi Configuration
WIFI_SSID = "Wokwi-GUEST"
WIFI_PASSWORD = ""

# WebSocket Server Configuration
WS_HOST = "echo.websocket.org"
WS_PORT = 80
WS_PATH = "/"

# Timing
HEARTBEAT_INTERVAL = 30  # Send ping every 30s
SEND_INTERVAL = 5  # Send sensor data every 5s

print("[CONFIG] Configuration loaded")
print(f"[CONFIG] WebSocket Server: ws://{WS_HOST}:{WS_PORT}")

Langkah 3: Hardware Setup

# ============================================
# HARDWARE SETUP
# ============================================

led = Pin(2, Pin.OUT)
led.value(0)
print("[HARDWARE] LED initialized on GPIO 2")

dht_sensor = dht.DHT22(Pin(15))
print("[HARDWARE] DHT22 sensor initialized on GPIO 15")

# State
ws_connected = False
last_ping_time = 0
last_send_time = 0

Langkah 4: WiFi Function

# ============================================
# WIFI CONNECTION
# ============================================

def connect_wifi():
    """Connect to WiFi"""
    print("\n[WIFI] Connecting...")
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)

    if not wlan.isconnected():
        wlan.connect(WIFI_SSID, WIFI_PASSWORD)
        max_wait = 20
        while max_wait > 0 and not wlan.isconnected():
            time.sleep(1)
            max_wait -= 1

    if wlan.isconnected():
        print("[WIFI] ✓ Connected!")
        print(f"[WIFI] IP: {wlan.ifconfig()[0]}")
        return True
    return False

Langkah 5: WebSocket Handshake

WebSocket dimulai dengan HTTP upgrade handshake:

# ============================================
# WEBSOCKET FUNCTIONS
# ============================================

def generate_websocket_key():
    """Generate random WebSocket key"""
    random_bytes = bytes([urandom.getrandbits(8) for _ in range(16)])
    return ubinascii.b2a_base64(random_bytes).decode().strip()

def websocket_handshake(sock):
    """
    Perform WebSocket handshake
    Returns: True if success, False if failed
    """
    print("[WS] Performing handshake...")

    # Generate random key
    key = generate_websocket_key()

    # Build handshake request
    handshake = (
        f"GET {WS_PATH} HTTP/1.1\r\n"
        f"Host: {WS_HOST}\r\n"
        f"Upgrade: websocket\r\n"
        f"Connection: Upgrade\r\n"
        f"Sec-WebSocket-Key: {key}\r\n"
        f"Sec-WebSocket-Version: 13\r\n"
        f"\r\n"
    )

    # Send handshake
    sock.send(handshake.encode())
    print("[WS] Handshake request sent")

    # Wait for response
    try:
        response = sock.recv(1024).decode()
        print(f"[WS] Response received ({len(response)} bytes)")

        # Check for 101 Switching Protocols
        if "101" in response and "Upgrade" in response:
            print("[WS] ✓ Handshake successful!")
            return True
        else:
            print("[WS] ✗ Handshake failed")
            print(f"[WS] Response: {response[:200]}")
            return False

    except Exception as e:
        print(f"[WS] ✗ Handshake error: {e}")
        return False

Langkah 6: WebSocket Frame Format

WebSocket menggunakan frame format khusus. Untuk simplicity, kita buat fungsi helper:

def create_websocket_frame(payload, opcode=0x01):
    """
    Create WebSocket frame
    opcode: 0x01 = text, 0x02 = binary, 0x08 = close, 0x09 = ping
    """
    # Convert payload to bytes
    if isinstance(payload, str):
        payload = payload.encode()

    payload_len = len(payload)

    # Frame header
    frame = bytearray()
    frame.append(0x80 | opcode)  # FIN bit + opcode

    # Payload length
    if payload_len <= 125:
        frame.append(0x80 | payload_len)  # MASK bit + length
    elif payload_len <= 65535:
        frame.append(0x80 | 126)
        frame.extend(payload_len.to_bytes(2, 'big'))
    else:
        frame.append(0x80 | 127)
        frame.extend(payload_len.to_bytes(8, 'big'))

    # Masking key (required for client)
    mask = bytes([urandom.getrandbits(8) for _ in range(4)])
    frame.extend(mask)

    # Masked payload
    masked = bytearray(payload_len)
    for i in range(payload_len):
        masked[i] = payload[i] ^ mask[i % 4]
    frame.extend(masked)

    return bytes(frame)

def send_websocket_message(sock, message):
    """Send text message via WebSocket"""
    try:
        frame = create_websocket_frame(message, opcode=0x01)
        sock.send(frame)
        print(f"[WS] ✓ Sent: {message}")
        return True
    except Exception as e:
        print(f"[WS] ✗ Send error: {e}")
        return False

Langkah 7: Read WebSocket Messages

def read_websocket_message(sock, timeout=1):
    """
    Read incoming WebSocket message
    Returns: message string or None
    """
    try:
        # Set non-blocking with timeout
        sock.settimeout(timeout)

        # Read frame header (minimum 2 bytes)
        header = sock.recv(2)
        if len(header) < 2:
            return None

        # Parse first byte
        fin = (header[0] & 0x80) != 0
        opcode = header[0] & 0x0F

        # Parse second byte
        masked = (header[1] & 0x80) != 0
        payload_len = header[1] & 0x7F

        # Extended payload length
        if payload_len == 126:
            ext_len = sock.recv(2)
            payload_len = int.from_bytes(ext_len, 'big')
        elif payload_len == 127:
            ext_len = sock.recv(8)
            payload_len = int.from_bytes(ext_len, 'big')

        # Read masking key (if masked - servers don't mask)
        if masked:
            mask = sock.recv(4)

        # Read payload
        payload = bytearray()
        while len(payload) < payload_len:
            chunk = sock.recv(min(4096, payload_len - len(payload)))
            if not chunk:
                break
            payload.extend(chunk)

        # Unmask if needed
        if masked:
            for i in range(len(payload)):
                payload[i] ^= mask[i % 4]

        # Handle opcodes
        if opcode == 0x01:  # Text frame
            message = payload.decode('utf-8')
            print(f"[WS] ✓ Received: {message}")
            return message
        elif opcode == 0x08:  # Close frame
            print("[WS] ⚠ Close frame received")
            return None
        elif opcode == 0x09:  # Ping frame
            print("[WS] ⚠ Ping received")
            # Should send pong
            return None
        elif opcode == 0x0A:  # Pong frame
            print("[WS] ✓ Pong received")
            return None

        return None

    except Exception as e:
        # Timeout or error - normal in non-blocking mode
        return None

Langkah 8: Sensor Function

# ============================================
# SENSOR FUNCTIONS
# ============================================

def read_sensor():
    """Read DHT22 sensor"""
    try:
        dht_sensor.measure()
        time.sleep(0.5)

        temp = dht_sensor.temperature()
        hum = dht_sensor.humidity()

        if temp is None or hum is None:
            return None

        data = {
            'temperature': round(temp, 1),
            'humidity': round(hum, 1)
        }

        print(f"[SENSOR] ✓ Temp={data['temperature']}°C, Hum={data['humidity']}%")
        return data

    except Exception as e:
        print(f"[SENSOR] ✗ Error: {e}")
        return None

Langkah 9: Main Program dengan WebSocket

# ============================================
# MAIN PROGRAM
# ============================================

def main():
    """Main WebSocket client program"""
    global ws_connected, last_ping_time, last_send_time

    print("\n" + "="*50)
    print("Starting WebSocket IoT Application")
    print("="*50)

    # Connect WiFi
    if not connect_wifi():
        print("[ERROR] WiFi failed")
        return

    time.sleep(2)

    # Connect WebSocket
    print(f"\n[WS] Connecting to ws://{WS_HOST}:{WS_PORT}")

    try:
        # Create socket
        addr = socket.getaddrinfo(WS_HOST, WS_PORT)[0][-1]
        sock = socket.socket()
        sock.connect(addr)
        print("[WS] TCP connection established")

        # Perform handshake
        if not websocket_handshake(sock):
            print("[ERROR] WebSocket handshake failed")
            sock.close()
            return

        ws_connected = True

        print("\n" + "="*50)
        print("WebSocket Connected!")
        print("="*50)
        print("[INFO] Sending sensor data every 5 seconds")
        print("[INFO] Echo server will reply with same message")
        print("[INFO] Press STOP to exit")
        print("="*50 + "\n")

        last_ping_time = time.time()
        last_send_time = time.time()
        msg_count = 0

        # Main loop
        while ws_connected:
            try:
                current_time = time.time()

                # Send heartbeat ping
                if (current_time - last_ping_time) >= HEARTBEAT_INTERVAL:
                    ping_frame = create_websocket_frame("ping", opcode=0x09)
                    sock.send(ping_frame)
                    print("[WS] ❤ Heartbeat ping sent")
                    last_ping_time = current_time

                # Send sensor data
                if (current_time - last_send_time) >= SEND_INTERVAL:
                    msg_count += 1
                    print(f"\n{'='*50}")
                    print(f"Message #{msg_count}")
                    print(f"{'='*50}")

                    # Blink LED
                    led.value(1)

                    # Read sensor
                    sensor_data = read_sensor()

                    if sensor_data:
                        # Format message
                        import ujson as json
                        message = json.dumps({
                            'device_id': 'esp32-wokwi-001',
                            'temperature': sensor_data['temperature'],
                            'humidity': sensor_data['humidity'],
                            'timestamp': int(current_time),
                            'count': msg_count
                        })

                        # Send via WebSocket
                        send_websocket_message(sock, message)

                    led.value(0)
                    last_send_time = current_time

                # Check for incoming messages
                incoming = read_websocket_message(sock, timeout=0.1)
                if incoming:
                    print(f"[WS] Echo response: {incoming}")

                # Small delay
                time.sleep(0.1)

            except KeyboardInterrupt:
                print("\n[INFO] Interrupted by user")
                break

            except Exception as e:
                print(f"[ERROR] Loop error: {e}")
                ws_connected = False
                break

        # Cleanup
        print("\n[WS] Closing connection...")
        try:
            close_frame = create_websocket_frame("", opcode=0x08)
            sock.send(close_frame)
            time.sleep(0.5)
        except:
            pass

        sock.close()
        print("[WS] Connection closed")

    except Exception as e:
        print(f"[ERROR] Connection error: {e}")

    finally:
        led.value(0)
        print("[INFO] Program ended")

# Entry point
if __name__ == "__main__":
    main()

Langkah 10: Test WebSocket

  1. Copy semua code WebSocket ke main.py
  2. Klik "Play" di Wokwi
  3. Tunggu WebSocket handshake
  4. Setiap 5 detik, ESP32 kirim sensor data
  5. Echo server akan reply dengan message yang sama

Expected Output:

==================================================
WebSocket Protocol Implementation
Platform: Wokwi ESP32
==================================================
[CONFIG] Configuration loaded
[CONFIG] WebSocket Server: ws://echo.websocket.org:80
[HARDWARE] LED initialized on GPIO 2
[HARDWARE] DHT22 sensor initialized on GPIO 15

[WIFI] Connecting...
[WIFI] ✓ Connected!
[WIFI] IP: 192.168.1.100

[WS] Connecting to ws://echo.websocket.org:80
[WS] TCP connection established
[WS] Performing handshake...
[WS] Handshake request sent
[WS] Response received (234 bytes)
[WS] ✓ Handshake successful!

==================================================
WebSocket Connected!
==================================================
[INFO] Sending sensor data every 5 seconds
[INFO] Echo server will reply with same message
[INFO] Press STOP to exit
==================================================

==================================================
Message #1
==================================================
[SENSOR] ✓ Temp=24.5°C, Hum=55.3%
[WS] ✓ Sent: {"device_id":"esp32-wokwi-001","temperature":24.5,"humidity":55.3,"timestamp":123456,"count":1}
[WS] ✓ Received: {"device_id":"esp32-wokwi-001","temperature":24.5,"humidity":55.3,"timestamp":123456,"count":1}
[WS] Echo response: {"device_id":"esp32-wokwi-001","temperature":24.5,"humidity":55.3,"timestamp":123456,"count":1}

==================================================
Message #2
==================================================
[SENSOR] ✓ Temp=24.6°C, Hum=55.1%
[WS] ✓ Sent: {"device_id":"esp32-wokwi-001","temperature":24.6,"humidity":55.1,"timestamp":123461,"count":2}
[WS] ✓ Received: {"device_id":"esp32-wokwi-001","temperature":24.6,"humidity":55.1,"timestamp":123461,"count":2}
[WS] Echo response: {"device_id":"esp32-wokwi-001","temperature":24.6,"humidity":55.1,"timestamp":123461,"count":2}

[WS] ❤ Heartbeat ping sent

Keuntungan WebSocket: - ✓ Bi-directional: Server bisa push data tanpa polling - ✓ Real-time: Latency sangat rendah - ✓ Persistent: Connection tetap open, tidak perlu reconnect - ✓ Efficient: Tidak ada overhead HTTP headers per message


Bagian 4: CoAP Protocol

CoAP (Constrained Application Protocol) adalah protokol khusus untuk constrained devices dengan memory dan power terbatas. CoAP menggunakan UDP dan dirancang mirip HTTP tapi jauh lebih lightweight.

4.1 Pengenalan CoAP

Karakteristik CoAP:

  • Transport: UDP (bukan TCP)
  • Message Size: Sangat kecil (4 bytes header minimum)
  • Methods: GET, POST, PUT, DELETE (mirip HTTP)
  • Reliability: Optional confirmable messages
  • Resource Discovery: Built-in resource discovery

Perbandingan dengan HTTP:

Aspek HTTP CoAP
Transport TCP UDP
Header Size ~200-500 bytes 4-10 bytes
Message Type Request-Response CON, NON, ACK, RST
Default Port 80/443 5683/5684
Bandwidth High Very Low
Ideal For Web services Sensor networks

CoAP Message Types:

CON (Confirmable):
Client ─── CON Request ──→ Server
       ←──── ACK Response ─
Reliable, perlu acknowledgment

NON (Non-confirmable):
Client ─── NON Request ──→ Server
       ←──── NON Response ─
Unreliable, no acknowledgment

ACK (Acknowledgment):
Confirm CON message received

RST (Reset):
Reject message / reset connection

CoAP Message Format:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Ver| T |  TKL  |      Code     |          Message ID           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   Token (if any, TKL bytes) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   Options (if any) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 1 1 1 1 1 1 1|    Payload (if any) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Ver: Version (2 bits) = 01
T: Type (2 bits) = CON(0), NON(1), ACK(2), RST(3)
TKL: Token Length (4 bits)
Code: Method/Response Code (8 bits)

4.2 Implementasi CoAP Client

Praktikum 4.1: CoAP GET dan POST

Kita akan implementasi simple CoAP client untuk communicate dengan public CoAP server.

Langkah 1: Create CoAP Client File

Buat file baru atau clear main.py:

# ============================================
# CoAP Protocol - Constrained Application Protocol
# Platform: Wokwi ESP32 Simulator
# ============================================

import network
import time
import socket
import struct
import urandom
from machine import Pin
import dht

print("\n" + "="*50)
print("CoAP Protocol Implementation")
print("Platform: Wokwi ESP32")
print("="*50)

# ============================================
# CONFIGURATION
# ============================================

# WiFi Configuration
WIFI_SSID = "Wokwi-GUEST"
WIFI_PASSWORD = ""

# CoAP Server Configuration
# Menggunakan coap.me public test server
COAP_SERVER = "coap.me"
COAP_PORT = 5683

# CoAP Constants
COAP_VERSION = 1
COAP_TYPE_CON = 0  # Confirmable
COAP_TYPE_NON = 1  # Non-confirmable
COAP_TYPE_ACK = 2  # Acknowledgment
COAP_TYPE_RST = 3  # Reset

# CoAP Method Codes
COAP_GET = 1
COAP_POST = 2
COAP_PUT = 3
COAP_DELETE = 4

# CoAP Response Codes
COAP_CREATED = 65      # 2.01
COAP_DELETED = 66      # 2.02
COAP_VALID = 67        # 2.03
COAP_CHANGED = 68      # 2.04
COAP_CONTENT = 69      # 2.05

# Timing
SEND_INTERVAL = 10  # Send data every 10 seconds

print("[CONFIG] Configuration loaded")
print(f"[CONFIG] CoAP Server: coap://{COAP_SERVER}:{COAP_PORT}")

Langkah 2: Hardware Setup

# ============================================
# HARDWARE SETUP
# ============================================

led = Pin(2, Pin.OUT)
led.value(0)
print("[HARDWARE] LED initialized on GPIO 2")

dht_sensor = dht.DHT22(Pin(15))
print("[HARDWARE] DHT22 sensor initialized on GPIO 15")

# State
message_id = urandom.getrandbits(16)

Langkah 3: WiFi Function

# ============================================
# WIFI CONNECTION
# ============================================

def connect_wifi():
    """Connect to WiFi"""
    print("\n[WIFI] Connecting...")
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)

    if not wlan.isconnected():
        wlan.connect(WIFI_SSID, WIFI_PASSWORD)
        max_wait = 20
        while max_wait > 0 and not wlan.isconnected():
            time.sleep(1)
            max_wait -= 1

    if wlan.isconnected():
        print("[WIFI] ✓ Connected!")
        print(f"[WIFI] IP: {wlan.ifconfig()[0]}")
        return True

    print("[WIFI] ✗ Connection failed")
    return False

Langkah 4: CoAP Message Builder

# ============================================
# COAP FUNCTIONS
# ============================================

def build_coap_message(msg_type, code, token=b'', uri_path='', payload=b''):
    """
    Build CoAP message packet

    Args:
        msg_type: COAP_TYPE_CON, NON, ACK, or RST
        code: Method code (GET, POST, etc) or response code
        token: Token bytes (optional)
        uri_path: URI path string (e.g., "sensor/data")
        payload: Payload bytes

    Returns:
        bytes: Complete CoAP packet
    """
    global message_id

    # Increment message ID
    message_id = (message_id + 1) & 0xFFFF

    # Build header (4 bytes)
    # Byte 0: Ver(2) | Type(2) | TKL(4)
    tkl = len(token) & 0x0F
    byte0 = (COAP_VERSION << 6) | (msg_type << 4) | tkl

    # Byte 1: Code (class.detail)
    byte1 = code

    # Bytes 2-3: Message ID
    header = struct.pack('!BBH', byte0, byte1, message_id)

    # Add token
    packet = header + token

    # Add options (Uri-Path)
    if uri_path:
        # Split path by '/'
        path_segments = [seg for seg in uri_path.split('/') if seg]

        option_number = 11  # Uri-Path option number
        for segment in path_segments:
            seg_bytes = segment.encode('utf-8')
            seg_len = len(seg_bytes)

            # Option header: delta(4 bits) | length(4 bits)
            option_header = (option_number << 4) | seg_len
            packet += bytes([option_header]) + seg_bytes

            option_number = 0  # Subsequent same options have delta 0

    # Add payload marker and payload
    if payload:
        packet += b'\xFF'  # Payload marker
        if isinstance(payload, str):
            payload = payload.encode('utf-8')
        packet += payload

    return packet

def parse_coap_response(data):
    """
    Parse CoAP response packet

    Returns:
        dict with: type, code, message_id, token, payload
    """
    if len(data) < 4:
        return None

    # Parse header
    byte0 = data[0]
    version = (byte0 >> 6) & 0x03
    msg_type = (byte0 >> 4) & 0x03
    tkl = byte0 & 0x0F

    code = data[1]
    message_id = struct.unpack('!H', data[2:4])[0]

    # Parse token
    pos = 4
    token = data[pos:pos+tkl]
    pos += tkl

    # Skip options (simplified - just find payload marker)
    payload = b''
    while pos < len(data):
        if data[pos] == 0xFF:  # Payload marker
            payload = data[pos+1:]
            break
        else:
            # Skip option
            option_delta = (data[pos] >> 4) & 0x0F
            option_len = data[pos] & 0x0F
            pos += 1 + option_len

    return {
        'type': msg_type,
        'code': code,
        'message_id': message_id,
        'token': token,
        'payload': payload
    }

Langkah 5: CoAP GET Request

def coap_get(sock, server_addr, uri_path):
    """
    Send CoAP GET request

    Args:
        sock: UDP socket
        server_addr: (host, port) tuple
        uri_path: Resource path (e.g., "test")

    Returns:
        Response payload or None
    """
    try:
        print(f"\n[CoAP] Sending GET request")
        print(f"[CoAP] URI: coap://{COAP_SERVER}/{uri_path}")

        # Build GET request (Confirmable)
        token = bytes([urandom.getrandbits(8) for _ in range(4)])
        packet = build_coap_message(
            msg_type=COAP_TYPE_CON,
            code=COAP_GET,
            token=token,
            uri_path=uri_path
        )

        print(f"[CoAP] Packet size: {len(packet)} bytes")

        # Send request
        sock.sendto(packet, server_addr)
        print("[CoAP] ✓ Request sent")

        # Wait for response
        sock.settimeout(5)
        data, addr = sock.recvfrom(1024)

        print(f"[CoAP] ✓ Response received ({len(data)} bytes)")

        # Parse response
        response = parse_coap_response(data)

        if response:
            code_class = response['code'] >> 5
            code_detail = response['code'] & 0x1F
            print(f"[CoAP] Response Code: {code_class}.{code_detail:02d}")

            if response['payload']:
                payload_str = response['payload'].decode('utf-8', errors='ignore')
                print(f"[CoAP] Payload: {payload_str}")
                return payload_str

        return None

    except Exception as e:
        print(f"[CoAP] ✗ GET error: {e}")
        return None

Langkah 6: CoAP POST Request

def coap_post(sock, server_addr, uri_path, payload):
    """
    Send CoAP POST request

    Args:
        sock: UDP socket
        server_addr: (host, port) tuple
        uri_path: Resource path
        payload: Data to send (string or bytes)

    Returns:
        bool: True if success
    """
    try:
        print(f"\n[CoAP] Sending POST request")
        print(f"[CoAP] URI: coap://{COAP_SERVER}/{uri_path}")
        print(f"[CoAP] Payload: {payload}")

        # Build POST request (Confirmable)
        token = bytes([urandom.getrandbits(8) for _ in range(4)])
        packet = build_coap_message(
            msg_type=COAP_TYPE_CON,
            code=COAP_POST,
            token=token,
            uri_path=uri_path,
            payload=payload
        )

        print(f"[CoAP] Packet size: {len(packet)} bytes")

        # Send request
        sock.sendto(packet, server_addr)
        print("[CoAP] ✓ Request sent")

        # Wait for ACK
        sock.settimeout(5)
        data, addr = sock.recvfrom(1024)

        print(f"[CoAP] ✓ ACK received ({len(data)} bytes)")

        # Parse response
        response = parse_coap_response(data)

        if response:
            code_class = response['code'] >> 5
            code_detail = response['code'] & 0x1F
            print(f"[CoAP] Response Code: {code_class}.{code_detail:02d}")

            # Check for success (2.xx codes)
            if code_class == 2:
                print("[CoAP] ✓ POST successful!")
                return True
            else:
                print(f"[CoAP] ✗ POST failed with code {code_class}.{code_detail:02d}")
                return False

        return False

    except Exception as e:
        print(f"[CoAP] ✗ POST error: {e}")
        return False

Langkah 7: Sensor Function

# ============================================
# SENSOR FUNCTIONS
# ============================================

def read_sensor():
    """Read DHT22 sensor"""
    try:
        dht_sensor.measure()
        time.sleep(0.5)

        temp = dht_sensor.temperature()
        hum = dht_sensor.humidity()

        if temp is None or hum is None:
            return None

        data = {
            'temperature': round(temp, 1),
            'humidity': round(hum, 1)
        }

        print(f"[SENSOR] ✓ Temp={data['temperature']}°C, Hum={data['humidity']}%")
        return data

    except Exception as e:
        print(f"[SENSOR] ✗ Error: {e}")
        return None

Langkah 8: Main Program

# ============================================
# MAIN PROGRAM
# ============================================

def main():
    """Main CoAP client program"""

    print("\n" + "="*50)
    print("Starting CoAP IoT Application")
    print("="*50)

    # Connect WiFi
    if not connect_wifi():
        print("[ERROR] WiFi failed")
        return

    time.sleep(2)

    # Resolve server address
    print(f"\n[CoAP] Resolving {COAP_SERVER}...")
    try:
        addr_info = socket.getaddrinfo(COAP_SERVER, COAP_PORT)[0]
        server_addr = addr_info[-1]
        print(f"[CoAP] ✓ Resolved to: {server_addr[0]}:{server_addr[1]}")
    except Exception as e:
        print(f"[CoAP] ✗ DNS resolution failed: {e}")
        return

    # Create UDP socket
    print("[CoAP] Creating UDP socket...")
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    print("[CoAP] ✓ Socket created")

    print("\n" + "="*50)
    print("CoAP Client Ready!")
    print("="*50)
    print("[INFO] Testing GET and POST operations")
    print("[INFO] Sending sensor data every 10 seconds")
    print("[INFO] Press STOP to exit")
    print("="*50 + "\n")

    # Test GET first
    print("\n[TEST] Testing CoAP GET...")
    coap_get(sock, server_addr, "test")

    time.sleep(2)

    last_send_time = time.time()
    msg_count = 0

    # Main loop
    while True:
        try:
            current_time = time.time()

            # Send sensor data via POST
            if (current_time - last_send_time) >= SEND_INTERVAL:
                msg_count += 1
                print(f"\n{'='*50}")
                print(f"Sending Message #{msg_count}")
                print(f"{'='*50}")

                # Blink LED
                led.value(1)

                # Read sensor
                sensor_data = read_sensor()

                if sensor_data:
                    # Format payload (simple text format)
                    payload = f"temp={sensor_data['temperature']}&hum={sensor_data['humidity']}"

                    # Send via CoAP POST
                    coap_post(sock, server_addr, "sensor", payload)

                led.value(0)
                last_send_time = current_time

            # Small delay
            time.sleep(0.5)

        except KeyboardInterrupt:
            print("\n[INFO] Interrupted by user")
            break

        except Exception as e:
            print(f"[ERROR] Loop error: {e}")
            time.sleep(5)

    # Cleanup
    print("\n[CoAP] Closing socket...")
    sock.close()
    led.value(0)
    print("[INFO] Program ended")

# Entry point
if __name__ == "__main__":
    main()

Langkah 9: Test CoAP

  1. Copy semua code CoAP ke main.py
  2. Klik "Play" di Wokwi
  3. Akan test GET request dulu
  4. Kemudian loop POST sensor data setiap 10 detik

Expected Output:

==================================================
CoAP Protocol Implementation
Platform: Wokwi ESP32
==================================================
[CONFIG] Configuration loaded
[CONFIG] CoAP Server: coap://coap.me:5683
[HARDWARE] LED initialized on GPIO 2
[HARDWARE] DHT22 sensor initialized on GPIO 15

[WIFI] Connecting...
[WIFI] ✓ Connected!
[WIFI] IP: 192.168.1.100

[CoAP] Resolving coap.me...
[CoAP] ✓ Resolved to: 134.102.218.18:5683
[CoAP] Creating UDP socket...
[CoAP] ✓ Socket created

==================================================
CoAP Client Ready!
==================================================
[INFO] Testing GET and POST operations
[INFO] Sending sensor data every 10 seconds
[INFO] Press STOP to exit
==================================================

[TEST] Testing CoAP GET...

[CoAP] Sending GET request
[CoAP] URI: coap://coap.me/test
[CoAP] Packet size: 11 bytes
[CoAP] ✓ Request sent
[CoAP] ✓ Response received (15 bytes)
[CoAP] Response Code: 2.05
[CoAP] Payload: welcome to the ETSI plugtest!

==================================================
Sending Message #1
==================================================
[SENSOR] ✓ Temp=24.5°C, Hum=55.3%

[CoAP] Sending POST request
[CoAP] URI: coap://coap.me/sensor
[CoAP] Payload: temp=24.5&hum=55.3
[CoAP] Packet size: 38 bytes
[CoAP] ✓ Request sent
[CoAP] ✓ ACK received (12 bytes)
[CoAP] Response Code: 2.01
[CoAP] ✓ POST successful!