Monitoring ground vibrations—whether from microseismic events, volcanic tremor, or anthropogenic noise—typically requires expensive digitizers and software. Here we describe a simple, portable system built from off-the-shelf components: an Arduino, an ADS1015 ADC, and any modern browser’s Web-Serial API. You’ll stream live velocity measurements (in mm/s) from up to four geophone channels and plot them in real time using SVG charts.

Hardware & Wiring
• Arduino Uno (or any 5 V/3.3 V board with USB-serial)
• Adafruit ADS1015 12-bit, four-channel ADC breakout
• Single-ended geophones (in our case wired to AIN0–AIN3 (common return to ADS1015 GND)
• USB cable to connect Arduino → PC or running browser device
System Block Diagram
[Geophones] → [ADS1015 ADC] → [Arduino I²C] → [USB-Serial] → [Browser Dashboard]

https://www.seis-tech.com/gs-one-geophone-sensor-equivalent/
Wiring
If you’re using the Adafruit (or similar) ADS1015 “breakout” board rather than the bare chip, the wiring is almost identical—the module already carries I²C pull-ups and an address‐select jumper. Here’s exactly how to hook everything up.
- Power & Ground
• ADS1015 VDD → Arduino 5 V (or 3.3 V if that’s your board’s logic rail)
• ADS1015 GND → Arduino GND - I²C Bus
• ADS1015 SDA → Arduino SDA
– On an Uno/Nano: analog pin A4
– On a Mega: pin 20
– On a Leonardo/Micro: the dedicated SDA pin next to SCL
• ADS1015 SCL → Arduino SCL
– On an Uno/Nano: analog pin A5
– On a Mega: pin 21
– On a Leonardo/Micro: the dedicated SCL pinThe breakout board includes 10 kΩ pull-ups from SDA/SCL → VDD, so you don’t need extra resistors for short runs. - Address Selection (if you have multiple ADS1X15 boards)
• The default ADS1015 I²C address is 0x48 (ADDR pin → GND).
• If you must change it: move ADDR → VDD (0x49) or ADDR → SDA (0x4A) or ADDR → SCL (0x4B). - Sensor Inputs (Single-Ended Mode)
Each geophone is effectively a voltage source referenced to ground. On the ADS1015 breakout:• Geophone #0 “+” lead → ADS1015 A0 (labeled A0 or “AIN0”)
• Geophone #1 “+” lead → ADS1015 A1 (“AIN1”)
• Geophone #2 “+” lead → ADS1015 A2 (“AIN2”)
• Geophone #3 “+” lead → ADS1015 A3 (“AIN3”)
• All geophone “–” (negative) leads → ADS1015 GND (common ground) - USB & Browser Link
• Connect your Arduino’s USB port to the PC or tablet that will run the browser.
• The browser page uses the Web-Serial API to open the Arduino’s COM port at 230 400 baud.
Arduino Sketch for Data Acquisition
We configure the ADS1015 for ±4.096 V range (GAIN_ONE) and the highest data rate (3 300 samples/s). A continuous, round-robin conversion cycle reads each channel in turn. Every 4 ms (250 Hz per channel), the loop:
- Reads four 16-bit signed counts via
ads.getLastConversionResults(). - Converts raw counts → voltage → ground-velocity (mm/s) using a scale factor.
- Prints all four channel values as comma-separated ASCII floats, e.g.:
0.123,-0.045,0.067,-0.012\n - Re-starts the next conversion in the cycle.
Serial is set to 230 400 baud to sustain this throughput without buffer overruns.
Sketch
#include <Wire.h>
#include <Adafruit_ADS1X15.h>
Adafruit_ADS1015 ads;
// Geophone + ADS1015 parameters
const float V_PER_COUNT = 4.096f / 2048.0f; // ±4.096 V full-scale → 2 mV/count
const float GEO_SENS = 20.9f; // V per (m/s) with 1 kΩ shunt
const float SCALE_MM_S = (V_PER_COUNT / GEO_SENS) * 1000.0f; // mm/s per count
const uint32_t SAMPLE_INTERVAL_MS = 4; // 250 Hz → every 4 ms
void setup() {
Serial.begin(230400);
while (!Serial);
if (!ads.begin()) {
Serial.println("ERROR: ADS1015 not found");
while (true);
}
ads.setGain(GAIN_ONE); // ±4.096 V
ads.setDataRate(RATE_ADS1015_3300SPS); // 3 300 SPS
}
void loop() {
static uint32_t last = 0;
if (millis() - last < SAMPLE_INTERVAL_MS) return;
last += SAMPLE_INTERVAL_MS;
// read 4 single-ended channels in single-shot mode
float v[4];
for (uint8_t ch = 0; ch < 4; ch++) {
int16_t raw = ads.readADC_SingleEnded(ch);
v[ch] = raw * SCALE_MM_S;
}
// send as CSV float with 3 decimal places
Serial.print(v[0], 3); Serial.print(',');
Serial.print(v[1], 3); Serial.print(',');
Serial.print(v[2], 3); Serial.print(',');
Serial.println(v[3], 3);
}
Browser-Based Dashboard (HTML + JavaScript)
We leverage the Web-Serial API to open the Arduino’s COM port at 230 400 baud. Incoming bytes pass through a TextDecoderStream and accumulate until each newline. Each line—four comma-separated floats—is parsed, stamped with a timestamp, and pushed into per-channel circular buffers (up to 1 000 samples each).
Every ~33 ms (≈30 FPS), the page redraws four SVG charts. Charts feature:
• Auto-scaled Y-axes with gridlines at 0.25 mm/s steps
• Time labels on the X-axis (up to six per chart)
• Checkboxes to show/hide individual channels
• Smooth scrolling effect as new data arrives
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
<title>Geophones On Arduino Live (mm/s)</title>
<style>
* { box-sizing:border-box; margin:0; padding:0 }
body {
font-family: sans-serif;
background: #f5f7fa;
color: #333;
max-width: 90vw;
margin: 2em auto;
padding: 0 1em;
}
h1 { text-align:center; margin-bottom:1em }
.controls { text-align:center; margin-bottom:1em }
button {
padding:.4em .8em; margin:0 .3em;
font-size:.9em; border:none; border-radius:4px;
cursor:pointer; background:#0366d6; color:#fff;
transition:background .2s;
}
button:hover { background:#024f9b }
button:disabled { background:#bbb; cursor:default }
.charts { display:flex; flex-direction:column; gap:1em }
.chart-card {
background:#fff; border-radius:6px;
box-shadow:0 1px 4px rgba(0,0,0,.1);
overflow:hidden;
}
.chart-header {
display:flex; justify-content:space-between; align-items:center;
background:#f5f5f5; padding:.5em .75em;
border-bottom:1px solid #e0e0e0; font-weight:600;
}
.chart-body { width:100%; height:200px; background:#fafafa }
.chart-body svg { width:100%; height:100% }
.grid-line { stroke:#ddd; stroke-width:1 }
.axis { stroke:#333; stroke-width:1.2 }
.ylabel { fill:#333; font-size:11px; text-anchor:end }
.xlabel { fill:#333; font-size:11px; text-anchor:middle }
.line0 { fill:none; stroke:#e74c3c; stroke-width:2 }
.line1 { fill:none; stroke:#3498db; stroke-width:2 }
.line2 { fill:none; stroke:#27ae60; stroke-width:2 }
.line3 { fill:none; stroke:#f39c12; stroke-width:2 }
</style>
</head>
<body>
<h1>Geophones On Arduino Live (mm/s)</h1>
<div class="controls">
<button id="btnConnect">Connect</button>
<button id="btnDisconnect" disabled>Disconnect</button>
<span id="status">Status: Disconnected</span>
</div>
<div class="charts">
<div class="chart-card" id="card0">
<div class="chart-header">
<span>Channel 0 (AIN0)</span>
<label><input type="checkbox" class="toggle-ch" data-idx="0" checked> Show</label>
</div>
<div class="chart-body"><svg id="svg0"></svg></div>
</div>
<div class="chart-card" id="card1">
<div class="chart-header">
<span>Channel 1 (AIN1)</span>
<label><input type="checkbox" class="toggle-ch" data-idx="1" checked> Show</label>
</div>
<div class="chart-body"><svg id="svg1"></svg></div>
</div>
<div class="chart-card" id="card2">
<div class="chart-header">
<span>Channel 2 (AIN2)</span>
<label><input type="checkbox" class="toggle-ch" data-idx="2" checked> Show</label>
</div>
<div class="chart-body"><svg id="svg2"></svg></div>
</div>
<div class="chart-card" id="card3">
<div class="chart-header">
<span>Channel 3 (AIN3)</span>
<label><input type="checkbox" class="toggle-ch" data-idx="3"> Show</label>
</div>
<div class="chart-body" style="display:none"><svg id="svg3"></svg></div>
</div>
</div>
<script>
if (!("serial" in navigator)) {
alert("Web Serial API not supported.");
}
const MAX_PTS = 1000;
const Y0_STEP = 0.5; // Y‐axis tick step
const DRAW_INT_MS = 33; // ~30 FPS
const svgs = [0,1,2,3].map(i => document.getElementById('svg'+i));
const visible = [true,true,true,false];
const data = [[],[],[],[]];
let port, reader, run = false, buf = '', lastDraw = 0;
const btnConnect = document.getElementById('btnConnect');
const btnDisconnect = document.getElementById('btnDisconnect');
const statusSpan = document.getElementById('status');
// Channel visibility toggles
document.querySelectorAll('.toggle-ch').forEach(chk => {
chk.onchange = () => {
const idx = +chk.dataset.idx;
visible[idx] = chk.checked;
document
.getElementById('card'+idx)
.querySelector('.chart-body')
.style.display = chk.checked ? '' : 'none';
drawAll();
};
});
// Connect button
btnConnect.onclick = async () => {
try {
port = await navigator.serial.requestPort();
await port.open({ baudRate: 230400 });
const decoder = new TextDecoderStream();
port.readable.pipeTo(decoder.writable);
reader = decoder.readable.getReader();
run = true;
btnConnect.disabled = true;
btnDisconnect.disabled = false;
statusSpan.textContent = 'Status: Connected';
readLoop();
} catch (e) {
console.error(e);
statusSpan.textContent = 'Error';
}
};
// Disconnect button
btnDisconnect.onclick = async () => {
run = false;
btnConnect.disabled = false;
btnDisconnect.disabled = true;
statusSpan.textContent = 'Status: Disconnected';
if (reader) { await reader.cancel(); reader.releaseLock(); }
if (port) { await port.close(); }
};
// Read incoming ASCII lines of "f0,f1,f2,f3\n"
async function readLoop() {
while (run) {
const { value, done } = await reader.read();
if (done) break;
buf += value;
let nl;
while ((nl = buf.indexOf('\n')) >= 0) {
const line = buf.slice(0, nl).trim();
buf = buf.slice(nl + 1);
const parts = line.split(',').map(parseFloat);
if (parts.length === 4 && parts.every(v => !isNaN(v))) {
const ts = Date.now();
parts.forEach((v, i) => {
data[i].push({ v, t: ts });
if (data[i].length > MAX_PTS) data[i].shift();
});
const now = performance.now();
if (now - lastDraw > DRAW_INT_MS) {
lastDraw = now;
drawAll();
}
}
}
}
}
// Draw all visible charts
function drawAll() {
svgs.forEach((svg,i) => drawChart(svg, data[i], visible[i], i));
}
// Single‐chart draw routine
function drawChart(svg, arr, show, idx) {
const W = svg.clientWidth, H = svg.clientHeight;
const m = { left:40, right:40, top:20, bottom:30 };
const w = W - m.left - m.right, h = H - m.top - m.bottom;
while (svg.firstChild) svg.removeChild(svg.firstChild);
if (!show || arr.length < 2) return;
// dynamic Y‐range
const absMax = Math.max(...arr.map(o => Math.abs(o.v)));
const Y0 = Math.ceil(absMax / Y0_STEP) * Y0_STEP;
const lines = Math.ceil(Y0 / Y0_STEP);
// horizontal grid + Y‐labels
for (let i = -lines; i <= lines; i++) {
const yVal = (i * Y0_STEP).toFixed(2);
const f = (i * Y0_STEP) / Y0;
const yPos = m.top + ((1 - (f + 1)/2) * h);
mk(svg,'line', { x1:m.left, y1:yPos, x2:m.left+w, y2:yPos, class:'grid-line' });
mk(svg,'text',{ x:m.left-6, y:yPos+4, class:'ylabel' }, yVal);
}
// Y‐axis
mk(svg,'line',{ x1:m.left, y1:m.top, x2:m.left, y2:m.top+h, class:'axis' });
// X‐axis
mk(svg,'line',{ x1:m.left, y1:m.top+h, x2:m.left+w, y2:m.top+h, class:'axis' });
// X‐ticks + time labels (up to 6)
const pts = arr.length, maxLab = 6, skip = Math.ceil(pts / maxLab);
for (let i = 0; i < pts; i++) {
const x = m.left + i * (w / (MAX_PTS - 1));
if (i % skip === 0 || i === pts - 1) {
mk(svg,'line',{
x1:x, y1:m.top+h, x2:x, y2:m.top+h+4, class:'axis'
});
mk(svg,'text',{
x:x, y:m.top+h+18, class:'xlabel'
}, fmtTime(arr[i].t));
}
}
// data polyline
const ptsStr = arr.map((o,i) => {
const x = m.left + i * (w / (MAX_PTS - 1));
const y = m.top + h - ((o.v + Y0) / (2 * Y0)) * h;
return `${x},${y}`;
}).join(' ');
mk(svg,'polyline',{ points:ptsStr, class:'line'+idx });
}
// format timestamp for X‐axis
function fmtTime(ms) {
return new Date(ms).toTimeString().substr(0,8);
}
// utility to make SVG elements
function mk(svg,tag,attrs,text) {
const el = document.createElementNS('http://www.w3.org/2000/svg', tag);
for (const k in attrs) el.setAttribute(k, attrs[k]);
if (text != null) el.textContent = text;
svg.appendChild(el);
}
window.addEventListener('resize', drawAll);
</script>
</body>
</html>
Monitoring Geo-Activity
With this setup, you can:
• Visualize seismic arrivals and tremor in real time.
• Log data (e.g. via extending the JS to POST CSV) for offline analysis.
• Deploy multiple units for array-based localization.
• Demonstrate ground-motion sensing in field or classroom settings at minimal cost.
Conclusion
This Arduino + ADS1015 + Web-Serial combination yields a powerful yet accessible geophone monitoring station. You get live, zoom-free plots of ground-velocity in mm/s, full control over channel visibility, and the flexibility to extend both the data‐acquisition code and web‐based interface for logging, alarm triggers, or deeper signal processing.