SmartPioneers DIY IOT Wind Direction Sensor

by hzqahusna in Circuits > Raspberry Pi

155 Views, 2 Favorites, 0 Comments

SmartPioneers DIY IOT Wind Direction Sensor

WhatsApp Image 2026-01-03 at 5.13.40 PM.jpeg

Hello! Today we are going to show you the DIY wind direction sensor that our group, SmartPioneer has done for the IoT Campus Environmental Monitoring System. The DIY sensor that we built also can measure temperature along with light intensity. All of the data is collected by a Micropython Pico W that sends the data via wifi then it will sent to thingspeak as an IoT analytics platform for collecting, analyzing, and acting on live data streams. Thingspeak allows devices to send sensor data via REST API. On the dashboard, we can see real-time data from the DIY wind direction sensor for example of the temperature and the light intensity graph. If this sounds difficult, don't worry, in this guide you can find all the information. To see more details about the construction, watch the video on YouTube.


Supplies

Supplies: The Smart Pioneer Toolkit

To build this optical weather station, our team combined standard DIY electronics with robust hardware to withstand the coastal environment. Here is exactly what we used to build the system:

The "Brain" & Electronics

  1. Raspberry Pi Pico W: The core controller, chosen for its ability to run MicroPython and connect to Wi-Fi.
  2. 4x IR Obstacle Avoidance Sensors: The eyes of our wind direction mechanism.
  3. Light Dependent Resistor (LDR): To measure sunlight intensity.
  4. 5V to 3.3V Level Shifters: Essential for protecting the Pico W (which is 3.3V logic) from the 5V signals coming from the sensors.
  5. Micro-USB Cable: For programming and power delivery.
  6. Breadboard & Jumper Wires: To organize the circuit inside the base.

The Mechanical "Tower" Structure

  1. Clear Plastic Storage Container: Serves as the heavy base and houses the microcontroller (the "Server Room").
  2. Blue IP65 Junction Box: Serves as the top compartment for the mechanical wind components.
  3. Metal Rod & Ball Bearing: Creates a low-friction axis for the wind vane to spin freely.
  4. 3D-Printed Arrow Vane: Custom-designed to catch the wind.
  5. The "Connector" (Shaft Coupler): A white plastic piece modified with Black Electrical Tape (crucial for our custom IR reflection logic).

Power & Tools

  1. Portable Power Bank: To power the station remotely during field testing.
  2. Soldering Iron: For securing connections on the level shifters and sensor pins.
  3. Hot Glue Gun: For waterproofing cable entry points and securing the sensors inside the lid.
  4. Power Drill: To create the shaft hole in the junction box and cable pass-throughs.
  5. Computer: Running Thonny IDE for coding and the ThingSpeak dashboard for monitoring.

The "Dual-Box" Structure & Base

Screenshot 2026-01-10 121027.png
WhatsApp Image 2025-12-18 at 9.23.58 PM.jpeg

Every weather station needs a stable foundation. Instead of a single heavy pole, Smart Pioneer opted for a modular "dual-box" tower design to keep our electronics organized and stable. For the base, we used a sturdy clear plastic storage container. This serves two purposes: it acts as a stable anchor for the station (which can be weighed down if needed), and it functions as the "server room," housing our microcontroller and breadboard safely away from the mechanical parts. On top of this base, we mounted a robust blue IP65 weatherproof junction box. This top box serves as the platform for the wind vane. We drilled a hole through the center of the blue lid to insert the rotating metal shaft. A 3D-printed arrow vane is friction-fitted to the top of this shaft, free to spin with the wind while the base remains rock solid.

The "Connector" & Black Tape Logic

WhatsApp Image 2026-01-03 at 5.13.41 PM.jpeg

The most critical part of our design was figuring out how to trigger the sensors accurately. We didn't use a standard encoder; we built a custom solution inside the blue box using a modified "Connector".

The Sensor Layout: Inside the blue box, we mounted four IR Obstacle Avoidance Sensors in a strict "Cross" (+) formation on the floor of the box. Each sensor faces inward toward the center, representing North, South, East, and West.

The "Connector" Modification: As shown in the image below, we attached a custom-shaped white plastic "connector" to the metal shaft that comes through the lid. This connector rotates as the wind vane spins.

The Physics of Reflection: We learned that IR sensors work by detecting reflected light. White surfaces reflect IR light well, while black surfaces absorb it. To get precise readings, we modified our white connector by wrapping specific sections in black electrical tape.

  1. White Surface: When the white part of the connector faces a sensor, it reflects the IR signal, triggering a "High" reading.
  2. Black Tape: When the taped section faces a sensor, the light is absorbed, preventing false triggers.

This simple "Tape & Plastic" tuning allowed us to calibrate exactly when the station registers a wind direction change without needing complex code.


Monitoring Light & System Health

While the blue box handles the wind, we needed to monitor the rest of the environment. We maximized our microcontroller's efficiency by adding a light sensor and using internal telemetry.

  1. Light Intensity (LDR): We wired a Light Dependent Resistor (LDR) into our circuit. By reading the analog values, the system detects if it is sunny, cloudy, or dark. This helps us correlate wind speed with sunlight intensity.
  2. Pico Internal Temperature: Rather than using external probes, we utilized the Raspberry Pi Pico W's onboard temperature sensor. This gives us a reliable reading of the ambient temperature inside the enclosure, acting as a system health check.

Wiring the "Brain"

WhatsApp Image 2026-01-08 at 9.06.07 AM.jpeg
WhatsApp Image 2026-01-10 at 3.17.04 PM (1).jpeg

The "brain" of our station—the Raspberry Pi Pico W—lives safely in the bottom clear container.

We used a breadboard to organize our connections. The challenge was routing the data cables from the four IR sensors in the top blue box down to the Pico in the bottom clear box. We carefully managed these wires to ensure they wouldn't snag as the boxes were stacked. This separation allows us to troubleshoot the code in the bottom box without disturbing the delicate sensor alignment in the top box.

Write the Code

Screenshot 2026-01-10 145236.png
Screenshot 2026-01-10 145245.png
Frontend Code (Dashboard):
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SmartPioneers - IoT Monitor</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
</head>

<body class="bg-slate-900 min-h-screen p-2 sm:p-4 md:p-6">
<div class="max-w-6xl mx-auto space-y-3 md:space-y-4">
<div class="bg-slate-800 border-2 border-slate-700 p-3 md:p-4">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-3 md:gap-0">
<div class="flex items-center gap-3 md:gap-4">
<img src="logo_iot.png" alt="SmartPioneers Logo" class="h-10 w-10 md:h-12 md:w-12 object-contain flex-shrink-0">
<div>
<h1 class="text-xl md:text-2xl font-bold text-amber-400 uppercase tracking-wider">SmartPioneers</h1>
<p class="text-xs md:text-sm text-slate-400 mt-1 font-mono">ENVIRONMENTAL MONITORING SYSTEM</p>
</div>
</div>
<div class="text-left md:text-right border-t-2 md:border-t-0 md:border-l-2 border-slate-700 pt-3 md:pt-0 md:pl-4">
<div class="text-xs text-slate-400 uppercase tracking-wider font-mono">System Mode</div>
<div id="connectionStatus" class="text-base md:text-lg font-mono font-bold text-slate-500 mt-1">CONNECTING...</div>
<div class="text-xs text-slate-400 uppercase tracking-wider font-mono mt-2">Last Cloud Read</div>
<div id="lastUpdate" class="text-sm font-mono font-bold text-green-400 mt-1">
<div id="lastUpdateDate">--</div>
<div id="lastUpdateTime" class="text-xs">--:--</div>
</div>
</div>
</div>
</div>

<div class="bg-slate-800 border-2 border-slate-700 p-3 md:p-4">
<div class="text-xs text-amber-400 uppercase tracking-wider mb-3 md:mb-4 font-mono font-bold border-b-2 border-slate-700 pb-2">WIND DIRECTION SENSOR</div>

<div class="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6">
<div class="bg-slate-900 border-2 border-slate-600 p-3 md:p-4">
<div class="text-xs text-slate-400 uppercase tracking-wider mb-3 md:mb-4 font-mono">Current Direction</div>
<div class="flex items-center justify-center mb-3 md:mb-4">
<div class="relative w-40 h-40 sm:w-48 sm:h-48 mx-auto">
<svg class="w-full h-full" viewBox="0 0 200 200">
<circle cx="100" cy="100" r="90" fill="none" stroke="currentColor" stroke-width="2" class="text-slate-600"/>
<circle cx="100" cy="100" r="70" fill="none" stroke="currentColor" stroke-width="1" class="text-slate-700"/>

<text x="100" y="25" text-anchor="middle" fill="currentColor" font-size="16" font-weight="bold" class="text-amber-400">N</text>
<text x="175" y="105" text-anchor="middle" fill="currentColor" font-size="16" font-weight="bold" class="text-amber-400">E</text>
<text x="100" y="185" text-anchor="middle" fill="currentColor" font-size="16" font-weight="bold" class="text-amber-400">S</text>
<text x="25" y="105" text-anchor="middle" fill="currentColor" font-size="16" font-weight="bold" class="text-amber-400">W</text>

<g id="windArrow" transform="rotate(0 100 100)" class="transition-transform duration-700 ease-in-out">
<line x1="100" y1="100" x2="100" y2="40" stroke="currentColor" stroke-width="4" stroke-linecap="round" class="text-green-400"/>
<polygon points="100,30 95,45 105,45" fill="currentColor" class="text-green-400"/>
</g>
</svg>
</div>
</div>
<div class="text-center border-t-2 border-slate-700 pt-3 md:pt-4">
<div class="text-3xl md:text-4xl font-mono font-bold text-green-400 mb-1" id="windDegrees">--°</div>
<div class="text-xs md:text-sm text-amber-400 font-mono uppercase" id="windCompass">--</div>
</div>
</div>

<div class="space-y-2 md:space-y-3">
<div class="bg-slate-900 border-2 border-slate-600 p-2 md:p-3">
<div class="flex items-center justify-between mb-2 gap-2">
<span class="text-xs text-slate-400 uppercase font-mono flex-shrink-0">Data Source</span>
<span id="vaneStatus" class="text-xs font-mono font-bold text-blue-400 px-2 py-1 bg-blue-900 border border-blue-600 whitespace-nowrap">CLOUD SYNC</span>
</div>
<p class="text-xs text-slate-500 font-mono">READING FROM THINGSPEAK</p>
</div>

<div class="bg-slate-900 border-2 border-slate-600 p-2 md:p-3">
<div class="flex items-center justify-between mb-2 gap-2">
<span class="text-xs text-slate-400 uppercase font-mono flex-shrink-0">Analog Output (Est)</span>
<span id="analogValue" class="text-lg md:text-xl font-mono font-bold text-amber-400 whitespace-nowrap">-- V</span>
</div>
<p class="text-xs text-slate-500 font-mono">MAPPED FROM DEGREES</p>
</div>

<div class="bg-slate-900 border-2 border-slate-600 p-2 md:p-3">
<div class="flex items-center justify-between mb-2 gap-2">
<span class="text-xs text-slate-400 uppercase font-mono flex-shrink-0">Calibration Status</span>
<span class="text-xs font-mono font-bold text-green-400 px-2 py-1 bg-green-900 border border-green-600 whitespace-nowrap">CALIBRATED</span>
</div>
<p class="text-xs text-slate-500 font-mono">0-360° RANGE MAPPED</p>
</div>
</div>
</div>
</div>

<div class="grid grid-cols-1 md:grid-cols-2 gap-3 md:gap-4">
<div class="bg-slate-800 border-2 border-slate-700 p-3 md:p-4">
<div class="text-xs text-slate-400 uppercase tracking-wider mb-2 font-mono">Temperature</div>
<div class="text-3xl md:text-4xl font-mono font-bold text-amber-400 mb-1">
<span id="airTemp">--</span><span class="text-xl md:text-2xl text-slate-500">°C</span>
</div>
<p class="text-xs text-slate-500 font-mono border-t-2 border-slate-700 pt-2 mt-2">DHT/DS18B20 SENSOR</p>
</div>

<div class="bg-slate-800 border-2 border-slate-700 p-3 md:p-4">
<div class="text-xs text-slate-400 uppercase tracking-wider mb-2 font-mono">Light Intensity</div>
<div class="text-3xl md:text-4xl font-mono font-bold text-yellow-300 mb-1">
<span id="lightLevel">--</span><span class="text-xl md:text-2xl text-slate-500">%</span>
</div>
<div class="w-full bg-slate-700 h-2 mt-2 rounded-full overflow-hidden">
<div id="lightBar" class="bg-yellow-400 h-full transition-all duration-300" style="width: 0%"></div>
</div>
<p class="text-xs text-slate-500 font-mono border-t-2 border-slate-700 pt-2 mt-2">LDR PHOTORESISTOR</p>
</div>
</div>

<div class="bg-slate-800 border-2 border-slate-700 p-3 md:p-4">
<div class="text-xs text-amber-400 uppercase tracking-wider mb-3 md:mb-4 font-mono font-bold border-b-2 border-slate-700 pb-2">LIVE DATA LOG</div>

<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 sm:gap-0 mb-3 md:mb-4">
<div class="text-xs text-slate-400 font-mono">Export</div>
<div class="flex gap-2 w-full sm:w-auto">
<button id="downloadCsvBtn" class="flex-1 sm:flex-none px-3 md:px-4 py-2.5 md:py-2 bg-amber-500 text-slate-900 text-xs md:text-sm font-bold uppercase font-mono hover:bg-amber-600 active:bg-amber-700 transition-colors border-2 border-amber-400 touch-manipulation">
<span class="hidden sm:inline">Download CSV (Last 7 Days)</span>
<span class="sm:hidden">Download CSV</span>
</button>
</div>
</div>

<div class="grid grid-cols-1 lg:grid-cols-2 gap-3 md:gap-4">
<div class="bg-slate-900 border-2 border-slate-600 p-3 md:p-4 lg:col-span-2">
<div class="text-xs text-slate-400 uppercase tracking-wider mb-2 font-mono">Wind Direction (Degrees)</div>
<div style="height: 180px; min-height: 180px;" class="md:h-[200px]">
<canvas id="windDirectionChart"></canvas>
</div>
</div>

<div class="bg-slate-900 border-2 border-slate-600 p-3 md:p-4">
<div class="text-xs text-slate-400 uppercase tracking-wider mb-2 font-mono">Temperature (°C)</div>
<div style="height: 180px; min-height: 180px;" class="md:h-[200px]">
<canvas id="airTempChart"></canvas>
</div>
</div>

<div class="bg-slate-900 border-2 border-slate-600 p-3 md:p-4">
<div class="text-xs text-slate-400 uppercase tracking-wider mb-2 font-mono">Light Intensity (%)</div>
<div style="height: 180px; min-height: 180px;" class="md:h-[200px]">
<canvas id="lightChart"></canvas>
</div>
</div>
</div>
</div>

<div class="bg-slate-800 border-2 border-slate-700 p-3 md:p-4">
<div class="text-xs text-amber-400 uppercase tracking-wider mb-3 md:mb-4 font-mono font-bold border-b-2 border-slate-700 pb-2">Course Information</div>
<div class="text-xs md:text-sm font-bold text-slate-300 mb-4 md:mb-6 font-mono">CSM 3313 - INTERNET OF THINGS<br><span class="text-xs text-slate-500">SEMESTER I, 2025/26 SESSION</span></div>

<div class="text-xs text-amber-400 uppercase tracking-wider mb-3 md:mb-4 font-mono font-bold border-t-2 border-slate-700 pt-3 md:pt-4">Team Members</div>
<div class="space-y-2 text-xs text-slate-300 font-mono">
<div class="flex flex-col sm:flex-row sm:items-start gap-2 sm:gap-4 bg-slate-900 border border-slate-600 p-2 sm:p-2">
<span class="font-bold text-amber-400 sm:w-20 sm:flex-shrink-0">S72424</span>
<span class="font-light flex-1 break-words">MUHAMMAD FAOZAN ZIKRY BIN MOHD RIZAL</span>
</div>
<div class="flex flex-col sm:flex-row sm:items-start gap-2 sm:gap-4 bg-slate-900 border border-slate-600 p-2 sm:p-2">
<span class="font-bold text-amber-400 sm:w-20 sm:flex-shrink-0">S72370</span>
<span class="font-light flex-1 break-words">WAN MUHAMMAD ADIB ARSYAD BIN WAN MUHAMMAD</span>
</div>
<div class="flex flex-col sm:flex-row sm:items-start gap-2 sm:gap-4 bg-slate-900 border border-slate-600 p-2 sm:p-2">
<span class="font-bold text-amber-400 sm:w-20 sm:flex-shrink-0">S70383</span>
<span class="font-light flex-1 break-words">NURFAULINA ALIA BINTI BURHANUD-DIN</span>
</div>
<div class="flex flex-col sm:flex-row sm:items-start gap-2 sm:gap-4 bg-slate-900 border border-slate-600 p-2 sm:p-2">
<span class="font-bold text-amber-400 sm:w-20 sm:flex-shrink-0">S70704</span>
<span class="font-light flex-1 break-words">NUR HAZIQAH HUSNA BINTI AJI</span>
</div>
<div class="flex flex-col sm:flex-row sm:items-start gap-2 sm:gap-4 bg-slate-900 border border-slate-600 p-2 sm:p-2">
<span class="font-bold text-amber-400 sm:w-20 sm:flex-shrink-0">S72404</span>
<span class="font-light flex-1 break-words">MUHAMMAD NASRUL AMIN BIN ZAIDY</span>
</div>
</div>
</div>
</div>

<script>
// ==========================================
// --- 1. CONFIGURATION ---
// ==========================================
const TS_CHANNEL_ID = '3217474';
const TS_API_KEY = '3ARQZGPIHLR3MK9L'; // Read Key

// Check for new data every 15 minutes (only updates if new data is found)
const UPDATE_INTERVAL = 900000; // 15 minutes = 15 * 60 * 1000 milliseconds

// ==========================================
// --- 2. DATA STORAGE & CHARTS ---
// ==========================================
const maxDataPoints = 30;
const timeLabels = [];
const airTempData = [];
const lightData = [];
const windDirectionData = [];
let lastEntryId = null; // Track last ThingSpeak entry ID to detect new data

const chartConfig = {
type: 'line',
options: {
responsive: true, maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
x: { ticks: { color: '#94a3b8', font: { family: 'monospace', size: 10 } }, grid: { color: '#334155', drawBorder: false } },
y: { ticks: { color: '#94a3b8', font: { family: 'monospace', size: 10 } }, grid: { color: '#334155', drawBorder: false } }
}
}
};

const airTempChart = new Chart(document.getElementById('airTempChart'), { ...chartConfig, data: { labels: timeLabels, datasets: [{ label: 'Temp', data: airTempData, borderColor: '#fbbf24', backgroundColor: 'rgba(251, 191, 36, 0.1)', borderWidth: 2, fill: true }] } });
const lightChart = new Chart(document.getElementById('lightChart'), { ...chartConfig, data: { labels: timeLabels, datasets: [{ label: 'Light', data: lightData, borderColor: '#fde047', backgroundColor: 'rgba(253, 224, 71, 0.1)', borderWidth: 2, fill: true }] } });
const windDirectionChart = new Chart(document.getElementById('windDirectionChart'), { ...chartConfig, data: { labels: timeLabels, datasets: [{ label: 'Wind', data: windDirectionData, borderColor: '#4ade80', backgroundColor: 'rgba(74, 222, 128, 0.1)', borderWidth: 2, fill: true, pointRadius: 2 }] }, options: { ...chartConfig.options, scales: { ...chartConfig.options.scales, y: { min: 0, max: 360 } } } });

// ==========================================
// --- 3. HELPER FUNCTIONS ---
// ==========================================
function degreesToCompass(degrees) {
const directions = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'];
return directions[Math.round(degrees / 22.5) % 16] || "--";
}

function showStatus(message, type = 'info') {
console.log(`Status: ${message.toUpperCase()}`);
}

// ==========================================
// --- 4. DATA FETCH & SEND LOGIC ---
// ==========================================

// --- FETCH HISTORICAL DATA ON PAGE LOAD ---
async function fetchHistoricalData() {
try {
// Fetch last N entries from ThingSpeak (matching maxDataPoints)
const url = `https://api.thingspeak.com/channels/${TS_CHANNEL_ID}/feeds.json?api_key=${TS_API_KEY}&results=${maxDataPoints}`;
const response = await fetch(url);
if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); }
const data = await response.json();

if (data.feeds && data.feeds.length > 0) {
// Clear existing data
timeLabels.length = 0;
airTempData.length = 0;
lightData.length = 0;
windDirectionData.length = 0;

// Process historical data in chronological order
data.feeds.forEach(feed => {
const t = parseFloat(feed.field1 || 0);
const l = parseFloat(feed.field2 || 0);
const w = parseFloat(feed.field3 || 0);

if (feed.created_at) {
const feedTime = new Date(feed.created_at);
const timeStr = `${String(feedTime.getHours()).padStart(2,'0')}:${String(feedTime.getMinutes()).padStart(2,'0')}`;

timeLabels.push(timeStr);
airTempData.push(t);
lightData.push(l);
windDirectionData.push(w);
}
});

// Set the last entry ID to track new data and update dashboard with latest values
if (data.feeds.length > 0) {
const latestFeed = data.feeds[data.feeds.length - 1];
lastEntryId = latestFeed.entry_id;

// Update dashboard with latest historical data
const tempReading = latestFeed.field1;
const lightReading = latestFeed.field2;
const windReading = latestFeed.field3;

updateDashboardWithRealData(tempReading, lightReading, windReading, false, latestFeed.created_at);
}

// Update charts with historical data
airTempChart.update();
lightChart.update();
windDirectionChart.update();
}
} catch (error) {
console.error("Historical Data Fetch Error:", error);
}
}

// --- READING DATA ---
async function fetchThingSpeakData() {
const statusDiv = document.getElementById('connectionStatus');
try {
const url = `https://api.thingspeak.com/channels/${TS_CHANNEL_ID}/feeds/last.json?api_key=${TS_API_KEY}`;
const response = await fetch(url);
if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); }
const data = await response.json();

// Check if this is new data by comparing entry_id
const currentEntryId = data.entry_id;
const isNewData = lastEntryId === null || currentEntryId !== lastEntryId;

// Mappings
const tempReading = data.field1;
const lightReading = data.field2;
const windReading = data.field3;

// Always update dashboard values, but only add to charts if new data
updateDashboardWithRealData(tempReading, lightReading, windReading, isNewData, data.created_at);

// Update last entry ID if this is new data
if (isNewData) {
lastEntryId = currentEntryId;
}

statusDiv.textContent = "ONLINE / SYNCED";
statusDiv.className = "text-lg font-mono font-bold text-green-400 mt-1";
} catch (error) {
console.error("Fetch Error:", error);
statusDiv.textContent = "OFFLINE / ERROR";
statusDiv.className = "text-lg font-mono font-bold text-red-400 mt-1";
}
}

// ==========================================
// --- 5. UI UPDATE LOGIC ---
// ==========================================
function updateDashboardWithRealData(temp, light, windDir, isNewData = false, thingSpeakTimestamp = null) {
const t = parseFloat(temp || 0);
const l = parseFloat(light || 0);
const w = parseFloat(windDir || 0);

// Always update current values
document.getElementById('airTemp').textContent = t.toFixed(1);
document.getElementById('lightLevel').textContent = parseInt(l);
document.getElementById('lightBar').style.width = Math.min(l, 100) + '%';
document.getElementById('windDegrees').textContent = Math.round(w) + '°';
document.getElementById('windCompass').textContent = degreesToCompass(w);
document.getElementById('analogValue').textContent = (w / 360 * 5).toFixed(2) + " V";
document.getElementById('windArrow').setAttribute('transform', `rotate(${w} 100 100)`);

// Use ThingSpeak timestamp if available, otherwise use current time
let updateTime;
if (thingSpeakTimestamp) {
updateTime = new Date(thingSpeakTimestamp);
} else {
updateTime = new Date();
}

document.getElementById('lastUpdateDate').textContent = updateTime.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
document.getElementById('lastUpdateTime').textContent = `${String(updateTime.getHours()).padStart(2,'0')}:${String(updateTime.getMinutes()).padStart(2,'0')}`;

// Only add to charts if this is new data from ThingSpeak
if (isNewData) {
const timeStr = `${String(updateTime.getHours()).padStart(2,'0')}:${String(updateTime.getMinutes()).padStart(2,'0')}`;
if(timeLabels.length > maxDataPoints) {
timeLabels.shift(); airTempData.shift(); lightData.shift(); windDirectionData.shift();
}
timeLabels.push(timeStr);
airTempData.push(t);
lightData.push(l);
windDirectionData.push(w);
airTempChart.update();
lightChart.update();
windDirectionChart.update();
}
}

// ==========================================
// --- 6. EVENT LISTENERS (BUTTONS) ---
// ==========================================

// CSV Export - Last 7 Days from ThingSpeak
document.getElementById('downloadCsvBtn').addEventListener('click', async () => {
showStatus('FETCHING DATA FROM THINGSPEAK...', 'info');
try {
// Fetch last 7 days of data from ThingSpeak
const url = `https://api.thingspeak.com/channels/${TS_CHANNEL_ID}/feeds.json?api_key=${TS_API_KEY}&days=7`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();

if (!data.feeds || data.feeds.length === 0) {
showStatus('NO DATA FOUND FOR LAST 7 DAYS', 'error');
return;
}

// Generate CSV rows - Only temp, light, and wind
const rows = [];
rows.push(['timestamp','date','Temperature_C','Light_Percent','Wind_Degrees'].join(','));

data.feeds.forEach(feed => {
if (feed.created_at) {
const feedDate = new Date(feed.created_at);
const dateStr = feedDate.toISOString().split('T')[0];
const timeStr = feedDate.toLocaleTimeString('en-US', { hour12: false });
const timestamp = `${dateStr} ${timeStr}`;

const temp = parseFloat(feed.field1 || 0).toFixed(1);
const light = parseFloat(feed.field2 || 0).toFixed(0);
const wind = parseFloat(feed.field3 || 0).toFixed(0);

rows.push([timestamp, dateStr, temp, light, wind].join(','));
}
});

// Create and download CSV
const blob = new Blob([rows.join('\n')], { type: 'text/csv' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
const dateStr = new Date().toISOString().split('T')[0];
a.download = `smart_pioneers_iot_reading_export_${dateStr}.csv`;
a.click();

showStatus(`CSV EXPORTED: ${data.feeds.length} RECORDS`, 'success');
} catch (error) {
console.error("CSV Export Error:", error);
showStatus('FAILED TO EXPORT CSV', 'error');
}
});

// --- STARTUP ---
// First load historical data, then start monitoring for new data
fetchHistoricalData().then(() => {
fetchThingSpeakData(); // Get latest values for dashboard
});
setInterval(fetchThingSpeakData, UPDATE_INTERVAL);

</script>
</body>
</html>

Below shows the step on how to do the dashboard:


STEP 1: SET UP HTML FILE

1.1 Create index.html file

1.2 Add required libraries in <head>:

- Tailwind CSS (for styling)

- Chart.js (for graphs)

1.3 Create basic page structure with dark theme background

STEP 2: CREATE DASHBOARD SECTIONS

2.1 HEADER SECTION

- Add logo image

- Add "SmartPioneers" title

- Add connection status indicator

- Add "Last Cloud Read" timestamp display

2.2 WIND DIRECTION SENSOR SECTION

- Create compass visualization (SVG)

- Add wind direction display (degrees)

- Add compass direction text (N, NE, E, etc.)

- Add analog voltage estimate display

- Add data source indicator

2.3 TEMPERATURE & LIGHT CARDS

- Temperature display card

- Light intensity display card with progress bar

2.4 LIVE DATA LOG SECTION

- Add "Download CSV" button

- Add three chart containers (canvas elements):

* Wind Direction chart

* Temperature chart

* Light Intensity chart

2.5 COURSE INFORMATION SECTION

- Add course details

- Add team members list

STEP 3: CONFIGURE THINGSPEAK CONNECTION

3.1 Get your ThingSpeak credentials:

- Channel ID (e.g., 3217474)

- Read API Key (for reading data)

3.2 Set up configuration variables:

- Store Channel ID

- Store Read API Key

- Set update interval (15 minutes = 900000 milliseconds)

3.3 IMPORTANT: Verify your ThingSpeak field mappings:

- Field 1 = Temperature (°C)

- Field 2 = Light Intensity (%)

- Field 3 = Wind Direction (degrees, 0-360)

STEP 4: IMPLEMENT DATA READING FROM THINGSPEAK

This is the CORE functionality - how the dashboard gets data from ThingSpeak.

4.1 LOAD HISTORICAL DATA (On Page Load)

Purpose: Show past data when dashboard first opens

Process:

1. Call ThingSpeak API to get last 30 entries

2. API Endpoint:

/channels/{CHANNEL_ID}/feeds.json?api_key={KEY}&results=30

3. Parse the response (JSON format)

4. Extract temperature, light, and wind from each entry

5. Extract timestamp from each entry

6. Format time as HH:MM (no seconds)

7. Add all data points to chart arrays

8. Store the last entry_id to track new data

9. Update all three charts with historical data

Key Point: This gives users immediate context when they open the dashboard.

4.2 CHECK FOR NEW DATA (Every 15 Minutes)

Purpose: Update dashboard with latest sensor readings

Process:

1. Call ThingSpeak API to get ONLY the latest entry

2. API Endpoint: /channels/{CHANNEL_ID}/feeds/last.json?api_key={KEY}

3. Get the entry_id from response

4. Compare with stored lastEntryId

5. If entry_id is different = NEW DATA

6. If entry_id is same = NO NEW DATA (skip chart update)

7. Always update dashboard displays (current values)

8. Only add to charts if it's genuinely new data

9. Update lastEntryId if new data found

CRITICAL: Entry ID Tracking

- ThingSpeak assigns unique entry_id to each data point

- By comparing entry_id, we prevent duplicate data in charts

- Dashboard always shows latest values, but charts only grow with NEW data

4.3 UPDATE DASHBOARD DISPLAY

When new data arrives:

1. Parse temperature, light, and wind values

2. Update all display elements:

- Temperature number

- Light percentage and progress bar

- Wind direction degrees

- Compass direction (N, NE, E, etc.)

- Analog voltage (calculated: wind/360 * 5)

- Rotate compass arrow to match direction

- Update "Last Cloud Read" timestamp

3. If new data: Add to chart arrays and update charts

4. If not new: Skip chart update (prevents duplicates)

STEP 5: SET UP CHARTS

5.1 Initialize Chart.js

5.2 Create three line charts:

- Temperature chart (amber/yellow color)

- Light chart (yellow color)

- Wind Direction chart (green color, y-axis 0-360)

5.3 Configure charts:

- Responsive design

- Dark theme colors

- No legend

- Show grid lines

- Limit to 30 data points (remove oldest when exceeded)

5.4 Connect charts to data arrays:

- X-axis: Time labels (HH:MM format)

- Y-axis: Sensor values

STEP 6: CSV EXPORT FUNCTIONALITY

6.1 When user clicks "Download CSV" button:

6.2 Process:

1. Fetch last 7 days of data from ThingSpeak

2. API Endpoint: /channels/{CHANNEL_ID}/feeds.json?api_key={KEY}&days=7

3. Generate CSV format:

- Header row: timestamp, date, Temperature_C, Light_Percent, Wind_Degrees

- Data rows: One per feed entry

4. Format each row:

- Full timestamp (date + time)

- Date only

- Temperature (1 decimal place)

- Light (whole number)

- Wind (whole number)

5. Create downloadable file

6. Filename: smart_pioneers_iot_reading_export_YYYY-MM-DD.csv

7. Trigger browser download

STEP 7: STARTUP SEQUENCE

When page loads:

1. Initialize all three charts (empty at first)

2. Load historical data (last 30 entries)

3. Populate charts with historical data

4. Get latest current values

5. Update all dashboard displays

6. Start periodic checking (every 15 minutes)

Periodic Updates:

- Every 15 minutes: Check ThingSpeak for new data

- Only update charts if new data found

- Always update current value displays


THINGSPEAK API ENDPOINTS USED

1. GET LAST ENTRY (for periodic updates)

URL: https://api.thingspeak.com/channels/{CHANNEL_ID}/feeds/last.json?api_key={API_KEY}


Returns: Single most recent entry

Used: Every 15 minutes to check for new data

2. GET HISTORICAL DATA (for initial load)

URL: https://api.thingspeak.com/channels/{CHANNEL_ID}/feeds.json?api_key={API_KEY}&results=30


Returns: Last 30 entries

Used: Once on page load to populate charts

3. GET LAST 7 DAYS (for CSV export)

URL: https://api.thingspeak.com/channels/{CHANNEL_ID}/feeds.json?api_key={API_KEY}&days=7


Returns: All entries from last 7 days

Used: When user clicks CSV export button

Response format {
"feeds": [
{
"entry_id": 123, // Unique ID - used to detect new data
"created_at": "2025-01-15T14:30:00Z", // Timestamp from ThingSpeak
"field1": "25.5", // Temperature
"field2": "75", // Light
"field3": "180" // Wind Direction
}
]
}



Backend Code(Thonny) is in the attached file below:

Downloads

Data Logic (From Binary to Degrees)

Screenshot 2026-01-10 151331.png
Screenshot 2026-01-10 151341.png
Screenshot 2026-01-10 151348.png

One specific challenge we faced was translating the raw signals from our hardware into meaningful data for the cloud. Our IR sensors operate on digital logic, outputting simple "0" or "1" signals.

We couldn't just send these raw binary bits directly. We wrote a specific class in our Python code, WindVane, to interpret these signals in three stages:

  1. Combination Logic: The code doesn't just check one sensor at a time. It checks for pairs. For example, if both the North (Pin 12) and East (Pin 13) sensors are triggered simultaneously, the system intelligently identifies this as "North-East". This allows us to get 8 directions (N, NE, E, SE, etc.) from just 4 sensors.
  2. Degrees Conversion: ThingSpeak graphs work best with numbers, not text. So, we created a mapping dictionary (wind_map) that converts the text direction into degrees (e.g., North = 0, East = 90, South = 180).
  3. Dominant Wind Calculation: Instead of uploading every tiny fluctuation, our code collects readings into a list (wind_list) over 30 minutes. Before uploading, it calculates the "Dominant Wind"—the direction that appeared most frequently during that period.

The "Blackout" Solution

During initial testing, we faced a physics problem: sunlight is essentially giant IR interference! Even inside the blue box, strong daylight could confuse the sensors.

To solve this, we ensured the top blue box was strictly light-sealed. However, our LDR (Light Sensor) needs light. We solved this by positioning the LDR near a transparent section of the enclosure or a dedicated window. That is why we drilled the hole just nicely for the wire connector of the LDR sensor to detect the sunlight , while keeping the wind sensors in the dark. This ensures the wind sensors only see the IR light they emit, while the light sensor sees the sun.

Deployment & Testing

Screenshot 2026-01-10 125227.png
WhatsApp Image 2026-01-03 at 5.13.40 PM.jpeg

We deployed the Smart Pioneer station for a field test. The wide base of the bottom storage box proved very stable on the grass, especially with the added weight of the bricks.

The modified connector worked perfectly. Because we used black tape to fine-tune the reflection, the sensors were responsive and didn't "stutter" between directions. We verified the alignment with a compass and watched as the data started flowing to our dashboard.

What We Gained From the Project

WhatsApp Image 2026-01-10 at 3.17.04 PM.jpeg
WhatsApp Image 2026-01-08 at 9.06.07 AM (1).jpeg

At the end of our field deployment, this project gave us much more than just data points on a screen. It demonstrated how a complete engineering system is built from scratch—starting with a plastic storage box and a roll of electrical tape, moving through complex Python logic, and ending with a cloud ecosystem that serves both our custom dashboard and our lecturer's server simultaneously.

Seeing the Wind Direction (calculated from our custom "0 & 1" logic), Light Intensity, and Internal Temperature appear vividly on our web interface made the challenges of the build truly worth it. It proved that we didn't need expensive industrial sensors to get results; we just needed smart engineering.

This project also taught the Smart Pioneer team invaluable lessons about persistence and adaptability. We didn't just assemble parts; we had to solve real-world physics problems. When sunlight confused our sensors, we engineered a blackout solution. When the raw binary data was messy, we wrote code to calculate the "Dominant Wind." We experienced the reality of field work—managing battery life, troubleshooting Wi-Fi stability, and ensuring our "dual-box" tower stood firm against the coastal breeze.

In the end, Smart Pioneer didn't just build a weather station; we built a bridge between hardware and software. We learned to divide tasks, trust each other's code, and iterate on our design until it worked. That shared achievement—watching our station "talk" to the cloud from the beach—is the true success we will carry into our future engineering careers.