Avoid blocking in NTPClient::update()

When a NTP request is sent, it may take several milliseconds to retrieve
the response.  This commit changes the NTPClient::update() behaviour to
asynchronous allowing a NTP request to be sent with one update() call
and handle the response when it's available, in another call eliminating
active waiting.

This commit also changes the NTPClient::forceUpdate() implementation to
rely on the logic in NTPClient::update().  However, the behaviour of
this function does not change from the API user's perspective.  It is
still synchronous, it only returns when all processing is complete.
This commit is contained in:
Michał Leśniewski 2022-01-27 23:15:04 +01:00
parent 531eff39d9
commit 3f9957dcf1
2 changed files with 119 additions and 48 deletions

View File

@ -75,16 +75,30 @@ void NTPClient::begin() {
void NTPClient::begin(unsigned int port) { void NTPClient::begin(unsigned int port) {
this->_port = port; this->_port = port;
this->_state = State::uninitialized;
this->_udp->begin(this->_port);
this->_udpSetup = true;
} }
bool NTPClient::forceUpdate() { bool NTPClient::update() {
#ifdef DEBUG_NTPClient switch (this->_state) {
Serial.println("Update from NTP Server"); case State::uninitialized:
#endif this->_udp->begin(this->_port);
this->_state = State::idle;
// fall through -- we're all initialized now
case State::idle:
if ((millis() - this->_lastUpdate < this->_updateInterval) // Update after _updateInterval
&& this->_lastUpdate != 0) // Update if there was no update yet.
return false;
this->_state = State::send_request;
// fall through -- ready to send request now
case State::send_request:
#ifdef DEBUG_NTPClient
Serial.println(F("Sending NTP request"));
#endif
// flush any existing packets // flush any existing packets
while(this->_udp->parsePacket() != 0) while(this->_udp->parsePacket() != 0)
@ -92,38 +106,85 @@ bool NTPClient::forceUpdate() {
this->sendNTPPacket(); this->sendNTPPacket();
// Wait till data is there or timeout... this->_lastRequest = millis();
byte timeout = 0; this->_state = State::wait_response;
int cb = 0;
do {
delay ( 10 );
cb = this->_udp->parsePacket();
if (timeout > 100) return false; // timeout after 1000 ms
timeout++;
} while (cb == 0);
this->_lastUpdate = millis() - (10 * (timeout + 1)); // Account for delay in reading the time // fall through -- if we're lucky we may already receive a response
case State::wait_response:
if (!this->_udp->parsePacket()) {
// no reply yet
if (millis() - this->_lastRequest >= 1000) {
// time out
#ifdef DEBUG_NTPClient
Serial.println(F("NTP reply timeout"));
#endif
this->_state = State::idle;
}
return false;
}
#ifdef DEBUG_NTPClient
Serial.println(F("NTP reply received"));
#endif
// got a reply!
this->_lastUpdate = this->_lastRequest;
this->_udp->read(this->_packetBuffer, NTP_PACKET_SIZE); this->_udp->read(this->_packetBuffer, NTP_PACKET_SIZE);
{
unsigned long highWord = word(this->_packetBuffer[40], this->_packetBuffer[41]); unsigned long highWord = word(this->_packetBuffer[40], this->_packetBuffer[41]);
unsigned long lowWord = word(this->_packetBuffer[42], this->_packetBuffer[43]); unsigned long lowWord = word(this->_packetBuffer[42], this->_packetBuffer[43]);
// combine the four bytes (two words) into a long integer // combine the four bytes (two words) into a long integer
// this is NTP time (seconds since Jan 1 1900): // this is NTP time (seconds since Jan 1 1900):
unsigned long secsSince1900 = highWord << 16 | lowWord; unsigned long secsSince1900 = highWord << 16 | lowWord;
this->_currentEpoc = secsSince1900 - SEVENZYYEARS; this->_currentEpoc = secsSince1900 - SEVENZYYEARS;
}
return true; // return true after successful update return true; // return true after successful update
default:
this->_state = State::uninitialized;
}
return false;
} }
bool NTPClient::update() { bool NTPClient::forceUpdate() {
if ((millis() - this->_lastUpdate >= this->_updateInterval) // Update after _updateInterval // In contrast to NTPClient::update(), this function always sends a NTP
|| this->_lastUpdate == 0) { // Update if there was no update yet. // request and only returns when the whole operation completes (no matter
if (!this->_udpSetup || this->_port != NTP_DEFAULT_LOCAL_PORT) this->begin(this->_port); // setup the UDP client if needed // if it's a success or a failure because of a timeout). In other words
return this->forceUpdate(); // this function is fully synchronous. It will block until the whole
// NTP operation completes.
//
// We could only make this function switch the state to State::send_request
// to ensure a NTP request would happen with the next call to
// NTPClient::update(). However, this would be an API change, users could
// expect synchronous behaviour and even skip the calls to NTPClient::update()
// completely relying only on this function for time updates.
// ensure we're initialized
if (this->_state == State::uninitialized) {
this->_udp->begin(this->_port);
}
// At this point we can be in any state except for State::uninitialized.
// Let's ignore that and switch right to State::send_request to send a
// fresh NTP request.
this->_state = State::send_request;
while (true) {
if (this->update()) {
// time updated
return true;
} else if (this->_state != State::idle) {
// still waiting for response
delay(10);
} else {
// failure
return false;
}
} }
return false; // return false if update does not occur
} }
bool NTPClient::isTimeSet() const { bool NTPClient::isTimeSet() const {
@ -165,8 +226,7 @@ String NTPClient::getFormattedTime() const {
void NTPClient::end() { void NTPClient::end() {
this->_udp->stop(); this->_udp->stop();
this->_state = State::uninitialized;
this->_udpSetup = false;
} }
void NTPClient::setTimeOffset(int timeOffset) { void NTPClient::setTimeOffset(int timeOffset) {
@ -209,4 +269,7 @@ void NTPClient::sendNTPPacket() {
void NTPClient::setRandomPort(unsigned int minValue, unsigned int maxValue) { void NTPClient::setRandomPort(unsigned int minValue, unsigned int maxValue) {
randomSeed(analogRead(0)); randomSeed(analogRead(0));
this->_port = random(minValue, maxValue); this->_port = random(minValue, maxValue);
// we've set a new port, remember to reinitialize UDP next time
this->_state = State::uninitialized;
} }

View File

@ -10,8 +10,8 @@
class NTPClient { class NTPClient {
private: private:
UDP* _udp; UDP* _udp;
bool _udpSetup = false;
const char* _poolServerName = "pool.ntp.org"; // Default time server const char* _poolServerName = "pool.ntp.org"; // Default time server
IPAddress _poolServerIP; IPAddress _poolServerIP;
@ -22,6 +22,14 @@ class NTPClient {
unsigned long _currentEpoc = 0; // In s unsigned long _currentEpoc = 0; // In s
unsigned long _lastUpdate = 0; // In ms unsigned long _lastUpdate = 0; // In ms
unsigned long _lastRequest = 0; // In ms
enum class State {
uninitialized,
idle,
send_request,
wait_response,
} _state = State::uninitialized;
byte _packetBuffer[NTP_PACKET_SIZE]; byte _packetBuffer[NTP_PACKET_SIZE];