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.
The example code
ac_clamp_meter.html
📋 Copy code
<!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>