mars time 🛸

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

Year:
2025
Tools:
NASA API & P5.js

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.

One problem.

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.

Ok i'm pivoting!

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}&current_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();
}

how it came out