Эх сурвалжийг харах

Adding Control Connecction Via Web

Aldino Arya Pramana 7 сар өмнө
parent
commit
efbb01323b

+ 6 - 0
README.md

@@ -40,3 +40,9 @@ This project focuses on the development of a **Bluetooth Low Energy (LE) Audio T
 ---
 
 ## Project Structure
+/project
+├─ /src # Source code for simulation
+├─ /docs # Documentation and diagrams
+├─ /test # Program for function Test
+├─ README.md
+└─ LICENSE

+ 60 - 44
src/main/main.ino

@@ -2,8 +2,10 @@
 #include <BLEServer.h>
 #include <BLEUtils.h>
 #include <BLE2902.h>
-#include <vector>
 #include <ArduinoJson.h>
+#include <map>
+
+std::map<int, int> connIdToIndex;  // mapping connId → index di array Devices
 
 BLEServer *pServer = NULL;
 BLECharacteristic *pCharacteristic = NULL;
@@ -11,59 +13,68 @@ int connectedClients = 0;
 bool deviceConnected = false;
 uint32_t value = 0;
 unsigned int lastTime = 0;
-String json;
-
-struct ClientInfo {
-  String mac;
-  int connectedID;
-  String peerID;
-  uint16_t connInterval;
-  uint16_t latency;
-  uint16_t timeout;
-};
-
-JsonDocument doc;
 
-std::vector<ClientInfo> clients;
+StaticJsonDocument<512> doc;                           // dokumen utama
+JsonArray devices = doc.createNestedArray("Devices");  // array untuk semua device
 
 #define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
 #define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"
 
 class MyServerCallbacks : public BLEServerCallbacks {
   void onConnect(BLEServer *pServer, ble_gap_conn_desc *desc) override {
-    ClientInfo ci;
-    ci.mac = BLEAddress(desc->peer_ota_addr).toString().c_str();
-    ci.connectedID = pServer->getConnId();
-    ci.peerID = BLEAddress(desc->peer_id_addr).toString().c_str();
-    ci.connInterval = desc->conn_itvl;
-    ci.latency = desc->conn_latency;
-    ci.timeout = desc->supervision_timeout;
-
-    doc["mac"] = BLEAddress(desc->peer_ota_addr).toString().c_str();
-    doc["connectedID"] = pServer->getConnId();
-    doc["peerID"] = BLEAddress(desc->peer_id_addr).toString().c_str();
-    doc["connInterval"] = desc->conn_itvl;
-    doc["latency"] = desc->conn_latency;
-    doc["timeout"] = desc->supervision_timeout;
+    connectedClients++;
+
+    // Buat object untuk device baru
+    JsonObject device = devices.createNestedObject();
+    device["mac"] = BLEAddress(desc->peer_ota_addr).toString().c_str();
+    device["connectedID"] = pServer->getConnId();
+    device["connInterval"] = desc->conn_itvl;
+    device["latency"] = desc->conn_latency;
+    device["timeout"] = desc->supervision_timeout;
+
+    // Simpan mapping connId → index device
+    connIdToIndex[pServer->getConnId()] = devices.size() - 1;
+
+    // Update status umum
+    doc["Status"] = "NEW_CONNECT";
+    doc["connectedClients"] = connectedClients;
 
-    
     serializeJson(doc, Serial);
     Serial.println();
-
-    clients.push_back(ci);
-    connectedClients++;
-    BLEDevice::startAdvertising();
   }
 
   void onDisconnect(BLEServer *pServer) override {
-    Serial.println("=== Device Disconnected ===");
-    if (!clients.empty()) clients.pop_back();
     connectedClients--;
+    doc["Status"] = "DEVICE_DISCONNECTED";
+    doc["connectedClients"] = connectedClients;
+
+    int connId = pServer->getConnId();
+    if (connIdToIndex.count(connId)) {
+      int index = connIdToIndex[connId];
+      // Hapus device dari array (buat ulang array)
+      JsonArray newDevices = doc.createNestedArray("Devices");
+      for (int i = 0; i < devices.size(); i++) {
+        if (i == index) continue;  // skip device yang disconnect
+        JsonObject oldDevice = devices[i];
+        JsonObject newDevice = newDevices.createNestedObject();
+        newDevice["mac"] = oldDevice["mac"];
+        newDevice["connectedID"] = oldDevice["connectedID"];
+        newDevice["connInterval"] = oldDevice["connInterval"];
+        newDevice["latency"] = oldDevice["latency"];
+        newDevice["timeout"] = oldDevice["timeout"];
+      }
+      devices = newDevices;         // update devices
+      connIdToIndex.erase(connId);  // hapus mapping
+    }
+
+    serializeJson(doc, Serial);
+    Serial.println();
   }
 };
 
 void setup() {
   Serial.begin(115200);
+  pinMode(LED_BUILTIN, OUTPUT);
 
   BLEDevice::init("ESP32");
 
@@ -85,34 +96,39 @@ void setup() {
   pAdvertising->setMinPreferred(0x0);
   BLEDevice::startAdvertising();
 
-  Serial.println("Waiting for client connections to notify...");
-  Serial.println("Type 'disconnect <connId>' to disconnect a client.");
+  // Serial.println("Waiting for client connections to notify...");
+  // Serial.println("Type 'disconnect <connId>' to disconnect a client.");
 }
 
 void loop() {
-  
+
   if (connectedClients > 0) {
     pCharacteristic->setValue((uint8_t *)&value, 4);
     pCharacteristic->notify();
     value++;
     delay(100);
+    
+
   }
 
   if (Serial.available()) {
     String cmd = Serial.readStringUntil('\n');
     cmd.trim();
-    if (cmd.startsWith("disconnect ")) {
-      int id = cmd.substring(11).toInt();
-      Serial.print("Trying to disconnect connId: ");
-      Serial.println(id);
-      pServer->disconnect(id);
+    digitalWrite(LED_BUILTIN, HIGH); 
+    if (cmd.startsWith("DISCONNECT:")) {
+      
+      int connId = cmd.substring(11).toInt();
+      // Serial.println("Arduino will disconnect connID: " + String(connId));
+      // panggil fungsi BLE untuk disconnect
+      pServer->disconnect(connId);  // sesuai API ESP32 BLE
     }
   }
+  digitalWrite(LED_BUILTIN, LOW); 
 
   if (connectedClients == 0 && deviceConnected) {
     delay(500);
     pServer->startAdvertising();
-    Serial.println("No clients connected, restarting advertising");
+    // Serial.println("No clients connected, restarting advertising");
     deviceConnected = false;
   }
 

+ 207 - 43
src/web_side/index.html

@@ -1,49 +1,213 @@
 <!DOCTYPE html>
 <html lang="en">
+
 <head>
-<meta charset="UTF-8">
-<title>BLE Clients</title>
-<style>
-  .card { border:1px solid #ccc; padding:10px; margin:5px; border-radius:5px; }
-  button { margin-top:5px; }
-</style>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>BLE Device Monitor + Serial</title>
+  <style>
+    body {
+      font-family: Arial;
+      padding: 20px;
+      background: #f5f5f5;
+    }
+
+    h1 {
+      text-align: center;
+    }
+
+    #controls {
+      text-align: center;
+      margin-bottom: 20px;
+    }
+
+    #deviceContainer {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 20px;
+      justify-content: center;
+    }
+
+    .deviceCard {
+      background: white;
+      border-radius: 10px;
+      box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+      padding: 15px;
+      width: 250px;
+      position: relative;
+    }
+
+    .deviceCard h3 {
+      margin-top: 0;
+      font-size: 18px;
+    }
+
+    .deviceCard button {
+      position: absolute;
+      top: 10px;
+      right: 10px;
+      background: red;
+      color: white;
+      border: none;
+      padding: 5px 10px;
+      border-radius: 5px;
+      cursor: pointer;
+    }
+
+    .deviceData p {
+      margin: 5px 0;
+    }
+
+    button {
+      padding: 10px 15px;
+      margin: 5px;
+      cursor: pointer;
+    }
+  </style>
 </head>
+
 <body>
-<h2>Connected BLE Devices</h2>
-<div id="devices"></div>
-
-<script>
-const ws = new WebSocket(`http://localhost:3000`);
-
-ws.onmessage = (event) => {
-  let clients;
-  try {
-    clients = JSON.parse(event.data);
-  } catch(e) {
-    console.error("Failed to parse JSON", e);
-    return;
-  }
-
-  const container = document.getElementById("devices");
-  container.innerHTML = "";
-
-  clients.forEach(client => {
-    const card = document.createElement("div");
-    card.className = "card";
-    card.innerHTML = `
-      <p>MAC: ${client.mac}</p>
-      <p>Interval: ${client.connInterval}</p>
-      <p>Latency: ${client.latency}</p>
-      <p>Timeout: ${client.timeout}</p>
-      <button onclick='deleteClient("${client.connectedID}")'>Delete</button>
-    `;
-    container.appendChild(card);
-  });
-};
-
-function deleteClient(id) {
-  ws.send(JSON.stringify({ action: "delete", id }));
-}
-</script>
+  <h1>BLE Device Monitor</h1>
+
+  <div id="controls">
+    <button id="selectPortBtn">Select Arduino Port</button>
+    <button id="openPortBtn" disabled>Open Port</button>
+    <button id="micBtn">🎤 Start Mic</button>
+  </div>
+
+  <div id="deviceContainer"></div>
+
+  <script>
+    let port;
+    let reader;
+    let keepReading = false;
+
+    const deviceContainer = document.getElementById('deviceContainer');
+    const selectPortBtn = document.getElementById('selectPortBtn');
+    const openPortBtn = document.getElementById('openPortBtn');
+    const micBtn = document.getElementById('micBtn');
+
+    const ws = new WebSocket("ws://localhost:8080");
+
+    ws.onmessage = (event) => {
+      const data = JSON.parse(event.data);
+
+      if (data.Status === 'NEW_CONNECT') {
+        data.Devices.forEach(d => addDevice(d));
+      }
+      if (data.Status === 'DEVICE_DISCONNECTED') {
+        data.Devices.forEach(d => removeDevice(d.connectedID));
+      }
+    };
+
+    // Pilih port Arduino
+    selectPortBtn.onclick = async () => {
+      try {
+        port = await navigator.serial.requestPort();
+        console.log('Port selected:', port);
+        openPortBtn.disabled = false;
+      } catch (err) {
+        console.error('Port selection cancelled', err);
+      }
+    };
+
+    // Buka port dan mulai membaca
+    openPortBtn.onclick = async () => {
+      if (!port) return;
+      await port.open({ baudRate: 115200 });
+      console.log('Port opened');
+
+      const textDecoder = new TextDecoderStream();
+      const readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
+      reader = textDecoder.readable.getReader();
+      keepReading = true;
+
+      readLoop();
+    };
+
+    async function readLoop() {
+      while (keepReading) {
+        try {
+          const { value, done } = await reader.read();
+          if (done) break;
+          if (value) parseSerialData(value.trim());
+        } catch (err) {
+          console.error('Error reading:', err);
+          break;
+        }
+      }
+    }
+
+    // Parse JSON dari Arduino
+    function parseSerialData(line) {
+      try {
+        const data = JSON.parse(line);
+        if (data.Status === 'NEW_CONNECT') {
+          data.Devices.forEach(d => addDevice(d));
+        }
+        if (data.Status === 'DEVICE_DISCONNECTED') {
+          // bisa hapus semua device yang disconnect
+          data.Devices.forEach(d => removeDevice(d.connectedID));
+        }
+      } catch (err) {
+        console.warn('Invalid JSON:', line);
+      }
+    }
+
+    // Tambah device card
+    function addDevice(device) {
+      if (document.getElementById(`device-${device.connectedID}`)) return;
+
+      const card = document.createElement('div');
+      card.className = 'deviceCard';
+      card.id = `device-${device.connectedID}`;
+
+      const title = document.createElement('h3');
+      title.textContent = `Device ${device.connectedID}`;
+      card.appendChild(title);
+
+      const btn = document.createElement('button');
+      btn.textContent = 'Disconnect';
+      btn.onclick = () => {
+        // kirim command ke Node.js via WebSocket
+        ws.send(JSON.stringify({
+          type: "DISCONNECT",
+          connID: device.connectedID
+        }));
+        console.log(`Request disconnect ${device.connectedID}`);
+        removeDevice(device.connectedID); // hapus card sementara
+      };
+      card.appendChild(btn);
+
+      const dataDiv = document.createElement('div');
+      dataDiv.className = 'deviceData';
+      dataDiv.innerHTML = `
+    <p><b>MAC:</b> ${device.mac}</p>
+    <p><b>ConnID:</b> ${device.connectedID}</p>
+    <p><b>Interval:</b> ${device.connInterval}</p>
+    <p><b>Latency:</b> ${device.latency}</p>
+    <p><b>Timeout:</b> ${device.timeout}</p>
+  `;
+      card.appendChild(dataDiv);
+
+      deviceContainer.appendChild(card);
+    }
+
+    function removeDevice(connID) {
+      const card = document.getElementById(`device-${connID}`);
+      if (card) card.remove();
+    }
+
+    // Mic input placeholder
+    let micStream;
+    micBtn.onclick = async () => {
+      if (!micStream) {
+        micStream = await navigator.mediaDevices.getUserMedia({ audio: true });
+        console.log('Mic started', micStream);
+        micBtn.textContent = '🎤 Mic Active';
+      }
+    };
+  </script>
 </body>
-</html>
+
+</html>

+ 19 - 4
src/web_side/read_serial.js

@@ -4,7 +4,7 @@ import { SerialPort } from "serialport";
 import { ReadlineParser } from "@serialport/parser-readline";
 
 const app = express();
-const portHTTP = 3000;
+const portHTTP = 8080;
 
 // === SERVE FRONTEND ===
 app.use(express.static("public"));
@@ -38,10 +38,25 @@ wss.on("connection", (ws) => {
   ws.on("message", (msg) => {
     const command = JSON.parse(msg);
     console.log("📤 Dari Browser:", command);
+    // console.log(command.type);
 
-    if (command.action === "disconnect") {
-      // kirim ke ESP32 lewat serial
-      serial.write(`DISCONNECT:${command.connId}\n`);
+
+    if (command.type === "DISCONNECT") {
+      const data = `DISCONNECT:${command.connID}\n`;
+      console.log("DISCONNECTING...", data);
+
+      // Kirim tiap karakter satu per satu
+      for (let i = 0; i < data.length; i++) {
+        const char = data[i];
+        serial.write(Buffer.from(char, 'ascii'), (err) => {
+          if (err) {
+            console.error(`Error writing char '${char}':`, err.message);
+          } else {
+            // console.log(`Char '${char}' sent`);
+          }
+        });
+      }
     }
+
   });
 });