Newer
Older
Tardis / third_party / sntp / sntp.cpp
/*- Project:    A simple SNTP client
  - Author:     Richard James Howe
  - License:    The Unlicense
  - Email:      howe.r.j.89@gmail.com
  - Repository: https://github.com/howerj/sntp
  - mbed port (c) 2023 John Watts and Casey Reeves */
#include "sntp.h"
#include "UDPSocket.h"

#include "mbed.h"
#include "mbed_trace.h"
#define TRACE_GROUP "SNTP"

EventFlags dns_arrived; // Holds a flag used to signal DNS arrival
nsapi_value_or_error_t dns_result; // Result from the DNS callback
SocketAddress dns_addr; // DNS address from the DNS callback

void gethostbyname_callback(
	nsapi_value_or_error_t result, SocketAddress *address) {
	dns_result = result;
	dns_addr = *address;
	dns_arrived.set(1);
}

nsapi_error_t gethostbyname_timeout(NetworkInterface &net, const char *server,
	SocketAddress &addr, uint32_t timeout_ms) {
	// Clear flags before call in case we had an unexpected callback
	dns_arrived.clear();

	// dns_id will be an ID for the async transaction or an error
	nsapi_value_or_error_t dns_id;
	dns_id = net.gethostbyname_async(server, gethostbyname_callback);

	if (dns_id < 0) {
		// dns_id is an error, creating the async transaction failed
		return dns_id;
	}

	if (dns_arrived.wait_any(1, timeout_ms) != 1) {
		// We timed out, cancel the transaction
		net.gethostbyname_async_cancel(dns_id);
		return NSAPI_ERROR_TIMEOUT;
	}

	if (dns_result < 0) {
		// The result itself errored, return that value
		return dns_result;
	}

	// Everything is fine, set the address and return
	addr = dns_addr;
	return NSAPI_ERROR_OK;
}

time_t ntp_seconds_to_unix(uint32_t ntp_time) {
	// NTP time starts at 1900, our time starts at 1970
	// Subtract 70 years from NTP time to compensate
	// We use unsigned ints here, so if NTP gives us a
	// year before 70 we will wrap around and have the
	// correct post 2038 year up to year 2106
	// Hopefully by then we will have migrated to NTPv5
	uint32_t offset = 2208988800;
	return ntp_time - offset;
}

unsigned long ntp_fraction_to_microseconds(uint32_t ntp_fraction) {
	// NTP fractions are 1/4294967296 of a second
	// microseconds are 1/1000000 of a second
	// We need to convert between the two
	//
	// The process to convert between the two is:
	// 1. Divide the current fraction back in to seconds
	// 2. Multiply the seconds in to the new fraction (NTP or microseconds)
	// For example:
	// 1. NTP fraction 3439758684 becomes 0.800881 seconds
	// 2. 0.800881 seconds becomes 800881 microseconds
	//
	// Here we use a 32-bit float so we can convert between different
	// magnitudes of numbers without data loss or overflow.
	// The actual floating value is only 24-bit which is enough to store
	// calculate the microseconds without losing precision.
	float seconds = ntp_fraction / 4294967296.0;
	float microseconds = seconds * 1000000.0;
	return (unsigned long)microseconds;
}

unsigned long unpack32(unsigned char *p) {
	unsigned long l = 0;
	l |= ((unsigned long)p[0]) << 24;
	l |= ((unsigned long)p[1]) << 16;
	l |= ((unsigned long)p[2]) << 8;
	l |= ((unsigned long)p[3]) << 0;
	return l;
}

SNTPError sntp(NetworkInterface &net, const char *server, unsigned int port,
	struct timeval *tv) {
	SocketAddress addr;
	UDPSocket sock;
	nsapi_error_t err;
	unsigned char pkt[48] = {
		0x23, // Bits: 00 (No leap second) 100 (NTPv4) 011 (Client)
	};

	tr_info("Getting SNTP time from %s", server);

	err = gethostbyname_timeout(net, server, addr, 1000);
	if (err != NSAPI_ERROR_OK) {
		tr_err("gethostbyname error: %i", err);
		return SNTPDnsError;
	}

	addr.set_port(port);
	tr_debug("SNTP server: %s:%d", addr.get_ip_address(), addr.get_port());

	err = sock.open(&net);
	if (err != NSAPI_ERROR_OK) {
		tr_err("socket open error: %i", err);
		return SNTPOpenError;
	}

	sock.set_timeout(1000);

	err = sock.connect(addr);
	if (err != NSAPI_ERROR_OK) {
		tr_err("socket connect error: %i", err);
		return SNTPConnectError;
	}

	if (sock.send(pkt, sizeof pkt) != sizeof pkt) {
		tr_err("socket send error: %i", err);
		return SNTPSendError;
	}

	if (sock.recv(pkt, sizeof pkt) != sizeof pkt) {
		tr_err("socket receive error: %i", err);
		return SNTPReceiveError;
	}

	if (sock.close() != NSAPI_ERROR_OK) {
		tr_warn("socket close error: %i", err);
	}

	uint32_t ntp_seconds = unpack32(&pkt[40]);
	uint32_t ntp_fractional = unpack32(&pkt[44]);
	tv->tv_sec = ntp_seconds_to_unix(ntp_seconds);
	tv->tv_usec = ntp_fraction_to_microseconds(ntp_fractional);

	tr_debug("SNTP time: %lu seconds %lu fractional", ntp_seconds,
		ntp_fractional);
	tr_debug("Unix time: %llu seconds %lu microseconds", tv->tv_sec,
		tv->tv_usec);

	return SNTPSuccess;
}