⚡ AC Clamp Meter

Free Ammeter in Your Pocket — no extra hardware, no external clamp, no soldering. Just your phone's built-in magnetic sensor and SoifGo.

0.00A
AC CURRENT · RMS ESTIMATE
📱 Open the App 🤖 AI Context

The whole story is simple

  1. Step 1 — Decimal → Integer. First, using a factor of 10, we convert the decimal values into integers. This gives us a much better numeric sample from the magnetic sensor.
  2. Step 2 — Take 50 samples. Then we take 50 samples of the alternating field.
  3. Step 3 — Find the peak. We find the highest and the lowest point of the field.
  4. Step 4 — Peak-to-peak. We subtract the low from the high — that gives us the peak of the field.
  5. Step 5 — Divide by calibration. We divide that peak by the calibration number. The result is the current in Amperes.

What is calibration? Why 420?

The number 420 is an experimental value found by the author of this app, measured on his own phone at 1 Ampere. On your phone, at 1 Ampere, you will probably get a different number.

So adjust it until your ammeter reads correctly. Use a known load (for example a 1 kW heater ≈ 4.35 A at 230 V), compare the reading with a reference ammeter, and change the CAL FACTOR in the app until it matches.

💡 Build your own version

The code below is a sample for you. You can build a better one — the idea is what matters. Take your own design idea and give it to an AI assistant; it will help you build it. If the assistant gets stuck, give it this address: https://soifgo.github.io/soifgo/SOIFGO_AI_CONTEXT.md — and if it still can't reach it, paste the content of that file to it. You can also give it the example code below.

Watch the video

Full video (MEGA)

The example code

ac_clamp_meter.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AC Clamp Meter</title>
<style>
  * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
  body {
    font-family: 'Segoe UI', sans-serif; margin: 0; padding: 14px;
    background: #0a0e14; color: #e6edf3;
    min-height: 100vh; display: flex; flex-direction: column;
    align-items: center; justify-content: flex-start;
  }
  h2 { margin: 4px 0 12px; font-size: 18px; font-weight: 600;
       color: #4dd0e1; letter-spacing: 1px; }

  .digital {
    background: #041410; border: 2px solid #0d5c4a; border-radius: 14px;
    padding: 14px 28px; margin-bottom: 16px; width: 100%; max-width: 340px;
    text-align: center; box-shadow: 0 0 24px rgba(0,255,180,.15) inset;
  }
  .digital .val {
    font-family: 'Courier New', monospace;
    font-size: 56px; font-weight: 700; color: #00ffb3;
    text-shadow: 0 0 18px rgba(0,255,179,.7); line-height: 1;
  }
  .digital .unit { font-size: 22px; color: #0a8f6f; margin-left: 6px; }
  .digital .sub { font-size: 12px; color: #3d8f7a; margin-top: 6px; letter-spacing: 1px; }

  .gauge-wrap { position: relative; width: 300px; height: 300px; margin-bottom: 6px; }
  .gauge-wrap svg { width: 100%; height: 100%; display: block; }

  #rangeBadge {
    font-family: monospace; font-size: 13px; color: #4dd0e1;
    letter-spacing: 1px; margin-bottom: 14px;
    background: #101820; padding: 4px 14px; border-radius: 20px;
    border: 1px solid #1d2c38;
  }

  .info {
    display: flex; gap: 10px; width: 100%; max-width: 340px;
    margin-bottom: 12px;
  }
  .info .card {
    flex: 1; background: #131a22; border-radius: 10px;
    padding: 8px; text-align: center; font-size: 12px; color: #7d8b99;
  }
  .info .card b { display: block; font-size: 18px; color: #e6edf3; margin-top: 2px; }

  .controls { width: 100%; max-width: 340px; display: flex; flex-direction: column; gap: 10px; }
  .row { display: flex; gap: 10px; }
  button {
    flex: 1; padding: 14px; font-size: 15px; font-weight: 600;
    border: 0; border-radius: 10px; cursor: pointer; transition: .15s;
  }
  .btn-zero { background: #f5a623; color: #1a1a1a; }
  .btn-zero:active { background: #c8851a; }
  .btn-reset { background: #2b3540; color: #cfd8dc; }
  .btn-reset:active { background: #1f272f; }

  .cal {
    background: #131a22; border-radius: 10px; padding: 12px;
    display: flex; align-items: center; gap: 10px;
  }
  .cal label { font-size: 12px; color: #7d8b99; white-space: nowrap; }
  .cal input {
    flex: 1; background: #0a0e14; border: 1px solid #2b3540; color: #4dd0e1;
    border-radius: 8px; padding: 10px; font-size: 16px; text-align: center;
    font-family: monospace;
  }
  #status { font-size: 11px; color: #4a5560; margin-top: 8px; text-align: center; }
</style>
</head>
<body>

  <h2>⚡ AC CLAMP METER</h2>

  <div class="digital">
    <div><span class="val" id="amps">0.00</span><span class="unit">A</span></div>
    <div class="sub">AC CURRENT (RMS EST.)</div>
  </div>

  <div class="gauge-wrap">
    <svg viewBox="0 0 300 300">
      <path id="arcTrack" fill="none" stroke="#1a232e" stroke-width="16"
            stroke-linecap="round"/>
      <path id="arcFill" fill="none" stroke="#00ffb3" stroke-width="16"
            stroke-linecap="round"/>
      <g id="ticks" stroke="#3d4b5a" stroke-width="2"></g>
      <g id="tickLabels" fill="#7d8b99" font-size="12" text-anchor="middle"
         font-family="monospace"></g>
    </svg>
  </div>
  <div id="rangeBadge">RANGE · 0 – 5 A</div>

  <div class="info">
    <div class="card">MAX<b id="maxVal">0.00</b></div>
    <div class="card">MIN<b id="minVal">0.00</b></div>
    <div class="card">P-P RAW<b id="ppVal">0</b></div>
  </div>

  <div class="controls">
    <div class="row">
      <button class="btn-zero"  onclick="zeroNow()">🎯 ZERO / TARE</button>
      <button class="btn-reset" onclick="resetAll()">↺ RESET</button>
    </div>
    <div class="cal">
      <label>CAL<br>FACTOR</label>
      <input id="calFactor" type="number" value="420" inputmode="decimal">
    </div>
    <div id="status">waiting…</div>
  </div>

<script>
var BUFFER_SIZE   = 50;
var SCALE         = 10;
var DEFAULT_CAL   = 420;

var START_ANGLE = -210;
var END_ANGLE   =   30;
var TOTAL_ARC   = END_ANGLE - START_ANGLE;
var CX = 150, CY = 150;
var R  = 120;

var RANGES = [5, 10, 20];
var currentRange = RANGES[0];

function angleToXY(deg, radius) {
  var a = deg * Math.PI / 180;
  return { x: CX + radius * Math.cos(a), y: CY + radius * Math.sin(a) };
}

function arcPath(degFrom, degTo, radius) {
  var p1 = angleToXY(degFrom, radius);
  var p2 = angleToXY(degTo,   radius);
  var delta = degTo - degFrom;
  var largeArc = Math.abs(delta) > 180 ? 1 : 0;
  var sweep    = delta > 0 ? 1 : 0;
  return 'M ' + p1.x + ' ' + p1.y +
         ' A ' + radius + ' ' + radius + ' 0 ' + largeArc + ' ' + sweep +
         ' ' + p2.x + ' ' + p2.y;
}

var buffer      = [];
var zeroOffset  = 0;
var currentA    = 0;
var maxSeen     = 0;
var minSeen     = Infinity;
var calFactor   = DEFAULT_CAL;
var lastPeak    = 0;

function buildGauge() {
  var full = arcPath(START_ANGLE, END_ANGLE, R);
  document.getElementById('arcTrack').setAttribute('d', full);
  document.getElementById('arcFill').setAttribute('d',  full);
  document.getElementById('arcFill')
          .setAttribute('stroke-dasharray', '0 9999');

  var ticksG = document.getElementById('ticks');
  var labG   = document.getElementById('tickLabels');
  ticksG.innerHTML = '';
  labG.innerHTML   = '';

  var steps = 10;
  for (var i = 0; i <= steps; i++) {
    var frac = i / steps;
    var deg  = START_ANGLE + frac * TOTAL_ARC;
    var p1 = angleToXY(deg, R);
    var p2 = angleToXY(deg, R - 12);

    var line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
    line.setAttribute('x1', p1.x); line.setAttribute('y1', p1.y);
    line.setAttribute('x2', p2.x); line.setAttribute('y2', p2.y);
    ticksG.appendChild(line);

    var pl = angleToXY(deg, R - 28);
    var txt = document.createElementNS('http://www.w3.org/2000/svg', 'text');
    txt.setAttribute('x', pl.x);
    txt.setAttribute('y', pl.y + 4);
    var lbl = currentRange * frac;
    txt.textContent = (currentRange >= 10) ? lbl.toFixed(0) : lbl.toFixed(1);
    labG.appendChild(txt);
  }

  document.getElementById('rangeBadge').textContent =
    'RANGE · 0 – ' + currentRange + ' A';
}

function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }

function setArc(amps) {
  var frac = clamp(amps / currentRange, 0, 1);
  var arcLen = (2 * Math.PI * R) * (TOTAL_ARC / 360);
  document.getElementById('arcFill')
          .setAttribute('stroke-dasharray', (frac * arcLen) + ' 9999');
}

function refreshReadout() {
  document.getElementById('amps').textContent = currentA.toFixed(2);
  document.getElementById('maxVal').textContent = (maxSeen === 0 && minSeen === Infinity)
        ? '0.00' : maxSeen.toFixed(2);
  document.getElementById('minVal').textContent = (minSeen === Infinity)
        ? '0.00' : minSeen.toFixed(2);
  document.getElementById('ppVal').textContent = Math.round(lastPeak);
  setArc(currentA);
}

function pickRange(amps) {
  for (var i = 0; i < RANGES.length; i++) {
    if (amps <= RANGES[i]) return RANGES[i];
  }
  return RANGES[RANGES.length - 1];
}

function zeroNow() {
  if (buffer.length === 0) return;
  var sum = 0;
  for (var i = 0; i < buffer.length; i++) sum += buffer[i];
  zeroOffset = sum / buffer.length;
  document.getElementById('status').textContent = 'Zeroed at ' + zeroOffset.toFixed(1);
}

function resetAll() {
  buffer = [];
  zeroOffset = 0;
  currentA = 0;
  maxSeen = 0;
  minSeen = Infinity;
  lastPeak = 0;
  currentRange = RANGES[0];
  buildGauge();
  refreshReadout();
  document.getElementById('status').textContent = 'Reset.';
}

document.getElementById('calFactor').addEventListener('input', function () {
  var v = parseFloat(this.value);
  if (!isNaN(v) && v > 0) calFactor = v;
});

function html_sensor_magno(data) {
  var raw = parseFloat(data);
  if (isNaN(raw)) return;
  raw = raw * SCALE;
  buffer.push(raw);
  if (buffer.length > BUFFER_SIZE) buffer.shift();
  if (buffer.length < BUFFER_SIZE) {
    document.getElementById('status').textContent =
      'buffering ' + buffer.length + '/' + BUFFER_SIZE;
    return;
  }
  var mx = -Infinity, mn = Infinity;
  for (var i = 0; i < buffer.length; i++) {
    var v = buffer[i] - zeroOffset;
    if (v > mx) mx = v;
    if (v < mn) mn = v;
  }
  var pp = mx - mn;
  lastPeak = pp;
  currentA = pp / calFactor;
  if (currentA > maxSeen) maxSeen = currentA;
  if (currentA < minSeen) minSeen = currentA;
  var newRange = pickRange(currentA);
  if (newRange !== currentRange) {
    currentRange = newRange;
    buildGauge();
  }
  refreshReadout();
  document.getElementById('status').textContent =
    'live · buffer ' + buffer.length + ' · cal ' + calFactor +
    ' · range ' + currentRange + 'A';
}
window.html_sensor_magno = html_sensor_magno;

window.onload = function () {
  buildGauge();
  refreshReadout();
  if (window.soifgo) {
    window.soifgo.CallSub('sensor_light', true);
    window.soifgo.CallSub('sensor_move', true);
    window.soifgo.CallSub('sensor_magno', true);
    document.getElementById('status').textContent = 'sensors activating…';
  } else {
    document.getElementById('status').textContent =
      'Not running inside SoifGo (bridge unavailable)';
  }
};
</script>
</body>
</html>

⬅ Back to Home