mars time 🛸
Using nasa's InSight lander data to predict the weather on mars.

what i had to do
Design and prototype an Internet-connected IoT device capable of monitoring environmental conditions over time.
Requirements:
- Utilize at least two distinct sensors to capture data.Deploy the device to collect a dataset spanning one week (7 days).
- Visualize the collected data to reveal trends or patterns.
- Document the system architecture, data flow, and final visualization.
what i did
To be fully transparent, I was SLAMMED with a client project that ate into me fully being able to enjoy this project. As a result, I asked Maxim (hi maxim) if I could using publicly available data through API's to create a sketch, and woo hoo he said yes!
Das Konzept
I've been in my Star Trek revival era so of course I had to do something space related. and also because Maxim said NASA has no shortage of public API data. So I wanted to make a mars weather prediction model from the InSight lander expedition from 2018.
When looking at NASA's API database, I found the InSight lander API but... the lander died in 2022. It's 2025. So I technically wasn't going to be getting any new data. Let's rethink this.

I'm stubborn so this minor setback wouldn't stop me. I let Maxim know the sad death of the InSight lander, and planned to make a weather prediction model based off the data it collected from its long fulfilling life between 2018-2022. Soooo... I made a Climatological Predictive Model based on the data I did have.

heres da code
The code for P5.js
I know you're not gonna read it all. I didn't. But it worked!
// --- CONFIGURATION ---
let myLat = 34.0522; // Default LA
let myLon = -118.2437;
let locationName = "Current location";
// MARS CONSTANTS
const MARS_LONGITUDE = 135.6;
const REFERENCE_JD = 2451545.0;
// --- GLOBAL VARIABLES ---
let weatherLA;
let marsData;
let rainParticles = [];
let marsDust = [];
let sunRays = [];
let noiseOffset = 0; // For cloud animation
function setup() {
createCanvas(windowWidth, windowHeight);
textAlign(CENTER, CENTER);
noStroke();
// Init Particle Systems
setupVisuals();
// Init Mars Physics
marsData = calculateMarsWeather();
// --- GPS CHECK ---
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(gotPosition, positionError);
} else {
fetchWeather(myLat, myLon);
}
}
function draw() {
background(0);
// --- EARTH ---
push();
clip(() => { rect(0, 0, width/2, height); });
if (weatherLA) {
drawEarthAtmosphere(weatherLA.code, weatherLA.isDay);
} else {
background(20, 30, 40); // Dark loading state
}
pop();
// --- MARS ---
push();
clip(() => { rect(width/2, 0, width/2, height); });
let marsHour = parseInt(marsData.lmst.split(":")[0]);
let isMarsDay = (marsHour > 6 && marsHour < 18);
drawMarsAtmosphere(isMarsDay);
pop();=
// Vignette
let ctx = drawingContext;
let grd = ctx.createRadialGradient(width/2, height/2, height/4, width/2, height/2, height);
grd.addColorStop(0, "rgba(0,0,0,0)");
grd.addColorStop(1, "rgba(0,0,0,0.8)");
ctx.fillStyle = grd;
rect(width/2, height/2, width, height);
// Center Divider
stroke(255, 50);
strokeWeight(2);
line(width/2, 0, width/2, height);
noStroke();
fill(255);
textFont('Courier New');
// EARTH HUD
if (weatherLA) {
textSize(14);
text("LIVE FEED // " + locationName.toUpperCase(), width * 0.25, height * 0.1);
textSize(120);
textStyle(BOLD);
text(Math.round(weatherLA.temp) + "°", width * 0.25, height * 0.45);
textStyle(NORMAL);
textSize(24);
text(weatherLA.desc.toUpperCase(), width * 0.25, height * 0.6);
// Wind/Humidity
drawStatBar(width*0.15, height*0.7, weatherLA.wind, 20, "WIND");
drawStatBar(width*0.35, height*0.7, 40, 100, "HUM");
} else {
textSize(16);
text("ESTABLISHING UPLINK...", width * 0.25, height/2);
}
// MARS HUD
textSize(14);
text("PREDICTIVE MODEL // ELYSIUM PLANITIA", width * 0.75, height * 0.1);
textSize(120);
textStyle(BOLD);
let tColor = map(marsData.tempF, -100, 30, 100, 255);
fill(255, tColor, tColor);
text(Math.round(marsData.tempF) + "°", width * 0.75, height * 0.45);
textStyle(NORMAL);
fill(255);
textSize(24);
text(marsData.season, width * 0.75, height * 0.6);
// Mars Time
textSize(16);
text("LMST: " + marsData.lmst + " // SOL: " + Math.round(marsData.Ls), width * 0.75, height * 0.75);
}
function setupVisuals() {
// Rain
for (let i = 0; i < 300; i++) {
rainParticles.push({
x: random(width/2),
y: random(-height, height),
speed: random(15, 25),
len: random(10, 30),
alpha: random(50, 200)
});
}
// Mars Dust Layers
for (let i = 0; i < 400; i++) {
marsDust.push({
x: random(width/2, width),
y: random(height),
size: random(2, 6),
speed: random(0.2, 2),
layer: random(0, 1) // Depth
});
}
}
function drawEarthAtmosphere(code, isDay) {
// WMO Codes: 0=Clear, 1-3=Cloudy, 45+=Fog, 50+=Rain
if (code <= 3 && isDay) {
// --- SUNNY RAYS ---
background(40, 120, 220); // Deep Sky Blue
// Draw Sun
fill(255, 255, 200);
ellipse(width * 0.1, height * 0.15, 120, 120);
// Rotating Sun Rays
push();
translate(width * 0.1, height * 0.15);
rotate(frameCount * 0.002);
for (let i = 0; i < 12; i++) {
rotate(PI / 6);
fill(255, 255, 255, 20); // Very transparent
triangle(0, 0, 100, 1000, -100, 1000);
}
pop();
// Light clouds if code > 0
if(code > 0) drawNoiseClouds(255, 100);
} else if (code <= 48 && !isDay) {
// --- CLEAR NIGHT ---
background(10, 15, 30);
// Stars would go here, but let's keep it clean
} else if (code >= 45 && code < 51) {
// --- FOG / CLOUDY ---
background(100, 110, 120);
drawNoiseClouds(200, 150);
} else if (code >= 51) {
// --- RAIN / STORM ---
background(30, 35, 40);
// Rain System
strokeWeight(2);
for (let p of rainParticles) {
stroke(150, 180, 255, p.alpha);
line(p.x, p.y, p.x, p.y + p.len);
p.y += p.speed;
if (p.y > height) {
p.y = random(-50, 0);
p.x = random(width/2);
}
}
noStroke();
} else {
background(50);
}
}
function drawMarsAtmosphere(isDay) {
if (isDay) {
background(160, 80, 40); // Butterscotch Sky
// Sun Glare
fill(255, 220, 180, 50);
ellipse(width*0.7, height*0.2, 200, 200);
} else {
background(40, 10, 5); // Deep rusty dark
}
// --- DUST STORM ---
for (let d of marsDust) {
fill(200, 160, 120, map(d.layer, 0, 1, 50, 150));
ellipse(d.x, d.y, d.size * d.layer); // Further ones are smaller
d.x -= d.speed; // Wind blows left
// Wrap around
if (d.x < width/2) d.x = width;
}
}
function drawNoiseClouds(brightness, alphaVal) {
// Perlin noise for 'clouds'
fill(brightness, alphaVal);
for (let x = 0; x < width/2; x += 10) {
let n = noise(x * 0.01, noiseOffset);
let h = map(n, 0, 1, 0, 200);
rect(x, 0, 10, h);
if(brightness < 200) rect(x, height-h, 10, h);
}
}
function drawStatBar(x, y, val, maxVal, label) {
let w = 80;
let h = 5;
fill(255, 50);
rect(x, y, w, h); // BG
fill(255);
let barW = map(val, 0, maxVal, 0, w, true);
rect(x - (w-barW)/2, y, barW, h);
textSize(10);
text(label, x, y + 15);
}
function gotPosition(position) {
myLat = position.coords.latitude;
myLon = position.coords.longitude;
fetchWeather(myLat, myLon);
}
function positionError(err) {
console.log("GPS Blocked. Using default.");
fetchWeather(myLat, myLon);
}
function fetchWeather(lat, lon) {
// Added "timezone=auto" to get correct day/night cycle
let url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t_weather=true&temperature_unit=fahrenheit&windspeed_unit=mph&timezone=auto`;
loadJSON(url, gotEarth);
}
function gotEarth(data) {
let code = data.current_weather.weathercode;
weatherLA = {
temp: data.current_weather.temperature,
wind: data.current_weather.windspeed,
code: code,
isDay: data.current_weather.is_day === 1,
desc: getWMODescription(code)
};
}
function calculateMarsWeather() {
let now = new Date();
let JD = (now.getTime() / 86400000) + 2440587.5;
let D = JD - REFERENCE_JD;
let M = (19.3870 + 0.52402075 * D) % 360;
let alpha = (270.3863 + 0.52403840 * D) % 360;
// Use p5's built-in radians() implicitly here or Math.PI conversion
let Ls = (alpha + (10.691 + 3.0e-7 * D) * Math.sin(M * (Math.PI/180))) % 360;
let msd = (JD - 2405522.0028779) / 1.027491252;
let mtc = (24 * msd) % 24;
let localTimeHours = (mtc + (MARS_LONGITUDE / 15)) % 24;
if (localTimeHours < 0) localTimeHours += 24;
let h = Math.floor(localTimeHours);
let m = Math.floor((localTimeHours - h) * 60);
let seasonBias = 10 * Math.sin((Ls - 90) * (Math.PI/180));
let hourAngle = (localTimeHours - 14) * (360/24);
let dailyVariation = 40 * Math.cos(hourAngle * (Math.PI/180));
let simulatedTempC = -60 + seasonBias + dailyVariation;
simulatedTempC += noise(frameCount * 0.01) * 2;
let seasonName = "N. WINTER";
if (Ls >= 0 && Ls < 90) seasonName = "N. SPRING";
else if (Ls >= 90 && Ls < 180) seasonName = "N. SUMMER";
else if (Ls >= 180 && Ls < 270) seasonName = "N. AUTUMN";
return {
tempF: (simulatedTempC * 9/5) + 32,
season: seasonName,
lmst: nf(h, 2) + ":" + nf(m, 2),
Ls: Ls
};
}
function getWMODescription(code) {
if (code === 0) return "Clear Sky";
if (code >= 1 && code <= 3) return "Partly Cloudy";
if (code >= 45 && code <= 48) return "Foggy";
if (code >= 51 && code <= 67) return "Rain";
if (code >= 71 && code <= 77) return "Snow";
if (code >= 80) return "Showers";
if (code >= 95) return "Thunderstorm";
return "Unknown";
}
function windowResized() {
resizeCanvas(windowWidth, windowHeight);
rainParticles = []; marsDust = [];
setupVisuals();
}