Vibe Coding on the Cardputer: From Wi-Fi Scanning to a Pocket-Sized Wind-Storm Alarm
There’s a moment, every once in a while, when a piece of technology pulls you right back into the joy of tinkering. Not work. Not strategy. Not architecture.
Just tinkering.
That’s exactly what happened when I picked up the Cardputer ADV, a pocket-sized ESP32 computer with a built-in display, keyboard, speaker, and enough personality to feel like it came straight out of a 1980s sci-fi film.
I didn’t buy it to solve a problem.
I bought it because it looked fun.
And it has been very fun.
It’s the perfect device for vibe coding: the kind of coding where you start with a vague idea, let Copilot/ChatGPT fill in the blanks, flash it, and see what happens. No ceremony. No twelve-step toolchain setup. Just code → upload → play.
And it immediately reminded me of something I had built almost a decade ago.
A Bit of Nostalgia: Wind Alerts on the Microsoft Band (2015)
Back when the Microsoft Band first came out, I built a tiny experiment: a wind alert app that triggered a notification on my wrist whenever winds exceeded a certain threshold.
I was fascinated by how weather could be turned into personal signals.
That old blog is still online:
https://learn.microsoft.com/en-us/archive/blogs/shishirs/wind-alerts-on-microsoft-band
It was simple, hacky, forward-leaning, and it made me want to recreate that experience with today’s tools.
So when I looked at the Cardputer, with its built-in speaker and Wi-Fi and instant-on feel, the idea resurfaced immediately:
What if I rebuilt my old Microsoft Band wind-alert system, this time with a loud alarm?
Experiment #1: Warm-Up with a Wi-Fi Scanner
Before building anything serious, I started with a warm-up project: a Wi-Fi scanner.
It took one prompt, one flash, and it worked on the first try.
The AI-generated code loaded, compiled, and ran with no drama:
Scanning nearby access points
Displaying SSIDs and signal strengths
Scrolling with the Cardputer keyboard
Formatting cleanly on the tiny screen
There was something deeply satisfying about this.
It was fast. It was simple. It was frictionless.
And it fueled the next idea.
Experiment #2: A Wind-Storm Alarm Using NOAA SISW1
Living on the coast, in the Pacific Northwest, I pay a lot of attention to the wind. Storms roll in from the Pacific Ocean, funnel through the Strait of Juan de Fuca, and then sweep inland with surprising force.
One of the best upstream indicators is NOAA Station SISW1 on Smith Island, perfectly positioned to catch those inbound marine winds long before they hit the mainland.
If SISW1 registers 30+ mph, I know it’s time to:
bring the furniture in
tie down anything loose
check the yard
prepare for gusty conditions
I don’t want to wait until winds hit 50 or 60 mph.
I want an early warning.
So I built a simple Cardputer app that:
Connects to Wi-Fi
Fetches current wind conditions from
https://api.weather.gov/stations/SISW1/observations/latest
Extracts wind speed, gusts, and direction
Converts m/s → mph
Displays everything on the Cardputer screen
Every 10 minutes, checks speed and plays a loud alarm if winds exceed 30 mph
A tiny storm sentinel in my pocket.
And with the right headers (Accept-Encoding: identity), the NOAA API returns uncompressed JSON that the ESP32 can parse cleanly.
I tested out the API using the Thunder Client Extension for Visual Studio Code
Why This Felt Meaningful
This wasn’t just a project.
It was a recreation of something I built 10 years ago, but now with better hardware, better APIs, better tools, and a better understanding of how to make it useful.
It reminded me how much I love building things that:
respond to the real world
help me prepare
feel personal
are fun to tinker with
Most importantly, it reminded me of why I love coding, not professionally, but creatively.
Not for delivery, but for discovery.
Vibe coding + Cardputer ADV = pure maker joy.
The Complete Cardputer Wind Alarm Code (Copy & Paste)
Here is the full working code that anyone can upload to their Cardputer ADV to recreate the wind-storm alarm exactly as I built it.
Just replace:
const char* ssid = “YOUR_WIFI_SSID”;
const char* password = “YOUR_WIFI_PASSWORD”;
Find the weather station closest to you via the National Data Buoy Center (mine is SISW1)
…and flash.
🌬 FULL CARDPUTER WIND-STORM ALARM CODE (NOAA SISW1)
#include “M5Cardputer.h”
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <ArduinoJson.h>
// ================== WIFI CONFIG ==================
const char* ssid = “YOUR_WIFI_SSID”;
const char* password = “YOUR_WIFI_PASSWORD”;
// ================== NWS CONFIG ===================
const char* host = “api.weather.gov”;
const int httpsPort = 443;
const char* stationId = “SISW1”; // Smith Island, WA
// ================== UPDATE & ALARM CONFIG ========
// 10 minutes in milliseconds
const unsigned long UPDATE_INTERVAL = 10UL * 60UL * 1000UL;
// Wind alarm threshold (mph)
const float WIND_ALARM_THRESHOLD_MPH = 20.0;
unsigned long lastUpdate = 0;
// ================== DATA STRUCT ==================
struct WindData {
double speedMps = NAN;
double gustMps = NAN;
double direction = NAN;
bool valid = false;
};
// ================== HELPERS ======================
String degToCompass(double deg) {
if (isnan(deg)) return “N/A”;
const char* dirs[] = {”N”,”NNE”,”NE”,”ENE”,”E”,”ESE”,”SE”,”SSE”,
“S”,”SSW”,”SW”,”WSW”,”W”,”WNW”,”NW”,”NNW”};
int idx = (int)((deg / 22.5) + 0.5);
idx = idx % 16;
return String(dirs[idx]);
}
double mpsToMph(double mps) {
if (isnan(mps)) return NAN;
return mps * 2.23694; // NWS m/s -> mph
}
// ================== DISPLAY ======================
void drawHeader(const char* status) {
auto& d = M5Cardputer.Display;
d.fillScreen(BLACK);
d.setCursor(0, 0);
d.setTextSize(1);
d.setTextColor(GREEN, BLACK);
d.println(”Smith Island Wind Monitor”);
d.setTextColor(WHITE, BLACK);
d.println(”Station: SISW1 (NOAA/NWS)”);
d.println(”---------------------------”);
d.println(status);
d.println();
}
void drawWindData(const WindData& wd) {
auto& d = M5Cardputer.Display;
d.setTextSize(2);
d.setTextColor(WHITE, BLACK);
d.print(”Speed: “);
if (isnan(wd.speedMps)) d.println(”N/A”);
else { d.print(mpsToMph(wd.speedMps), 1); d.println(” mph”); }
d.print(”Gust: “);
if (isnan(wd.gustMps)) d.println(”N/A”);
else { d.print(mpsToMph(wd.gustMps), 1); d.println(” mph”); }
d.print(”Dir: “);
if (isnan(wd.direction)) d.println(”N/A”);
else {
d.print((int)wd.direction);
d.print((char)247);
d.print(” “);
d.println(degToCompass(wd.direction));
}
}
// ================== ALARM ========================
void playWindAlarm() {
for (int i = 0; i < 8; i++) {
M5Cardputer.Speaker.tone(2200, 200);
delay(250);
M5Cardputer.Speaker.tone(900, 200);
delay(250);
}
}
void checkAndAlarm(const WindData& wd) {
double speedMph = mpsToMph(wd.speedMps);
if (!isnan(speedMph) && speedMph >= WIND_ALARM_THRESHOLD_MPH) {
playWindAlarm();
}
}
// ================== NWS FETCH ====================
bool fetchWindData(WindData& out) {
WiFiClientSecure client;
client.setInsecure();
String url = “/stations/” + String(stationId) + “/observations/latest”;
if (!client.connect(host, httpsPort)) return false;
String request =
String(”GET “) + url + “ HTTP/1.1\r\n” +
“Host: “ + host + “\r\n” +
“User-Agent: Cardputer-WindMonitor (youremail@example.com)\r\n” +
“Accept: application/geo+json\r\n” +
“Accept-Encoding: identity\r\n” +
“Connection: close\r\n\r\n”;
client.print(request);
unsigned long start = millis();
while (!client.available()) {
if (millis() - start > 7000) return false;
delay(10);
}
while (client.connected()) {
String line = client.readStringUntil(’\n’);
if (line.length() <= 2) break;
}
String payload;
while (client.available()) payload += client.readString();
client.stop();
StaticJsonDocument<16384> doc;
if (deserializeJson(doc, payload)) return false;
JsonObject props = doc[”properties”];
if (props.isNull()) return false;
out.speedMps = props[”windSpeed”][”value”] | NAN;
out.gustMps = props[”windGust”][”value”] | NAN;
out.direction = props[”windDirection”][”value”]| NAN;
out.valid = true;
return true;
}
// ================== WIFI CONNECT =================
void connectWiFi() {
drawHeader(”Connecting to WiFi...”);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 40) {
delay(500);
attempts++;
}
if (WiFi.status() == WL_CONNECTED) drawHeader(”WiFi Connected.”);
else drawHeader(”WiFi FAILED.”);
}
// ================== SETUP / LOOP =================
void setup() {
auto cfg = M5.config();
M5Cardputer.begin(cfg);
M5Cardputer.Display.setRotation(1);
M5Cardputer.Display.setTextWrap(true);
Serial.begin(115200);
delay(1000);
drawHeader(”Booting...”);
connectWiFi();
WindData wd;
if (WiFi.status() == WL_CONNECTED && fetchWindData(wd)) {
drawHeader(”Latest observation:”);
drawWindData(wd);
checkAndAlarm(wd);
} else {
drawHeader(”Error fetching data.”);
}
lastUpdate = millis();
}
void loop() {
M5Cardputer.update();
unsigned long now = millis();
if (now - lastUpdate >= UPDATE_INTERVAL) {
lastUpdate = now;
if (WiFi.status() != WL_CONNECTED) connectWiFi();
if (WiFi.status() == WL_CONNECTED) {
WindData wd;
if (fetchWindData(wd)) {
drawHeader(”Latest observation:”);
drawWindData(wd);
checkAndAlarm(wd);
} else {
drawHeader(”Error fetching data.”);
}
}
}
}
Wrapping Up
This project connects three eras of my personal tech journey:
2015: Building wind alerts on the Microsoft Band
2024: Rediscovering physical tinkering with the Cardputer
Today: Vibe coding practical, playful tools powered by real-world data
It’s a reminder that innovation doesn’t always come from big systems or ambitious roadmaps.
Sometimes it’s the small builds, the pocket-sized experiments, that reignite the spark.






