Solar calculation on ledstrip

Hi There,

I'am quite new with programming of the Arduino, however I get pretty far by using the AI tools. unfortunattely it seems that i reach the limit where they can help me, so I am looking for some human intelligence that is open to help me.

I am working on a project, in the project I use an ESP32 devkit1 that I program via the Arduino IDE. Here I will command an SK6812 adressable led strip. The goal is to adres every led to an "place in the world" via longitude and latitude coordinates. Then the solar engine should calculate the angle of the sun related to the horizon on that place and via that it must defined the state of the specific led, "Dusk", "sunrise", "dawn", "Sunset". during Day the led should be off and during night the led should be on. during the twilights there is a fase of fading.
This will result in a led strip that will be a live corresponding to the day night cycles as in the real world.
For so far this worked quite in the good direction. However it seems that it has some problem due to the datum line and refresh the date. For example it is now 20:44 in Amsterdam and led 0 is still on, where it should be off because in Sydney the new day is allready started.

Is there some one that allready made something like this or will take the challenge to help slove this problem.

see the sketch as it is right now:

#include <WiFi.h>
#include <Adafruit_NeoPixel.h>
#include <time.h>

/* ================= HARDWARE ================= */
#define LED_PIN 4
#define LED_COUNT 13
Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRBW + NEO_KHZ800);

/* ================= WIFI ================= */
const char* ssid = "XXXXX";
const char* password = "XXXXX";
const char* ntpServer = "pool.ntp.org";

/* ================= LOCATIONS ================= */
float ledLat[LED_COUNT]={52.37,48.85,51.50,41.90,59.33,55.75,1.35,35.68,-33.87,40.71,19.43,34.05,-23.55};
float ledLon[LED_COUNT]={4.90,2.35,-0.12,12.50,18.07,37.62,103.82,139.69,151.21,-74.01,-99.13,-118.24,-46.63};

/* ================= SOLAR ================= */
struct SolarState{
  time_t civilDawn;
  time_t sunrise;
  time_t sunset;
  time_t civilDusk;
  bool valid;
};

SolarState solar[LED_COUNT];

/* ================= MATH ================= */
constexpr double DEG2RAD=PI/180.0;
constexpr double RAD2DEG=180.0/PI;

double d2r(double d){return d*DEG2RAD;}
double r2d(double r){return r*RAD2DEG;}

float gammaCorrect(float x){
  if(x<0)x=0;
  if(x>1)x=1;
  return pow(x,2.2);
}




/* ================= SOLAR CALC ================= */
bool solarEventUTC(int year,int month,int day,double lat,double lon,double altitude,bool sunrise,time_t* outUTC){

  time_t midnight=utcMidnight(year,month,day);

  struct tm tmDay;
  gmtime_r(&midnight,&tmDay);
  int N=tmDay.tm_yday+1;

  double lngHour=lon/15.0;
  double tApprox=sunrise?N+(6-lngHour)/24.0:N+(18-lngHour)/24.0;

  double M=0.9856*tApprox-3.289;

  double L=M+1.916*sin(d2r(M))+0.020*sin(2*d2r(M))+282.634;
  while(L<0)L+=360;
  while(L>=360)L-=360;

  double RA=r2d(atan2(0.91764*sin(d2r(L)),cos(d2r(L))));
  while(RA<0)RA+=360;
  while(RA>=360)RA-=360;

  double Lq=floor(L/90)*90;
  double RAq=floor(RA/90)*90;
  RA=(RA+(Lq-RAq))/15.0;

  double sinDec=0.39782*sin(d2r(L));
  double cosDec=cos(asin(sinDec));

  double cosH=(sin(d2r(altitude))-sinDec*sin(d2r(lat)))/(cosDec*cos(d2r(lat)));

  if(cosH>1 || cosH<-1) return false;

  double H=sunrise?360-r2d(acos(cosH)):r2d(acos(cosH));
  H/=15.0;

  double T=H+RA-0.06571*tApprox-6.622;
  double UT=T-lngHour;

  while(UT<0)UT+=24;
  while(UT>=24)UT-=24;

  time_t result=midnight+(time_t)(UT*3600.0);
  if(!sunrise && UT<12) result+=86400;

  *outUTC=result;
  return true;
}

/* ================= UPDATE SOLAR ================= */
void updateSolar(int i,time_t now){


  time_t solarNow = now + (time_t)(ledLon[i] * 240.0);

  struct tm tm;
  gmtime_r(&solarNow,&tm);

  int y=tm.tm_year+1900;
  int m=tm.tm_mon+1;
  int d=tm.tm_mday;

  bool ok=
  solarEventUTC(y,m,d,ledLat[i],ledLon[i],-6,true,&solar[i].civilDawn)&&
  solarEventUTC(y,m,d,ledLat[i],ledLon[i],-0.833,true,&solar[i].sunrise)&&
  solarEventUTC(y,m,d,ledLat[i],ledLon[i],-0.833,false,&solar[i].sunset)&&
  solarEventUTC(y,m,d,ledLat[i],ledLon[i],-6,false,&solar[i].civilDusk);

  if(!ok){
    solar[i].valid=false;
    return;
  }

  /* sanity check volgorde */
  if(!(solar[i].civilDawn <= solar[i].sunrise &&
       solar[i].sunrise   <= solar[i].sunset &&
       solar[i].sunset    <= solar[i].civilDusk)){
    solar[i].valid=false;
    return;
  }

  solar[i].valid=true;
}


/* ================= COLOR ================= */
uint32_t makeColor(float p){
  uint8_t r=76*p;
  uint8_t g=42*p;
  uint8_t b=15*p;
  uint8_t w=4*p;
  return strip.Color(g,r,b,w);
}

/* ================= SETUP ================= */
void setup(){

Serial.begin(115200);

strip.begin();
strip.clear();
strip.show();

setenv("TZ","UTC0",1);
tzset();

WiFi.begin(ssid,password);
while(WiFi.status()!=WL_CONNECTED){delay(500);}

configTime(0,0,ntpServer);
while(time(nullptr)<1000000000){delay(500);}
}

/* ================= LOOP ================= */
void loop(){

time_t now=time(nullptr);

static int lastDay=-1;
struct tm tm;
gmtime_r(&now,&tm);

if(tm.tm_yday!=lastDay){
lastDay=tm.tm_yday;
for(int i=0;i<LED_COUNT;i++)
updateSolar(i,now);
}

for(int i=0;i<LED_COUNT;i++){

uint32_t c=0;

if(!solar[i].valid){
strip.setPixelColor(i,0);
continue;
}

if(now>=solar[i].civilDawn && now<solar[i].sunrise){

float dur=solar[i].sunrise-solar[i].civilDawn;
if(dur>0){
float p=gammaCorrect(1.0-float(now-solar[i].civilDawn)/dur);
c=makeColor(p);
}

}else if(now>=solar[i].sunrise && now<solar[i].sunset){

c=0;

}else if(now>=solar[i].sunset && now<solar[i].civilDusk){

float dur=solar[i].civilDusk-solar[i].sunset;
if(dur>0){
float p=gammaCorrect(float(now-solar[i].sunset)/dur);
c=makeColor(p);
}

}else{

c=makeColor(1.0);

}

strip.setPixelColor(i,c);
}

strip.show();
delay(100);
}

I'm getting this error when compiling the sketch:

sketch_feb10a:47:21: error: 'utcMidnight' was not declared in this scope; did you mean 'midnight'?
    |   time_t midnight = utcMidnight(year, month, day);
    |                     ^~~~~~~~~~~
    |                     midnight

From which link did you download the timer.h library?

You need to start instrumenting your code with Serial.print() statements to display the values from all your intermediate calculations. See if those values makes sense. If necessary, do the calculations manually and compare to what your program produces.

Doesn't sound intuitive to me..

It looks like you want to represent 24 time zones with 13 LEDs, and modify it according to "180" of latitude (pole[90] to equator [0] to pole [-90]).

At any longitude, from one hour after sunset until one hour before sunrise, physics intended the color of the sky to be black. On the equator, that would be ten hours of black, dark. At one pole, that would be none, at the other pole, that would be all day.

At any longitude from one hour after sunrise until one hour before sunset, physics (because nature has weather) intended the color of the sky to be "white" (call it what you will). On the Equator, that is ten hours of white, light. At one pole, no light, at the other pole, light all day.

Latitude can be divided into 180 divisions... one pole being "-90", the equator being "+/-0" and the other pole "+90"... The max value of 255 can be divided into those 180 degrees, from "255" (full light) at one pole to "0" (no light) at the other pole.

That leaves you with showing ten hours of black, ten hours of white, then two hours of "rise" and two hours of "set"... on one pixel (each), representing two hours (each).

I don't think that is what you wanted... I think you wanted all your pixels to represent the "color" of the moment at a particular lat/lon at a specific moment of time. I made a thing with WS2812B to represent an interpretation of "all day" sky color and put it in a simulation. Here it is for you to use or not... the function you want to be running is sunrisesunset();

The fade_from_to() function is the showcase of the sketch.

This choice seems a bit weird but why not.

I've something that might be of interest, here is the simulation in wokwi.

To make it more visual :

  • I used light green to show when it should be OFF
  • I use full RED when the led should be ON
  • I use a gradient of yellow during the twilights

The leds are updated every 5 seconds, the ESP32 maintains the time using NTP automagically fetching the real time every hour and WiFi should reconnect if needed.

Serial Monitor will show

Connecting to WiFi...
Network connection established
Obtained IP address = 10.10.0.2
NTP synchronized
NTP sync performed at UTC: 11/02/2026 17:48:05
Date (UTC): 2026-02-11 | All times are UTC
Amsterdam       : Civil Dawn: 06:11:17 | Sunrise: 06:47:12 | Sunset: 16:23:54 | Civil Dusk: 16:59:46
Paris           : Civil Dawn: 06:25:32 | Sunrise: 06:58:36 | Sunset: 16:53:11 | Civil Dusk: 17:26:13
London          : Civil Dawn: 06:48:12 | Sunrise: 07:23:15 | Sunset: 17:08:18 | Civil Dusk: 17:43:17
Rome            : Civil Dawn: 04:54:07 | Sunrise: 05:23:04 | Sunset: 15:46:13 | Civil Dusk: 16:15:09
Stockholm       : Civil Dawn: 04:41:19 | Sunrise: 05:25:23 | Sunset: 14:15:24 | Civil Dusk: 14:59:22
Moscow          : Civil Dawn: 01:56:33 | Sunrise: 02:35:55 | Sunset: 11:51:53 | Civil Dusk: 12:31:10
Singapore       : Civil Dawn: 16:00:21 | Sunrise: 16:21:42 | Sunset: 04:25:48 | Civil Dusk: 04:47:09
Tokyo           : Civil Dawn: 11:49:03 | Sunrise: 12:15:28 | Sunset: 22:58:32 | Civil Dusk: 23:24:56
Sydney          : Civil Dawn: 08:53:41 | Sunrise: 09:20:12 | Sunset: 22:48:34 | Civil Dusk: 23:15:03
New York        : Civil Dawn: 16:23:07 | Sunrise: 16:51:27 | Sunset: 03:21:51 | Civil Dusk: 03:50:09
Mexico City     : Civil Dawn: 19:21:22 | Sunrise: 19:43:55 | Sunset: 07:11:02 | Civil Dusk: 07:33:34
Los Angeles     : Civil Dawn: 22:09:05 | Sunrise: 22:34:52 | Sunset: 09:26:06 | Civil Dusk: 09:51:52
Sao Paulo       : Civil Dawn: 11:34:55 | Sunrise: 11:58:30 | Sunset: 00:55:54 | Civil Dusk: 01:19:28

On 11/02/2026, at 17:48:05 UTC
Amsterdam  Night    - LED fully on
Paris      Night    - LED fully on
London     Night    - LED fully on
Rome       Night    - LED fully on
Stockholm  Night    - LED fully on
Moscow     Night    - LED fully on
Singapore  Day      - LED off
Tokyo      Day      - LED off
Sydney     Day      - LED off
New York   Day      - LED off
Mexico City Night    - LED fully on
Los Angeles Night    - LED fully on
Sao Paulo  Day      - LED off

On 11/02/2026, at 17:48:10 UTC
Amsterdam  Night    - LED fully on
Paris      Night    - LED fully on
London     Night    - LED fully on
Rome       Night    - LED fully on
Stockholm  Night    - LED fully on
Moscow     Night    - LED fully on
Singapore  Day      - LED off
Tokyo      Day      - LED off
Sydney     Day      - LED off
New York   Day      - LED off
Mexico City Night    - LED fully on
Los Angeles Night    - LED fully on
Sao Paulo  Day      - LED off

On 11/02/2026, at 17:48:15 UTC
Amsterdam  Night    - LED fully on
Paris      Night    - LED fully on
London     Night    - LED fully on
Rome       Night    - LED fully on
Stockholm  Night    - LED fully on
Moscow     Night    - LED fully on
Singapore  Day      - LED off
Tokyo      Day      - LED off
Sydney     Day      - LED off
New York   Day      - LED off
Mexico City Night    - LED fully on
Los Angeles Night    - LED fully on
Sao Paulo  Day      - LED off

...

Neat.

These two are on the nearly the same longitude (99w and 118w)... and New York is four hours from dark shows just right! (shows fully on)

I’m not vet sure of the lat/long and names - some checking also on the math for the solar angle etc might be needed.

Welcome!

I understand what you are saying! I used AI for two days to get what you are trying to accomplish. Some of the code would work other parts wouldn't. Next try something else broke. What I finally found was the way we specify mm,dd,yy with the century assumed. I think I have it working now but will be a while until I can get back to it.

I use AI for formatting my code and making headers. It is good at finding when I have a O for a 0. I cannot see the difference on the monitor but the compiler does.

I used my non artificial intelligence (aka my brain) and some of the functions from OP’s source code and it was done in 30 min or so.

AI can send you down to rabbit holes pretty quickly and help you dig further if you don’t really get what it’s doing and can’t guide it out of the hole quickly.

It's scary how unpredictable AI is.
I asked chatgpt to convert simple short clean remote signal raw pulse data to binary. It got it wrong. I told that and after "you are absolutely right blabla" it got it wrong again. I told that and after blabla it presented every single pulse timing in column with corresponding 1/0. They were correct now. Then below that it printed the binary on one line with one bit wrong again.

Great explanation why newbies should not depend on AI, they would not have enough experience to see that the AI answer is wrong. It worked for you because you understood the problem and the possible solutions.

Tom.... :smiley: :+1: :coffee: :australia:

It's obvious (for most of us) that AI can make mistakes. What is not so obvious is that it can make ridiculously unpredictable mistakes.
Sub $1 MCU can count 1+1=-2 forever without mistakes, but that's not the case with AI.
Another thing I observed, it can loose it's "memory". If defined fact A>B makes conflict with the "big learning" it has made several years ago, you can go forward many steps with A>B until unpredictable B>A suddenly kicks in.

AI is useful, but never trust.

ps. sorry for off-topic post.

I feel Ai has taken you the long way round.
Calculating twilight(s) only needs AFAIK solar angle, not local time.
Calculating solar elevation (and azimuth) is easy with a library.
Then you only need one line of code to get sun elevation.

calcHorizontalCoordinates(now, latitude, longitude, az, el);

LED colour/brightness can be derived from el.

Time is built into the latest ESP core, and only needs one line of code to get UTC (now).
Attached is an example to get solar elevation for Sidney.

The code is longer than strictly needed because I added callback, to make sure that NTP was updated.
Leo..

#include <WiFi.h>                                     // ESP32
#include "esp_sntp.h"                                 // for callback
#include <SolarCalculator.h>                          // jpb10
const float latitude = -33.550, longitude = 151.150;  // Sidney, right-click on Google maps
double az, el;                                        // holds azimuth, elevation
unsigned long prevTime;
bool reSync;
time_t now;  // epoch
tm tm;       // time struct

void cbSyncTime(struct timeval *tv) {  // sync callback
  Serial.println("TimeSync");
  reSync = true;
}

void setup() {
  Serial.begin(115200);
  while (!Serial) delay(1000);                     // wait for serial
  WiFi.begin("SSID", "PASS");                      // WiFi credentials
  sntp_set_time_sync_notification_cb(cbSyncTime);  // enable callback
  configTzTime("", "pool.ntp.org");                // https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv
  Serial.println("Waiting for time sync");
  while (!reSync) yield(); // wait here for time sync
}


void loop() {
  time(&now);
  if (now != prevTime) {                                          // if time (seconds) has changed
    prevTime = now;                                               // remember
    calcHorizontalCoordinates(now, latitude, longitude, az, el);  // calculate sun angle

    Serial.print("Sun elevation: ");
    Serial.println(el);
  }
}

Wrote a quick world clock example that calculates current time and solar elevation.
Dawn/dusk can be calculated from that sun elevation.
Leo..

#include <WiFi.h>             // ESP32
#include <SolarCalculator.h>  // jpb10
#include "esp_sntp.h"
float latitude, longitude;  // right-click on Google Maps
double az, el;              // azimuth, elevation
bool reSync;
unsigned long prevTime;
time_t now;  // holds epoch
tm tm;       // time struct
char buffer[10];


void cbSyncTime(struct timeval *tv) {  // sync callback
  Serial.println("TimeSync");
  reSync = true;
}

void setup() {
  Serial.begin(115200);
  while (!Serial) delay(1000);
  sntp_set_time_sync_notification_cb(cbSyncTime); // NTP callback
  WiFi.begin("SSID", "PASS"); // WiFi credentials
  configTzTime("CET-1CEST,M3.5.0,M10.5.0/3", "pool.ntp.org");  // Home, assuming Amsterdam
  Serial.println("Waiting for time sync");
  while (!reSync) yield();
}

void loop() {
  time(&now);
  if (now != prevTime) {  // if time (seconds) has changed
    prevTime = now;       // remember

    // Amsterdam
    configTzTime("CET-1CEST,M3.5.0,M10.5.0/3", "");
    latitude = 52.3835;
    longitude = 4.8964;
    localtime_r(&now, &tm);
    snprintf(buffer, sizeof(buffer), "%02u:%02u:%02u", tm.tm_hour, tm.tm_min, tm.tm_sec);
    calcHorizontalCoordinates(now, latitude, longitude, az, el);
    Serial.print("Amsterdam: ");
    Serial.print(buffer);
    Serial.print("\tSun: ");
    Serial.print(el);
    Serial.println(" Degrees");

    // Singapore
    configTzTime("<+08>-8", "");                             // TZ change
    latitude = 1.2987;                                                   // location
    longitude = 103.8549;                                                // change
    localtime_r(&now, &tm);                                              // update time elements
    snprintf(buffer, sizeof(buffer), "%02u:%02u:%02u", tm.tm_hour, tm.tm_min, tm.tm_sec);  // DSTime
    calcHorizontalCoordinates(now, latitude, longitude, az, el);
    Serial.print("Singapore: ");
    Serial.print(buffer);
    Serial.print("\tSun: ");
    Serial.print(el);
    Serial.println(" Degrees");

    // New York
    configTzTime("EST5EDT,M3.2.0,M11.1.0", "");
    latitude = 40.690;
    longitude = -74.045;
    localtime_r(&now, &tm);
    snprintf(buffer, sizeof(buffer), "%02u:%02u:%02u", tm.tm_hour, tm.tm_min, tm.tm_sec);
    calcHorizontalCoordinates(now, latitude, longitude, az, el);
    Serial.print("New York:  ");
    Serial.print(buffer);
    Serial.print("\tSun: ");
    Serial.print(el);
    Serial.println(" Degrees");

    // Sydney
    configTzTime("AEST-10AEDT,M10.1.0,M4.1.0/3", "");
    latitude = -33.862;
    longitude = 151.189;
    localtime_r(&now, &tm);
    snprintf(buffer, sizeof(buffer),"%02u:%02u:%02u", tm.tm_hour, tm.tm_min, tm.tm_sec);
    calcHorizontalCoordinates(now, latitude, longitude, az, el);
    Serial.print("Sydney:    ");
    Serial.print(buffer);
    Serial.print("\tSun: ");
    Serial.print(el);
    Serial.println(" Degrees");

    Serial.println("");
  }
}

Negative = below the horizon (night).
Civil twilight is 0 to -6 degrees.
The map() command can convert solar angle to LED brightness.

Tested on an ESP32-C3 Supermini.

Add the leds and a few printout and wifi management and you won’t be far from what I posted as a wokwi

Did it fix the dateline jump that OP had trouble with.

I like to use code that I fully understand.
Leo..

Makes sense

If local times are not required, then you only need these three lines per city.

// Hong Kong
latitude = 22.33046;
longitude = 114.16705;
calcHorizontalCoordinates(now, latitude, longitude, az, el);
// current el contains sun pos.

Leo..