Yet Another ESP32 NTP Question (Looping / Update Interval)

I've Googled myself tired on this topic and I see that NTP time synchronization discussions are plentiful in the forums here, so apologies if I am asking a question that has been answered - I really tried to solve this myself lol.

I am building an ESP32 based project that needs to trigger a relay on a schedule. The time needs to be accurate and NTP seems like a reasonable solution. I understand that the default ESP32 time.h library includes everything needed to make that work. No problem there.

The confusion comes about because I have found references and examples to NTP synchronized local time both with and without additional NTP updates - many examples rely on the default 1 hour automatic NTP synch, while others turn off WiFi after the initial synch so the system time will drift at some point. I cannot seem to locate the API reference that identifies whether that automatic re-synch occurs or not. Or, perhaps the NTP code in time.h sees that the network is no longer active and just skips any future periodic updates. I do see a reference in this post to the Espressif API suggesting that the interval may be changed by modifying a variable named CONFIG_LWIP_SNTP_UPDATE_DELAY, but I'm uncertain how to set that variable and also the minimum is stated as 15 sec, so that obviously is not how the updates are disabled if that is desired.

Finally - my project displays the time with seconds, so I need a loop that occurs every second. Here is another area that is confusing because many examples seem to solve it differently and I have no idea what would be 'right' or 'wrong' here. This is what I've come up with -

i#include <WiFi.h>
#include <time.h>
#include <M5DinMeter.h> // My board defs

int LastSecond;

const char* NTP_SERVER = "us.pool.ntp.org";
const char* TZ_INFO    = "EST5EDT";  // enter your time zone (https://remotemonitoringsystems.ca/time-zone-abbreviations.php)

tm timeinfo;
time_t now;
long unsigned lastNTPtime;
unsigned long lastEntryTime;


void setup() {
	Serial.begin(115200);
	while (!Serial) { ; }		// wait for Serial port to connect. Needed for native USB port only
  
  WiFi.begin(ssid, password);

  int counter = 0;
  while (WiFi.status() != WL_CONNECTED) {
    delay(200);
    if (++counter > 100) ESP.restart();
    Serial.print ( "." );
  }
  Serial.println("\n\nWiFi connected\n\n");

  configTime(0, 0, NTP_SERVER);
  // See https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv for Timezone codes for your region
  setenv("TZ", TZ_INFO, 1);

  if (getNTPtime(10)) {  // wait up to 10sec to sync
  } else {
    Serial.println("Time not set");
    ESP.restart();
  }
  showTime(timeinfo);
  lastNTPtime = time(&now);
  lastEntryTime = millis();
}


void loop() {
  getNTPtime(10);
  showTime(timeinfo); // FOO update display with localtime(&now)
  //delay(1000);
  while (LastSecond == timeinfo.tm_sec) {
    time(&now);
    localtime_r(&now, &timeinfo);
    }
    LastSecond = timeinfo.tm_sec;
}

bool getNTPtime(int sec) {

  {
    uint32_t start = millis();
    do {
      time(&now);
      localtime_r(&now, &timeinfo);
      Serial.print(".");
      delay(10);
    } while (((millis() - start) <= (1000 * sec)) && (timeinfo.tm_year < (2016 - 1900)));
    if (timeinfo.tm_year <= (2016 - 1900)) return false;  // the NTP call was not successful
    // Serial.print("now ");  Serial.println(now);
    // char time_output[30];
    // strftime(time_output, 30, "%a  %d-%m-%y %T", localtime(&now));
    // Serial.println(time_output);
    // Serial.println();
  }
  return true;
}

I have not included any code related to running my tasks as the main issue is just getting a simple loop that runs every second. Additionally, I would like the NTP update interval to be twice a day, so I will set the aforementioned CONFIG_LWIP_SNTP_UPDATE_DELAY somehow.

The whole ESP32 time.h and hal-time libraries are way over my head and I'll just have to take it on faith now that they do what they do somehow in their own cpu thread - the Espressif documentation on this would be a week's research for me to understand.

Any tips / suggestions are appreciated. Or, if this has been answered before and I somehow didn't find that after hours of Googling, just toss me a link. The seconds based loop concerns me - I obviously don't want to accidentally make repetitive NTP calls and get blacklisted lol.

Thanks!

EDIT - the 'getNTPtime(10);' in my loop concerns me because it seems to allow up to 10 seconds for it to process... That's fine for the initial synch, but afterwards it would be catastrophic for my one second loop to take 10 seconds to run...

EDIT2 - I've been Googling CONFIG_LWIP_SNTP_UPDATE_DELAY and it appears this is (was?) a compiler directive and could not be changed by the user in running code... I see references to this being revised (2019 discussion), but I'm uncertain if it has been and compiler directives like that are currently beyond my experience level anyway. I guess being 'stuck' with hourly updates isn't the worst thing...

Try

sntp_set_sync_interval(12UL*60*60*1000);

Try

void loop() {
  static time_t lastNow;

  time(&now);
  if (now != lastNow) {
    // Display time
    lastNow = now;
  }
}
1 Like

Ironically, I just realized that my time variable should be static so I changed that.

But does the suggested code trigger on the exact second? I'd like the clock functionality to delay until the next second rolls over so everything is as 'timely' as possible... That's somewhat different than a loop that takes one second.

Hopefully I am conveying this well...

I will try the interval suggestion. I'm not yet certain how to prove out when the NTP synch occurs in my code to know if things are working as desired...

By 'exact second' do you mean the moment the second increments as per international standards? I ran into this problem and never really resolved it, you can read about my efforts here:

Yes - within a few ms anyway. I looked through your linked thread and ironically I have also been entertaining the thought of GPS time source because I worry about network reliability (not to mention that I obviously really have no clue how this NTP synching works). I've got a few DS3231 boards I am playing with on related projects.

However, I've got a few variations of this NTP code running on my M5Stack ESP32 boards. I used a library called ezTime on one module before I learned that NTP synchronization was built into the time.h library by default and and it has proven remarkedly accurate over the last 48 hours - every morning I come in and check and the time matches UTC website on my phone. It spits out periodic status reports to the serial port and while I haven't compared it on my scope it claims the system clock drifts less than 80ms between updates, which is more than sufficient for my needs. I am trying to use the standard time.h library on this test to make my code as 'standardized' as possible - I'd expect similar results with this.

Anyhoo - I used a similar loop to what I posted earlier and the display does appear to change digits while observing the UTC clock on my pc, so I think that may be where I end up. I could probably use this for logging similar to what it seems you were working on as well.

EDIT - the ezTime library makes it easy to know the status and NTP update intervals... If I could know for certain that the default time.h library is working as expected that would suffice. Still no idea how some people are turning WiFi off in their examples with what appears to be the same configTime() call as those that use interval updates.

Most likely to save power. Yes, it will drift over time. You can periodically reenable WiFi and force a resync to NTP. Using SNTP_SYNC_MODE_SMOOTH will ensure that time changes during the resync are always monotonic.

Obviously, resynch to NTP can't happen if WiFi is disabled.

No call the API function sntp_set_sync_interval

You're over thinking this. There's no reason to do that. Set a reasonable synch interval (say 1 hour) and let it happen in the background. Then get the time from the ESP32's internal RTC as needed.

That's more than sufficient. But as mentioned, it's simple to change the interval.

You can hook custom functions into the ESP32's background RTC adjustments for synching to GPS instead of NTP.

What does that mean?

1 Like

I think you answered this in your first comment... What I mean is that I see code examples that leave WiFi enabled after the NTP synch and others that disable WiFi to save power. I would expect the configTime() or whatever sets up the NTP synch to explicitly specify whether future NTP syncs are called or if it is a one-time call to set the system clock at boot, so the time.h library 'knows' whether to attempt additional NTP calls or not. By the looks if it, time.h must try to make additional NTP calls but if WiFi has been disabled obviously that will not happen. I do not seen any parameter related to 'enable updates' that is set in configTime() or anywhere else in the code examples - it's the same either way.

I half assumed that there might be status functions that could be called to let you know things like when the last sync was done or when the next sync is scheduled, or what the difference was between system clock and NTP when the last sync was performed... These functions are available in the ezTime library and I initially assumed the author was using time.h functions to provide this - but I'm getting the sense that NTP was not always available and the author may have written that library before NTP was implemented in time.h.

I've been doing a lot of Googling and I haven't discovered any ability to check the NTP status beyond the initial call at boot, so I guess you just have to have faith it is working?

I'm just going to do some tests over a few days to prove out everything is keeping time and that should be good enough for my small project.

Thanks for all the suggestions!

ESP32 supports standard POSIX time APIs. See the Espressif Documenation , the APIs in esp_sntp.h, as well as the APIs in time.h.

The adjtime() function can tell you how much time delta is still to be adjusted when you're in SNTP_SYNC_MODE_SMOOTH mode.

2 Likes

No, but it's as close as you can easily get. It depends what other code is in loop() and how long that takes to run. In the code I suggested, there is nothing else in loop() at all, so it will run thousands of times per second on an ESP32, and so should catch the change in time within a millisecond. Your mileage may vary, as the saying goes.

Even if it catches the change within a millisecond, I don't think the time from NTP servers is given to high precision anyway, so that could be a second out, or more if the servers or any part of the connection between the NTP server through to the ESP is a little busy and response times are longer than usual.

It's 3 hours (on ESP32 at least), 10800000 millis. You can check it with a function mentioned below.

(BTW, I see "synch" and think it rhymes with "lynch" and "lunch". The band is called NSYNC after all.)

The layers of software for ESP32 NTP are

  • your sketch
    • arduino-esp32 board platform (C++)
      • ESP-IDF (plain C)
        • lwIP (lightweight IP, also C)

Through typical C chicanery, the update interval takes effect at the bottom

    /* Set up timeout for next request (only if poll response was received)*/
    if (sntp_opmode == SNTP_OPMODE_POLL) {
      u32_t sntp_update_delay;
      sys_untimeout(sntp_try_next_server, NULL);
      sys_untimeout(sntp_request, NULL);

      /* Correct response, reset retry timeout */
      SNTP_RESET_RETRY_TIMEOUT();

      sntp_update_delay = (u32_t)SNTP_UPDATE_DELAY;
      sys_timeout(sntp_update_delay, sntp_request, NULL);
      LWIP_DEBUGF(SNTP_DEBUG_STATE, ("sntp_recv: Scheduled next time request: %"U32_F" ms\n",
                                     sntp_update_delay));
    }

SNTP_UPDATE_DELAY is actually a macro in lwIP, defined to call a function by and in ESP-IDF, instead of a hard-coded-at-compile-time delay.

#define SNTP_UPDATE_DELAY                 (sntp_get_sync_interval())

The function is a trivial getter for a module-static variable

void sntp_set_sync_interval(uint32_t interval_ms)
{
    if (interval_ms < 15000) {
        // SNTPv4 RFC 4330 enforces a minimum update time of 15 seconds
        interval_ms = 15000;
    }
    s_sync_interval = interval_ms;
}

uint32_t sntp_get_sync_interval(void)
{
    return s_sync_interval;
}

The setter is, as others have mentioned, sntp_set_sync_interval, which enforces the 15-second minimum. The variable is initialized with the compile-time macro

static uint32_t s_sync_interval = CONFIG_LWIP_SNTP_UPDATE_DELAY;

So lwIP makes the call out to the NTP server(s). It receives a response, and then calls sys_timeout to try again after the interval. As inferred from the name, sys_timeout is an internal way to queue stuff to happen later. As an embeddable library, the exact mechanism can vary, much like how the interval value can be customized.

ESP32 is dual-core. With ESP-IDF

  • Core 0 is for "protocol": Wi-Fi, Bluetooth, etc; and therefore where lwIP is running its tasks
  • Core 1 is the "app core", where your sketch runs

You can see this by modifying the ESP32 | Time | SimpleTime example

void printLocalTime() {
  struct tm timeinfo;
  if (!getLocalTime(&timeinfo)) {
    Serial.println("No time available (yet)");
    return;
  }
  Serial.println(&timeinfo, "%A, %B %d %Y %H:%M:%S");
  Serial.print("on core: ");         // add these
  Serial.println(xPortGetCoreID());  // two statements
}

will print something like

Got time adjustment from NTP!
Wednesday, March 26 2025 00:10:11
on core: 0
Wednesday, March 26 2025 00:10:13
on core: 1
Wednesday, March 26 2025 00:10:18
on core: 1

The first one is from the timeavailable callback, and the rest are from the main loop. So you can be misbehaving in your sketch without directly affecting time-sensitive background tasks. Speaking of which....

Your getNTPtime looks pretty much like the built-in getLocalTime on ESP32 (and ESP8266)

bool getLocalTime(struct tm *info, uint32_t ms) {
  uint32_t start = millis();
  time_t now;
  while ((millis() - start) <= ms) {
    time(&now);
    localtime_r(&now, info);
    if (info->tm_year > (2016 - 1900)) {
      return true;
    }
    delay(10);
  }
  return false;
}

except yours prints dots. Unless you really like those dots, you can avoid some technical debt and use the built-in. Either way, it should only be not-"instant" on the first NTP update, since the year will represent 1970 until that is complete. Whenever the subsequent updates happen "in the background", the year should not regress unless something has gone wrong.

You can code to defend against that, but it's probably not worth it.

2 Likes

Thanks very much for that informative reply - that is the clearest explanation of the NTP update process I have read, and I spent several hours Googling around and even tried to wrap my mind around the Espressif API documentation.

I knew that the ESP32 was dual core, but had no idea the libraries created those threads to run automatically on their own. This will also help understand BT and similar topics when I get to them. That actually makes a lot of sense lol.

One last question if I may - regarding the example code I found that disabled WiFi after getting the time - if the network drops, I assume all those processes simply get skipped? And perhaps if the network returns, they resume? I found no reference to configuring NTP as a one time request that will not continue running. I have no intention of implementing that type of usage, it’s just curiosity.

PS - I’m guessing that the Arduino IDE compiles the same dual core code that Platform IO and other Espressif backed tools creates?

There was a similar forum question a while back. I posted the following demonstration code. After connecting to WiFi and doing a SNTP_SYNC_MODE_IMMED synch to NTP, the code turns off the WiFi. It prints the current time and the amount of time adjustment pending from the last NTP synch every second.

Entering “o” (for offset) in the Serial Monitor will force a 10 second jump in the system time (simulating the effect of RTC drift without NTP synch available). If you then enter “w” (for WiFi) in the Serial Monitor, the code will re-enable WiFi, start re-synching the time to NTP, and then disable WiFi again. Since at this point the Synch Mode will be SNTP_SYNC_MODE_SMOOTH, you can watch as the time is slowly corrected.

#include "Arduino.h"
#include <WiFi.h>
#include "esp_sntp.h"
#include "TZ.h"

void handleWifiEvent(arduino_event_id_t event, arduino_event_info_t info);
void handleSynchEvent(timeval *tv);
void reEnableWiFiHandler(TimerHandle_t xTimer);
void printTimeTask(void *pvParameters);
void synchTimeTask(void *pvParameters);

const EventBits_t startTimePrinting {1UL << 0};
const EventBits_t synchTime {1UL << 1};
EventGroupHandle_t controlGroupHandle;

TimerHandle_t wifiEnableTimer;

void setup() {
	const char ssid[] {"xxxxxx"};
	const char password[] {"xxxxxx"};
	const TickType_t wifiEnableTime {3600UL * 1000};
	Serial.begin(115200);
	vTaskDelay(5000);

	controlGroupHandle = xEventGroupCreate();
	assert(controlGroupHandle != NULL && "Failed to create Control Event Group");

	BaseType_t returnCode {xTaskCreatePinnedToCore(synchTimeTask, "Synch Time Task", 1900, NULL, 3, NULL, CONFIG_ARDUINO_RUNNING_CORE)};
	assert(returnCode == pdTRUE && "Failed to create Synch Time Task");

	returnCode = xTaskCreatePinnedToCore(printTimeTask, "Print Task", 1900, NULL, 5, NULL, CONFIG_ARDUINO_RUNNING_CORE);
	assert(returnCode == pdTRUE && "Failed to create Print Task");

	wifiEnableTimer = xTimerCreate("Re-enable WiFi", wifiEnableTime, pdFALSE, nullptr, reEnableWiFiHandler);
	assert(wifiEnableTimer!=NULL && "Failed to create Wifi Re-enable Timer");

	WiFi.onEvent(handleWifiEvent);
	WiFi.begin(ssid, password);
}

void loop() {
	vTaskDelete(NULL);
}

void synchTimeTask(void *pvParameters) {
	bool firstTime {true};
	const char ntpServer1[] {"time.nist.gov"};
	const char ntpServer2[] {"pool.ntp.org"};
	const uint32_t syncInterval {120UL * 1000};
	uint8_t retryTest {0};
	uint8_t attemptsBeforeReset {5};
	timeval tv;
	tm *timeinfo;
	char timeString[100];
	time_t now;
	UBaseType_t lowestWatermark {10000};
	UBaseType_t currentWatermark;

	sntp_set_sync_interval(syncInterval);
	configTzTime(TZ_America_New_York, ntpServer1, ntpServer2);

	for (;;) {
		xEventGroupWaitBits(controlGroupHandle, synchTime, pdTRUE, pdFALSE, portMAX_DELAY);

		currentWatermark = uxTaskGetStackHighWaterMark(NULL);
		if (currentWatermark < lowestWatermark) {
			lowestWatermark = currentWatermark;
			log_i("New low watermark: %d\n", lowestWatermark);
		}

		if (!firstTime) {
			vTaskDelay(1000);
			esp_sntp_stop();
			vTaskDelay(500);
			esp_sntp_init();
			vTaskDelay(500);
		} else {
			firstTime = false;
			sntp_set_sync_mode(SNTP_SYNC_MODE_IMMED);  // Set for immediate synch
			retryTest = 0;
			while (true) {                 // Wait for NTP sync to take effect
				if (retryTest == 0) {
					if ((--attemptsBeforeReset) == 0) {
						ESP.restart();
					}
					log_i("Waiting for Time Sync");
					sntp_sync_time(&tv);
					retryTest = 10;
				} else {
					log_i(".");
					retryTest--;
				}
				now = time(nullptr);
				timeinfo = localtime(&now);
				if (timeinfo->tm_year >= (2024 - 1900)) {
					break;
				}
				vTaskDelay(2000);
			}
			sntp_set_sync_mode(SNTP_SYNC_MODE_SMOOTH);  // Set for smooth synch
			xEventGroupSetBits(controlGroupHandle, startTimePrinting);
			sntp_set_time_sync_notification_cb(handleSynchEvent);
			WiFi.disconnect();
			vTaskDelay(500);
			WiFi.mode(WIFI_OFF);
			BaseType_t result {xTimerStart(wifiEnableTimer, 0)};
			assert(result != pdFAIL && "Failed to start wifiEnableTimer");
		}
	}
}

void printTimeTask(void *pvParameters) {
	const TickType_t delayTime {1000};
	char timeString[100];
	TickType_t wakeTime;
	tm *timeinfo;
	char stat[20];
	UBaseType_t lowestWatermark {5000};
	UBaseType_t currentWatermark;

	xEventGroupWaitBits(controlGroupHandle, startTimePrinting, pdTRUE, pdFALSE, portMAX_DELAY);
	wakeTime = xTaskGetTickCount();

	for (;;) {
		time_t now = time(nullptr);
		tm *timeinfo = localtime(&now);
		strftime(timeString, 100, "Local Time: %A, %B %d %Y %H:%M:%S %Z", timeinfo);
		Serial.println(timeString);
		sntp_sync_status_t syncStatus {sntp_get_sync_status()};
		switch (syncStatus) {
			case SNTP_SYNC_STATUS_RESET:
				strcpy(stat, "Reset");
				break;
			case SNTP_SYNC_STATUS_COMPLETED:
				strcpy(stat, "Complete");
				break;
			case SNTP_SYNC_STATUS_IN_PROGRESS:
				strcpy(stat, "In Progress");
				break;
			default:
				strcpy(stat, "Invalid");
				break;
		}
		Serial.printf("Sync Status: %s, ", stat);
		timeval remaining;
		adjtime(nullptr, &remaining);
		int32_t delta {remaining.tv_usec + 1000000UL * remaining.tv_sec};
		Serial.printf("Remaining Time to Adjust: %ldus\n\n", delta);
		currentWatermark = uxTaskGetStackHighWaterMark(NULL);
		if (currentWatermark < lowestWatermark) {
			lowestWatermark = currentWatermark;
			log_i("New low watermark: %d\n", lowestWatermark);
		}

		char c {Serial.read()};
		if (c == 'o') {
			Serial.printf("\n\n******* Offsetting Time *******\n\n");
			timeval offset;
			offset.tv_sec = now + 10;
			offset.tv_usec = 0;
			settimeofday(&offset, nullptr);
		} else if (c == 'w') {
			Serial.printf("\n\n******* Turning on WiFi ******\n\n");
			WiFi.disconnect();
			vTaskDelay(500);
			WiFi.begin();
		}
		xTaskDelayUntil(&wakeTime, delayTime);
	}
}

void IRAM_ATTR handleWifiEvent(arduino_event_id_t event_id, arduino_event_info_t info) {
	switch (event_id) {
		case ARDUINO_EVENT_WIFI_READY:
			log_d("WiFi Ready");
			break;

		case ARDUINO_EVENT_WIFI_STA_START:
			log_d("WiFi Start");
			break;

		case ARDUINO_EVENT_WIFI_STA_STOP:
			log_d("WiFi Stop");
			break;

		case ARDUINO_EVENT_WIFI_STA_CONNECTED:
			log_d("Connected to SSID: %s", reinterpret_cast<char*>(info.wifi_sta_connected.ssid));
			break;

		case ARDUINO_EVENT_WIFI_STA_GOT_IP: {
			uint8_t bytes[4];
			uint32_t ipAddress {info.got_ip.ip_info.ip.addr};
			for (uint8_t i = 0; i < 4; i++) {
				bytes[i] = ipAddress & 0xFF;
				ipAddress >>= 8;
			}
			log_i("Got IP Address: %d.%d.%d.%d\n", bytes[0], bytes[1], bytes[2], bytes[3]);
			xEventGroupSetBits(controlGroupHandle, synchTime);
			break;
		}

		case ARDUINO_EVENT_WIFI_STA_LOST_IP:
			log_d("IP Address Lost");
			break;

		case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
			log_d("Disconnected From SSID: %s, Reason: %d", reinterpret_cast<char*>(info.wifi_sta_disconnected.ssid),
					info.wifi_sta_disconnected.reason);
			break;

		default:
			log_d("WiFi Event: %d", static_cast<uint8_t>(event_id));
			break;
	}
}

void handleSynchEvent(timeval *tv) {
	log_i("******* Synch Event ******\n\n");
	log_i("******* Turning off WiFi ******\n\n");
	WiFi.disconnect();
	vTaskDelay(500);
	WiFi.mode(WIFI_OFF);
	BaseType_t result {xTimerStart(wifiEnableTimer, 0)};
	assert(result != pdFAIL && "Failed to start wifiEnableTimer");
}

void reEnableWiFiHandler(TimerHandle_t xTimer) {
	log_i("*********** Re-Enabling Wifi by Timer  ***********\n\n\n");
	WiFi.disconnect();
	vTaskDelay(500);
	WiFi.begin();
}

Here's TZ.h:
TZ.h (22.2 KB)

1 Like

From esp_sntp.h:

/**
 * @brief Stops SNTP service
 */
void esp_sntp_stop(void);
1 Like

Thanks again - most informative!

I get the impression that you are more experienced with this platform than the average hobbyist... The overwhelming majority of Arduino examples I have found are fairly basic and only include the time.h and wifi.h libraries... I haven't stumbled upon examples referencing esp_sntp.h or even TZ.h in any NTP time demonstrations, though I did notice it in the Espressif API documentation.

Most hobbyists likely only need basic NTP time and don't get that deep into the weeds.

Thanks again for taking the time to explain this to me - it will keep me in sync :wink:

1 Like

Just noticed - my Arduino IDE has two settings named 'Events Run On: Core 1' and another named 'Arduino Runs On: Core 1'. The options for each setting are either Core 0 or Core 1. I have never modified these settings, so I assume they are either the default or potentially tied to the board settings file (this is M5Stack DinMeter).

Would this be related to the events core vs app core you referred to? I wonder if mine is set correctly... More Googling is in my future it would seem.

EDIT - I tested the Simple Time demo you mentioned earlier and I DO see both cores showing despite both those Arduino settings set to Core 1... So I guess another nothing to worry about moment. Though I really hate using settings and code that I don't understand. So this is why we all come here :wink:

07:11:10.394 -> Got time adjustment from NTP!
07:11:10.394 -> Wednesday, March 26 2025 13:11:08
07:11:10.394 -> on core: 0
07:11:14.652 -> Wednesday, March 26 2025 13:11:12
07:11:14.652 -> on core: 1
07:11:19.613 -> Wednesday, March 26 2025 13:11:17
07:11:19.613 -> on core: 1

The 'Arduino Run On' selection controls which core "loopTask" is spooled up on. This is the FreeRTOS task that calls your setup() function once and then repeatedly calls you loop() function. The 'Events Run On' selection controls which core WiFi callbacks that your register run on.

My whole life has been 8bit ucontrollers... The hardest thing to keep track of was interrupts and stacks. Most of my pc code has been SQL or VB (but VB is capable of some impressive apps) - threads and cores are a whole new world. I've got a long way to go - thanks for the help finding that path :smiling_face_with_sunglasses:

When I add this line I get the following error:

'sntp_set_sync_interval' was not declared in this scope

I know my code is acquiring ntp time through the time.h library. I'd assume that time.h calls the sntp library? Perhaps not and I need to explicitly include it? Or is the sntp library a different animal compared to time.h / 'regular' ntp?

EDIT - if I add the the following include, my code compiles without error. I'm not certain what the 'correct' libraries should be used with ntp time...

#include "esp_sntp.h"

If you want to use the APIs declared in esp_sntp.h, then you must #include that file.