วัดไฟไร้สายทะลุ 30A! สร้าง Power Meter อัจฉริยะด้วย ESP32-C6 (แสดงผลผ่าน Web)

Wireless High Amp Power Meter With ESP32C6

เบื่อไหมที่ต้องคอยเอา Multimeter มาจิ้มวัดไฟตลอดเวลา?

เคยอยากเช็คสถานะการจ่ายไฟของแบตเตอรี่แพ็ค หรือพาวเวอร์ซัพพลายจากระยะไกล โดยที่ไม่ต้องมานั่งเฝ้าหน้าเครื่องมัลติมิเตอร์ไหมครับ? โปรเจกต์นี้เกิดมาเพื่อแก้ปัญหานั้นเลย! บทความนี้จะพาคุณมาสร้างเครื่องวัดพลังงานไฟฟ้าแบบไร้สาย (Wi-Fi Power Meter) ที่สามารถวัดกระแสไฟได้สูงถึง 30 แอมป์ (30A) พร้อมหน้าเว็บ Dashboard ดูค่าโวลต์ กระแส และวัตต์แบบสดๆ ผ่านมือถือหรือเบราว์เซอร์ได้เลย

โปรเจกต์นี้ประกอบไปด้วยการออกแบบแผ่นวงจร (PCB) ขึ้นมาเอง กล่อง 3D Print สวยๆ และไมโครคอนโทรลเลอร์ตระกูล ESP32 ขนาดจิ๋ว ผลลัพธ์ที่ได้คืออุปกรณ์ที่ดูสะอาดตา เป็นมืออาชีพ และนำไปใช้งานได้จริงในชีวิตประจำวันครับ

Power Meter Setup
View more (ดูรูปอุปกรณ์เพิ่มเติม)
Power Meter Live Dashboard

เครื่องนี้ทำอะไรได้บ้าง? (What Can It Do?)

  • วัดค่าแรงดัน (Voltage), กระแสไฟฟ้า (Current) และกำลังไฟ (Wattage) ได้แบบเรียลไทม์
  • รองรับการวัดกระแสไฟสูงสุดถึง 30A
  • เชื่อมต่อ Wi-Fi สามารถมอนิเตอร์จากเบราว์เซอร์ไหนก็ได้ในวงเน็ตเวิร์กบ้านคุณ
  • มี Live Dashboard ผ่าน IP Address ไม่ต้องโหลดแอปพลิเคชันเพิ่ม
  • เหมาะมากสำหรับใช้เช็คแบตเตอรี่แพ็ค พาวเวอร์ซัพพลาย หรือแม้แต่งานระบบไฟรถยนต์

อุปกรณ์ที่ต้องใช้ (Supplies)

อุปกรณ์หลักๆ ที่เราจะใช้เป็นหัวใจของโปรเจกต์นี้มีดังนี้ครับ:

  • บอร์ดไมโครคอนโทรลเลอร์ Seeed Studio XIAO ESP32-C6
  • ไอซีวัดพลังงาน ACS37800
  • คอนเนคเตอร์ XT60PW30 Male Connector (2 ชิ้น)

Step 1: ทำความเข้าใจวงจร (Understanding the Circuit)

หัวใจหลักของงานนี้คือชิป ACS37800 ซึ่งเป็นไอซีวัดพลังงานจากค่าย Allegro มันสามารถวัดได้ทั้งกระแสไฟ (ผ่าน Shunt ด้านใน) และแรงดันไฟฟ้าของสายไฟหลัก และส่งข้อมูลผ่านโปรโตคอล I2C ข้อดีคือมันทำให้การเดินสายง่ายมาก! ใช้สายแค่ 4 เส้นต่อเข้ากับบอร์ด ESP32-C6 (VCC, GND, SDA, SCL)

เหตุผลที่เลือกใช้บอร์ด XIAO ESP32-C6 ก็เพราะมันมีขนาดเล็กจิ๋ว มี Wi-Fi ในตัว และมีขา GPIO เพียงพอสำหรับงานนี้ มันจะคอยอ่านข้อมูลพลังงานจากชิป ACS37800 ผ่าน I2C แล้วทำตัวเป็น Web Server เบาๆ เพื่อสตรีมข้อมูลสดๆ ไปยังเบราว์เซอร์นั่นเอง

Schematic Concept

Step 2: การออกแบบและสั่งทำบอร์ด PCB

ผมออกแบบแผ่นปริ้นท์ (PCB) ผ่านโปรแกรม EasyEDA ซึ่งเชื่อมต่อกับการสั่งผลิตได้ง่ายมาก ดีไซน์ถูกจัดวางให้กะทัดรัดที่สุดเพื่อให้ยัดลงกล่องได้พอดีเป๊ะ

EasyEDA PCB Design
View more (ดูรูปการจัดวาง PCB และแผ่นปริ้นท์จริง)

เมื่อดีไซน์เสร็จแล้ว ก็ทำการส่งออกไฟล์ Gerber เพื่อนำไปสั่งผลิต ผมเลือกสีแผ่น PCB เป็นสีขาว เพื่อให้เข้ากับสไตล์ของกล่องเคสครับ

PCB 3D Preview Manufactured PCB

Step 3: การออกแบบและ 3D Print เคส

ส่วนของกล่องเคส ผมออกแบบในโปรแกรม Fusion 360 ให้สวมเข้ากับบอร์ด PCB ได้พอดีแบบไม่ต้องขยับเยอะ มีการเจาะช่องเว้าสำหรับพอร์ต XT60, พอร์ต USB-C และเสาอากาศ Wi-Fi

ทริคสำหรับคนไทย: ในบทความต้นฉบับจะสั่งปริ้นท์เรซิ่นจากเมืองนอก แต่เราสามารถหาร้านรับทำ 3D Print ระบบ Resin ในประเทศไทย ได้ไม่ยากครับ แนะนำให้สั่งปริ้นท์ด้วย "เรซิ่นใส (Clear Resin)" มันจะให้ผิวสัมผัสที่ดูขุ่นๆ ฝ้าๆ (Frosted) ดูพรีเมียมและยังมองทะลุเห็นแผงวงจรด้านในได้นิดๆ ซึ่งดูโปรและเนียนตากว่าการใช้เครื่องปริ้นท์พลาสติกเส้น (FDM) ทั่วไปมากครับ

Enclosure 3D Design
View more (ดูรูปกล่อง 3D Print จริง)
3D Printed Enclosure

Step 4: การประกอบและบัดกรี PCB

เมื่อได้แผ่น PCB และอุปกรณ์ครบแล้ว ก็ถึงเวลาจับหัวแร้ง! ผมใช้ตะกั่วบัดกรีอุปกรณ์แบบ SMD ด้วยมือทั้งหมดเลยครับ ถ้าคุณมีทักษะการบัดกรีก็ลุยเองได้เลย แต่ถ้าไม่มั่นใจ การสั่งทำบอร์ดแบบประกอบสำเร็จ (PCBA) ก็เป็นทางเลือกที่ดีครับ

Soldering the PCB

ที่ด้านหน้าของบอร์ด PCB เราจะวางตัว Xiao และตัวปรับแรงดันไฟ (Voltage regulator) ส่วนด้านหลังเราจะวางเซ็นเซอร์วัดกระแส (ACS37800) และอย่าลืม พอกตะกั่วหนาๆ ไว้ที่แผ่นทองแดงขั้วบวก (Positive contact) ด้วยนะครับ เพราะจุดนี้จะมีกระแสไฟไหลผ่านสูงมาก อาจจะเอาลวดทองแดงมาบัดกรีทาบเข้าไปด้วยเพื่อเพิ่มทางเดินกระแสไฟให้ดีขึ้นครับ

Step 5: การประกอบลงกล่อง

วางบอร์ด PCB ลงไปที่แผงหลังของกล่องเคส เล็งตำแหน่งให้ตรง จากนั้นใช้น็อต M3 x 10mm จำนวน 4 ตัว ขันยึดทุกอย่างให้แน่นหนา แค่นี้ฮาร์ดแวร์ก็พร้อมแล้ว!

PCB in Enclosure Setup 1
View more (ดูรูปขั้นตอนการประกอบลงกล่อง)
PCB in Enclosure Setup 2 Assembled Power Meter

Step 6: การแฟลชเฟิร์มแวร์ (Flashing the Firmware)

เสียบสาย USB เข้ากับบอร์ด Xiao แล้วอัปโหลดโค้ดด้านล่างนี้ได้เลยครับ แต่อย่าลืมแก้ไขข้อมูล WIFI_SSID และ WIFI_PASSWORD ให้เป็นของเน็ตเวิร์กที่บ้านคุณด้วยนะ และต้องติดตั้งไลบรารี SparkFun_ACS37800_Arduino_Library ให้เรียบร้อยก่อนกดอัปโหลดด้วยครับ

Flashing Firmware
View more (ดู Full Code และกดคัดลอก / Copy Code)
/*
 ============================================================
  DC POWER MONITOR — XIAO ESP32-C6 + ACS37800
  Max rating: 36V DC / 30A
  Web dashboard served over home WiFi
 ============================================================
*/

#include <Arduino.h>
#include "SparkFun_ACS37800_Arduino_Library.h"
#include <Wire.h>
#include <WiFi.h>
#include <WebServer.h>
#include <ArduinoJson.h>

// ── WiFi credentials ─────────────────────────────────────
const char* WIFI_SSID     = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";

// ── Hardware limits (ACS37800 30A variant, 36V DC max) ───
constexpr float MAX_VOLTAGE = 36.0f;
constexpr float MAX_CURRENT = 30.0f;

// ── Calibration ───────────────────────────────────────────
// CURRENT_SCALE: calibrated against a 2.000 A reference load (reads 1.916 A)
//   → new scale = old_scale × (true / read) = 2.882 × (2.000 / 1.916) = 3.008
//   To recalibrate: CURRENT_SCALE = true_amps / sensor_raw_amps_before_scaling
constexpr float CURRENT_SCALE  = 3.008f;

// CURRENT_OFFSET: zero-load current offset (amps), captured at startup.
// Auto-zeroed in setup() — keep load disconnected at boot.
float gAmpsOffset = 0.0f;

// VOLTAGE_SCALE: calibrated from 3-point measurement
//   10V→10.11, 20V→20.27, 30V→30.43  (pure gain error, not offset)
//   scale = true / read = 30.00 / 30.43 = 0.9859
//   Cross-check: 10.11*0.9859=9.97V  20.27*0.9859=19.99V  checkmark
//   To recalibrate: VOLTAGE_SCALE = true_volts / displayed_volts
constexpr float VOLTAGE_SCALE  = 0.9859f;

// ── Objects ───────────────────────────────────────────────
ACS37800   sensor;
WebServer  server(80);

// ── Live readings (updated in loop) ──────────────────────
volatile float gVolts = 0.0f;
volatile float gAmps  = 0.0f;
volatile float gWatts = 0.0f;

// ─────────────────────────────────────────────────────────
//  HTML dashboard (stored in flash with PROGMEM)
// ─────────────────────────────────────────────────────────
const char INDEX_HTML[] PROGMEM = R"rawhtml(
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Power Monitor</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Barlow+Condensed:wght@300;600;800&display=swap" rel="stylesheet">
<style>
  :root {
    --bg:        #0a0c0f;
    --panel:     #0f1318;
    --border:    #1e2530;
    --accent:    #00e5ff;
    --accent2:   #ff6b35;
    --warn:      #ffcc00;
    --danger:    #ff2244;
    --text:      #c8d8e8;
    --dim:       #4a5a6a;
    --mono:      'Share Tech Mono', monospace;
    --sans:      'Barlow Condensed', sans-serif;
  }

  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

  html, body {
    height: 100%;
    background: var(--bg);
    color: var(--text);
    font-family: var(--sans);
    overflow-x: hidden;
  }

  /* subtle scanline texture */
  body::before {
    content: '';
    position: fixed; inset: 0;
    background: repeating-linear-gradient(
      0deg,
      transparent,
      transparent 2px,
      rgba(0,229,255,0.015) 2px,
      rgba(0,229,255,0.015) 4px
    );
    pointer-events: none;
    z-index: 999;
  }

  /* ── top bar ── */
  header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 14px 24px;
    border-bottom: 1px solid var(--border);
    background: var(--panel);
  }
  .logo {
    font-family: var(--sans);
    font-weight: 800;
    font-size: 1.15rem;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--accent);
  }
  .logo span { color: var(--text); font-weight: 300; }
  .status-dot {
    width: 9px; height: 9px;
    border-radius: 50%;
    background: var(--accent);
    box-shadow: 0 0 8px var(--accent);
    animation: pulse 2s ease-in-out infinite;
  }
  @keyframes pulse {
    0%,100% { opacity: 1; }
    50%      { opacity: 0.35; }
  }

  /* ── main grid ── */
  main {
    padding: 28px 20px 40px;
    display: flex;
    flex-direction: column;
    gap: 18px;
    max-width: 540px;
    margin: 0 auto;
  }

  /* ── metric card ── */
  .card {
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 4px;
    padding: 20px 24px 18px;
    position: relative;
    overflow: hidden;
    transition: border-color 0.3s;
  }
  .card::before {
    content: '';
    position: absolute;
    top: 0; left: 0;
    width: 3px; height: 100%;
    background: var(--card-accent, var(--accent));
  }
  .card-label {
    font-family: var(--sans);
    font-weight: 600;
    font-size: 0.7rem;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    color: var(--dim);
    margin-bottom: 6px;
  }
  .card-value {
    font-family: var(--mono);
    font-size: 3.4rem;
    line-height: 1;
    color: var(--card-accent, var(--accent));
    transition: color 0.3s;
    letter-spacing: -0.02em;
  }
  .card-unit {
    font-family: var(--sans);
    font-weight: 300;
    font-size: 1rem;
    color: var(--dim);
    margin-left: 6px;
    vertical-align: baseline;
    letter-spacing: 0.08em;
  }

  /* accent colours per card */
  .card-v  { --card-accent: #00e5ff; }
  .card-a  { --card-accent: #ff6b35; }
  .card-w  { --card-accent: #b4ff6e; }

  /* ── bar gauge ── */
  .gauge-wrap {
    margin-top: 14px;
    height: 4px;
    background: var(--border);
    border-radius: 2px;
    overflow: hidden;
  }
  .gauge-fill {
    height: 100%;
    border-radius: 2px;
    background: var(--card-accent, var(--accent));
    transition: width 0.4s ease;
    box-shadow: 0 0 6px var(--card-accent, var(--accent));
  }

  /* ── warn state ── */
  .card.warn { border-color: var(--warn); }
  .card.warn .card-value { color: var(--warn) !important; }
  .card.warn .gauge-fill  { background: var(--warn) !important; box-shadow: 0 0 6px var(--warn) !important; }

  .card.danger { border-color: var(--danger); animation: blink-border 0.6s step-end infinite; }
  .card.danger .card-value { color: var(--danger) !important; }
  .card.danger .gauge-fill  { background: var(--danger) !important; box-shadow: 0 0 6px var(--danger) !important; }

  @keyframes blink-border {
    0%,100% { border-color: var(--danger); }
    50%      { border-color: transparent;  }
  }

  /* ── power factor strip ── */
  .pf-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 4px;
    padding: 14px 24px;
    font-family: var(--mono);
    font-size: 0.78rem;
    color: var(--dim);
    letter-spacing: 0.05em;
  }
  .pf-row strong { color: var(--text); font-family: var(--sans); font-weight: 600; font-size: 0.85rem; }

  /* ── timestamp ── */
  .ts {
    text-align: center;
    font-family: var(--mono);
    font-size: 0.65rem;
    color: var(--dim);
    letter-spacing: 0.08em;
  }

  @media (min-width: 500px) {
    .card-value { font-size: 4rem; }
  }
</style>
</head>
<body>

<header>
  <div class="logo">DC<span>meter</span></div>
  <div class="status-dot" id="dot"></div>
</header>

<main>
  <!-- Voltage -->
  <div class="card card-v" id="card-v">
    <div class="card-label">Voltage</div>
    <div class="card-value" id="val-v">--.--<span class="card-unit">V</span></div>
    <div class="gauge-wrap"><div class="gauge-fill" id="bar-v" style="width:0%"></div></div>
  </div>

  <!-- Current -->
  <div class="card card-a" id="card-a">
    <div class="card-label">Current</div>
    <div class="card-value" id="val-a">--.--<span class="card-unit">A</span></div>
    <div class="gauge-wrap"><div class="gauge-fill" id="bar-a" style="width:0%"></div></div>
  </div>

  <!-- Power -->
  <div class="card card-w" id="card-w">
    <div class="card-label">Power</div>
    <div class="card-value" id="val-w">---.--<span class="card-unit">W</span></div>
    <div class="gauge-wrap"><div class="gauge-fill" id="bar-w" style="width:0%"></div></div>
  </div>

  <!-- Quick stats row -->
  <div class="pf-row">
    <span>MAX&nbsp;36V&nbsp;/&nbsp;30A</span>
    <strong id="load-pct">LOAD — %</strong>
    <span id="ts-lbl">--:--:--</span>
  </div>

  <div class="ts" id="ts-full">waiting for data…</div>
</main>

<script>
  const MAX_V   = 36.0;
  const MAX_A   = 30.0;
  const MAX_W   = MAX_V * MAX_A;   // 1080 W theoretical max

  const elV     = document.getElementById('val-v');
  const elA     = document.getElementById('val-a');
  const elW     = document.getElementById('val-w');
  const barV    = document.getElementById('bar-v');
  const barA    = document.getElementById('bar-a');
  const barW    = document.getElementById('bar-w');
  const cardV   = document.getElementById('card-v');
  const cardA   = document.getElementById('card-a');
  const cardW   = document.getElementById('card-w');
  const dot     = document.getElementById('dot');
  const loadPct = document.getElementById('load-pct');
  const tsLbl   = document.getElementById('ts-lbl');
  const tsFull  = document.getElementById('ts-full');

  function fmt(v, dec) { return (v >= 0 ? v : 0).toFixed(dec); }

  function setCardState(card, pct, warnPct = 80, dangerPct = 95) {
    card.classList.remove('warn', 'danger');
    if      (pct >= dangerPct) card.classList.add('danger');
    else if (pct >= warnPct)   card.classList.add('warn');
  }

  async function fetchData() {
    try {
      const r = await fetch('/data', { cache: 'no-store' });
      if (!r.ok) throw new Error('bad response');
      const d = await r.json();

      const v   = parseFloat(d.volts);
      const a   = parseFloat(d.amps);
      const w   = parseFloat(d.watts);

      const pctV = Math.min((v / MAX_V) * 100, 100);
      const pctA = Math.min((a / MAX_A) * 100, 100);
      const pctW = Math.min((w / MAX_W) * 100, 100);

      elV.innerHTML  = fmt(v, 2) + '<span class="card-unit">V</span>';
      elA.innerHTML  = fmt(a, 2) + '<span class="card-unit">A</span>';
      elW.innerHTML  = fmt(w, 1) + '<span class="card-unit">W</span>';

      barV.style.width = pctV.toFixed(1) + '%';
      barA.style.width = pctA.toFixed(1) + '%';
      barW.style.width = pctW.toFixed(1) + '%';

      setCardState(cardV, pctV);
      setCardState(cardA, pctA);
      setCardState(cardW, pctW);

      loadPct.textContent = 'LOAD ' + pctW.toFixed(0) + '%';

      dot.style.background    = '#00e5ff';
      dot.style.boxShadow     = '0 0 8px #00e5ff';

      const now = new Date();
      const hms = now.toTimeString().split(' ')[0];
      tsLbl.textContent = hms;
      tsFull.textContent = 'Last update: ' + now.toLocaleString();

    } catch (e) {
      dot.style.background = '#ff2244';
      dot.style.boxShadow  = '0 0 8px #ff2244';
      tsFull.textContent   = 'Connection lost — retrying…';
    }
  }

  fetchData();
  setInterval(fetchData, 500);   // poll every 500 ms
</script>
</body>
</html>
)rawhtml";

// ─────────────────────────────────────────────────────────
//  Route handlers
// ─────────────────────────────────────────────────────────
void handleRoot() {
  server.send_P(200, "text/html", INDEX_HTML);
}

void handleData() {
  // Build a tiny JSON: {"volts":12.34,"amps":1.50,"watts":18.51}
  StaticJsonDocument<128> doc;
  doc["volts"] = serialized(String(gVolts, 3));
  doc["amps"]  = serialized(String(gAmps,  3));
  doc["watts"] = serialized(String(gWatts, 3));

  String out;
  serializeJson(doc, out);

  server.sendHeader("Access-Control-Allow-Origin", "*");
  server.sendHeader("Cache-Control", "no-cache");
  server.send(200, "application/json", out);
}

void handleNotFound() {
  server.send(404, "text/plain", "Not found");
}

// ─────────────────────────────────────────────────────────
//  setup()
// ─────────────────────────────────────────────────────────
void setup() {
  Serial.begin(115200);
  delay(500);
  Serial.println("\n=== DC Power Monitor — XIAO ESP32-C6 ===");

  // ── I2C + sensor init ──────────────────────────────────
  Wire.begin();

  if (!sensor.begin()) {
    Serial.println("[ERROR] ACS37800 not found. Check wiring / I2C address.");
    while (true) { delay(1000); }
  }
  Serial.println("[OK] ACS37800 detected.");

  // DC mode: fixed sample window (bypass zero-crossing detection)
  sensor.setNumberOfSamples(1023, true);   // max samples, write to EEPROM
  sensor.setBypassNenable(true,  true);    // bypass N, write to EEPROM

  // Explicitly set 30A current range (required — default may be wrong variant)
  sensor.setCurrentRange(30);

  // Give EEPROM writes time to settle before first read
  delay(100);

  // ── Auto zero-offset calibration ──────────────────────
  // Boot with NO load connected. Takes 20 averaged samples to find baseline.
  Serial.println("[CAL] Auto-zeroing current offset — disconnect load now...");
  delay(2000);   // 2 s window to disconnect load if needed
  float offsetAccum = 0.0f;
  const int CAL_SAMPLES = 20;
  for (int i = 0; i < CAL_SAMPLES; i++) {
    float cv = 0.0f, ca = 0.0f;
    sensor.readRMS(&cv, &ca);
    offsetAccum += ca;
    delay(50);
  }
  gAmpsOffset = offsetAccum / CAL_SAMPLES;
  Serial.printf("[CAL] Zero offset = %.4f A\n", gAmpsOffset);

  // ── WiFi ───────────────────────────────────────────────
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  Serial.print("[WiFi] Connecting");

  uint8_t attempts = 0;
  while (WiFi.status() != WL_CONNECTED && attempts < 30) {
    delay(500);
    Serial.print('.');
    attempts++;
  }

  if (WiFi.status() == WL_CONNECTED) {
    Serial.println();
    Serial.print("[WiFi] Connected! IP: ");
    Serial.println(WiFi.localIP());
    Serial.println("[Web] Open http://" + WiFi.localIP().toString() + " in your browser");
  } else {
    Serial.println("\n[WiFi] Connection failed — check credentials.");
    // Device keeps running; WiFi will auto-reconnect
  }

  // ── Web server routes ──────────────────────────────────
  server.on("/",      handleRoot);
  server.on("/data",  handleData);
  server.onNotFound(handleNotFound);
  server.begin();
  Serial.println("[Web] HTTP server started on port 80");
}

// ─────────────────────────────────────────────────────────
//  loop()
// ─────────────────────────────────────────────────────────
void loop() {
  // Handle any incoming HTTP clients
  server.handleClient();

  // Read sensor (~every 250 ms)
  static unsigned long lastRead = 0;
  if (millis() - lastRead >= 250) {
    lastRead = millis();

    float v = 0.0f, a = 0.0f;

    // readRMS() averages over the full 1023-sample window — correct for DC.
    ACS37800ERR err = sensor.readRMS(&v, &a);
    if (err != ACS37800_SUCCESS) {
      Serial.printf("[WARN] readRMS error code: %d\n", (int)err);
    }

    // Apply calibration:
    //   1. Subtract zero-load offset captured at boot
    //   2. Multiply by scale factor (corrects for gain error)
    float aCal = (a - gAmpsOffset) * CURRENT_SCALE;
    float vCal = v * VOLTAGE_SCALE;

    // For DC: P = V x I
    float w = vCal * aCal;

    // Clamp negatives (DC — no negative current expected in normal use)
    gVolts = max(vCal, 0.0f);
    gAmps  = max(aCal, 0.0f);
    gWatts = max(w,    0.0f);

    // Safety: flag if limits are approached
    if (gVolts > MAX_VOLTAGE * 0.95f || gAmps > MAX_CURRENT * 0.95f) {
      Serial.printf("[WARN] Near limit — V:%.2f  A:%.2f  W:%.2f\n",
                    gVolts, gAmps, gWatts);
    }

    // Shows raw vs calibrated so you can fine-tune CURRENT_SCALE
    Serial.printf("V: %6.3f  A(raw): %+7.4f  A(cal): %6.3f  W: %7.3f\n",
                  v, a, gAmps, gWatts);
  }
}

Step 7: การทดสอบใช้งานจริง (Using the Power Meter)

จ่ายไฟเข้าเครื่องแล้วรอสัก 2-3 วินาทีเพื่อให้บอร์ดเชื่อมต่อ Wi-Fi จากนั้นให้เปิดเบราว์เซอร์ในมือถือหรือคอมพิวเตอร์ที่อยู่ในวงเน็ตเวิร์กเดียวกัน แล้วพิมพ์ IP Address ที่แสดงใน Serial Monitor (เช่น ของผมคือ 192.168.1.75)

คุณจะเห็น Live Dashboard เด้งขึ้นมาพร้อมแสดงค่า Voltage (V), Current (A) และ Power (W) แบบเรียลไทม์เลยครับ!

Live Dashboard Running

ดาวน์โหลดไฟล์โปรเจกต์ทั้งหมด (Custom parts and files)

คุณสามารถดาวน์โหลดไฟล์ 3D, ไฟล์แผงวงจร และซอร์สโค้ดทั้งหมดไปลุยต่อกันได้ที่ลิงก์ด้านล่างนี้เลยครับ:

อัปเดตเทรนด์ฮาร์ดแวร์และโปรเจกต์ IoT สุดล้ำ

อยากทำโปรเจกต์ IoT เจ๋งๆ แบบนี้ใช้เองที่บ้าน หรือกำลังมองหาบอร์ดไมโครคอนโทรลเลอร์อยู่? แวะมาพูดคุยและหาของเล่นสาย Tech กับเราได้เลยครับ!

คำเตือน: เนื้อหานี้เป็นการสรุปและเรียบเรียงจากบทความต้นฉบับภาษาอังกฤษ ข้อมูลฉบับภาษาไทยอาจมีความคลาดเคลื่อนบางประการจากการตีความหรือย่อเนื้อหา โปรดอ้างอิงข้อมูลการเดินวงจรและโค้ดโดยละเอียดจากต้นฉบับ
อ้างอิงจาก (References):

แท็ก


Blog posts

เข้าสู่ระบบ

ลืมรหัสผ่านใช่ไหม?

ยังไม่มีบัญชีใช่ไหม?
สร้างบัญชี