Skip to content

Example Code

Five demos, ordered from “plug it in and watch” to “it texts you at 3am.” Working through them in order is the fastest way to understand the board.

DemoDifficultySD CardWiFiExtra Setup
Basic DemoBeginnerNoNoNone
Using the Button and BuzzerBeginnerNoNoNone
SD Card LoggerBeginnerRequiredNoNone
Google SheetsIntermediateNoYesGoogle Apps Script
Text NotificationsAdvancedNoYesSMTP account

All five read the same three pins (pinouts here) and report raw ADC counts, 0 to 4095. If you haven’t read why there are no ppm numbers, start there.



Reads the three sensors and graphs them. That’s the whole sketch. No menu, no commands, no SD card.

Web Serial Not Supported. Please use Chrome, Edge, or Opera.

One line does the work:

Serial.printf("CH4:%d,H2S:%d,CO:%d\n", ...);

label:value pairs are what name the three traces instead of leaving you with “Channel 1, 2, 3.” Same convention as the IMU tutorial.

It runs at 2 Hz. These sensors respond over seconds, so faster only scrolls the graph off your screen quicker.

Hold a marker or an alcohol wipe near the board and you’ll see all three lines move, not just one. Metal-oxide sensors are selective, not specific: each responds most strongly to its target gas but none is blind to the others. That cross-sensitivity is why the pattern across all three channels tells you more than any single one.



The Basic Demo plus the two parts of the HAT it ignores. The button starts and stops the graph, the buzzer confirms with a chirp, and it beeps on its own when a channel goes over a threshold. That last part makes the board useful sitting on a shelf with nothing plugged into it.

This one graphs and takes commands, so its panel below has both tabs.

Web Serial Not Supported. Please use Chrome, Edge, or Opera.
KeyAction
rPrint a single reading
uToggle units: raw ADC or millivolts
aToggle the audible alert
bTest the buzzer
?Show menu
ButtonStart / stop the graph

The buzzer is one call. Every GPIO on the ESP32-S3 can do PWM, so pin 13 needs no setup beyond OUTPUT:

void beep(unsigned int freq, unsigned long ms) {
tone(PIN_BUZZER, freq, ms);
delay(ms);
noTone(PIN_BUZZER);
}

tone() returns immediately rather than blocking, so the delay() is what actually holds the note. Different frequencies carry different meanings here: rising for on, falling for off, and a sharper tone for the alert. You can tell what the board is doing from across the room.

The button is active-low on the RX pin, and it gets debounced:

if (reading != lastButtonState) lastDebounceTime = millis();
if ((millis() - lastDebounceTime) > 50) { ... }

Note the moment the reading last changed, then only believe it once it has held still for 50 ms. Skip this and one press registers as three, because the metal contacts physically bounce.

The alert fires when any channel passes ALERT_THRESHOLD, with a 3-second cooldown so a sustained event chirps instead of screaming. 1200 is a placeholder. Watch your own clean-air readings first.



Writes a CSV to the micro SD card instead of your screen. Leave it running for a day and pull the card into a spreadsheet. A new file (/LOG_0001.CSV upward) is created every boot, so power-cycling never overwrites yesterday’s run.

Web Serial Not Supported. Please use Chrome, Edge, or Opera.
KeyAction
sForce a flush to the card now
pPrint the last 10 entries
nClose this file and start a new one
?Show menu
ButtonPause / resume logging

Two filters run on every value, and they kill different noise.

Supersampling averages 100 rapid ADC reads into one sample:

float supersample(int pin) {
long sum = 0;
for (int i = 0; i < SUPERSAMPLE_COUNT; i++) sum += analogRead(pin);
return (float)sum / SUPERSAMPLE_COUNT;
}

Random noise falls off as the square root of the sample count, so this is roughly 10x quieter than a single read, for about a millisecond of work. It kills jitter inside one measurement. It also gives you real digits after the decimal point that no single reading had, which is why the CSV keeps two of them.

The EMA is a one-line low-pass filter with no history buffer:

ema = EMA_ALPHA * newValue + (1.0f - EMA_ALPHA) * ema;

Each value is 10% newest reading, 90% everything before it. It kills wobble across measurements. Lower EMA_ALPHA is smoother and slower, higher tracks faster and lets more noise through.

Both columns go in the CSV so you can decide later which tells the better story.

The EMA seeds itself on the first reading (if (ema < 0.0f) ema = newValue;). Starting from zero would give you a filtered value climbing up from 0 for the first several rows, which looks exactly like a gas event.

The file flushes every 10 entries, capping your worst-case loss at ten seconds if someone trips over the USB cable. Millis_ms is milliseconds since boot, since there’s no RTC on the tinyCore.



Posts readings into a Google Sheet over WiFi. The board hits a Google Apps Script Web App, and the script appends a row with a real timestamp. No service account, no OAuth, no API key.

  1. Make a new Google Sheet, then go to Extensions → Apps Script. Delete what’s in the editor and paste in the doGet function from the comment block at the top of the sketch below.

  2. Click Deploy → New deployment, type Web app. Set Execute as to Me and Who has access to Anyone.

    That second setting means anyone who knows the URL can add a row. The URL is a long random string and is the only credential, so treat it like a password.

  3. Click Deploy, authorize when Google asks, and copy the Web app URL.

  4. Fill in your WiFi details, upload, and open the console below. On first boot the sketch asks you to paste that URL. It’s saved to flash and you won’t be asked again.

Web Serial Not Supported. Please use Chrome, Edge, or Opera.
KeyAction
rPost a reading right now
uUpdate the Apps Script URL
wPrint WiFi status and signal strength
?Show menu
ButtonPause / resume logging

Press r first. If a row lands in your sheet, everything downstream works.

The URL lives in NVS via the Preferences library, which survives reflashing. Paste it once and it stays through every future upload. If you want your WiFi credentials to work the same way, loadUrl() / saveUrl() / promptForUrl() are the whole pattern.

Data goes out as query parameters on a plain GET, which the script reads as e.parameter.ch4. Useful side effect: you can test the pipeline by pasting the URL into your browser before the ESP32 is involved.

One line does more than it looks like:

http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);

Apps Script always answers a request with a 302 redirect to script.googleusercontent.com. Without this, HTTPClient hands back a 302, your data goes nowhere, and nothing looks broken. No error, just rows that never appear.

Uploads run every 10 seconds rather than every second, since each one is a network round-trip.



Watches all three channels against thresholds you set. If one goes over, the tinyCore emails you over SMTP. Point that email at your carrier’s SMS gateway and it arrives as a text.

Web Serial Not Supported. Please use Chrome, Edge, or Opera.
KeyAction
tPrint the active thresholds
sPrint current sensor readings
aArm / disarm the alarm
eSend a test email
wPrint WiFi status and signal strength
?Show menu
ButtonArm / disarm the alarm

Send a test email with e before you trust it. SMTP fails for boring reasons: wrong port, App Password not generated, provider blocking an unfamiliar IP. If it fails, flip smtp.debug(0) to smtp.debug(1) and the library will tell you where the handshake died.

The 1200 thresholds are placeholders, not tuned for your board. Every sensor has its own resting resistance and your room’s air isn’t anyone else’s. Run the Basic Demo, watch your channels sit in clean air for a while, note where each rests and how far it drifts on its own, then set each threshold above that drift with room to spare. Press s here to check readings without leaving the sketch.

Threshold checks use the same 100-read supersample as the logger. A single noisy ADC spike is exactly what would fire off a false 3am text.

Each gas has its own 60-second cooldown, so a methane event doesn’t silence your CO alerts. With no cooldown, a two-minute leak at a 2-second sample interval would send 60 emails and get you rate-limited.

Carrier SMS gateways go in ALERT_RECIPIENT_EMAIL:

CarrierAddress format
AT&T5551234567@txt.att.net
Verizon5551234567@vtext.com
T-Mobile5551234567@tmomail.net
Google Fi5551234567@msg.fi.google.com

These are free but not guaranteed. Carriers throttle them and occasionally break them.



The pieces recombine. The buzzer code from the second demo dropped into Text Notifications gives you a local alert alongside the text message. The logger plus threshold logic gives you a device that records continuously and shouts when something happens.