SatCat5
ntp_client.cc
1 // Copyright 2024-2025 The Aerospace Corporation.
3 // This file is a part of SatCat5, licensed under CERN-OHL-W v2 or later.
5 
6 #include <satcat5/log.h>
7 #include <satcat5/ntp_client.h>
8 #include <satcat5/ptp_tracking.h>
9 #include <satcat5/utils.h>
10 
12 using satcat5::log::Log;
15 using satcat5::ptp::Time;
16 using satcat5::udp::PORT_NTP_SERVER;
17 
18 // Set debugging verbosity (0/1/2)
19 static constexpr unsigned DEBUG_VERBOSE = 0;
20 
21 // Type filter for UDP sockets is the server port number.
22 satcat5::net::Type TYPE_NTP(PORT_NTP_SERVER.value);
23 
24 // Assume the offset from TAI to UTC is constant.
25 // The value provided below is valid from 2017 to 2035.
26 #ifndef SATCAT5_UTC_OFFSET
27 #define SATCAT5_UTC_OFFSET 37
28 #endif
29 
30 // The effective NTP epoch is 1900-01-01T00:00:00UTC + L, where L is the
31 // current TAI-UTC offset (i.e., SATCAT5_UTC_OFFSET). Convert this to the
32 // PTP epoch, which is 1970-01-01T00:00:00TAI.
33 // See also: https://www.ntp.org/reflib/leap/
34 // See also: https://stackoverflow.com/questions/29112071/
35 constexpr u64 NTP_OFFSET_SEC = 2208988800ull - SATCAT5_UTC_OFFSET;
36 
37 Client::Client(
40  : Protocol(TYPE_NTP)
41  , m_refclk(refclk)
42  , m_iface(iface)
43  , m_reftime(0)
44  , m_leap(Header::LEAP_NONE) // No awareness of upcoming leap-seconds.
45  , m_stratum(0)
46  , m_rate(0)
47 {
48  m_iface.udp()->add(this);
49 }
50 
51 #if SATCAT5_ALLOW_DELETION
52 Client::~Client() {
53  m_iface.udp()->remove(this);
54 }
55 #endif
56 
57 void Client::client_connect(const satcat5::ip::Addr& server, s8 poll_rate) {
58  if (DEBUG_VERBOSE > 0) Log(DEBUG, "NtpClient: client_connect").write(server);
59  m_iface.connect(server, PORT_NTP_SERVER, PORT_NTP_SERVER);
60  client_set_rate(poll_rate);
61 }
62 
64  if (DEBUG_VERBOSE > 0) Log(DEBUG, "NtpClient: client_close").write(m_iface.dstaddr());
65  m_iface.close();
66 }
67 
68 void Client::client_set_rate(s8 poll_rate) {
69  m_rate = poll_rate;
70  timer_every(1000 << poll_rate);
71 }
72 
74  // Note the receive timestamp as soon as possible.
75  u64 rxtime = ntp_now();
76  if (DEBUG_VERBOSE > 1) Log(DEBUG, "NtpClient: frame_rcvd").write(rxtime);
77 
78  // Read and sanity-check the incoming NTP message.
79  // (Our NTPv4 client/server is backwards-compatible with NTPv3.)
80  Header msg;
81  if (!msg.read_from(&src)) return;
82  if (msg.vn() < Header::VERSION_3) return;
83  if (msg.vn() > Header::VERSION_4) return;
84 
85  // How should we respond? (RFC-5905 Section 9.2)
86  if (msg.mode() == Header::MODE_SERVER) {
87  // Ignore anything that doesn't come from the expected server.
88  // TODO: Support broadcast mode for auto-association?
89  if (m_iface.udp()->reply_ip() == m_iface.dstaddr())
90  rcvd_reply(msg, rxtime);
91  } else if (msg.mode() == Header::MODE_CLIENT) {
92  // If server mode is active, respond to client queries.
93  if (m_stratum) send_reply(msg, rxtime);
94  }
95 }
96 
98  // The only timer event is for starting each client-mode query.
99  send_query();
100 }
101 
102 void Client::rcvd_reply(const Header& msg, u64 rxtime) {
103  if (DEBUG_VERBOSE > 0) Log(DEBUG, "NtpClient: rcvd_reply").write(msg.stratum);
104  if (msg.stratum == 0) {
105  // Check for kiss-of-death codes (Section 7.4).
106  if (msg.refid == Header::KISS_DENY) client_close();
107  if (msg.refid == Header::KISS_RSTR) client_close();
108  if (msg.refid == Header::KISS_RATE) client_set_rate(m_rate + 1);
109  } else {
110  // Update protocol state.
111  m_leap = msg.li();
112  m_reftime = msg.xmt;
113  m_stratum = msg.stratum + 1;
114  // Deliver completed measurement to callback(s).
116  m.t1 = to_ptp(msg.org);
117  m.t2 = to_ptp(msg.rec);
118  m.t3 = to_ptp(msg.xmt);
119  m.t4 = to_ptp(rxtime);
120  notify_callbacks(m);
121  }
122 }
123 
124 bool Client::send_reply(const Header& query, u64 rxtime) {
125  if (DEBUG_VERBOSE > 0) Log(DEBUG, "NtpClient: send_reply");
126  auto wr = m_iface.udp()->open_reply(TYPE_NTP, Header::HEADER_LEN);
127  bool ok = false;
128  if (wr) {
129  // Formulate and send the SNTP reply (Section 14).
130  Header msg;
131  msg.lvm = m_leap | query.vn() | Header::MODE_SERVER;
132  msg.stratum = m_stratum;
133  msg.poll = query.poll;
134  msg.precision = Header::TIME_1USEC;
135  msg.rootdelay = 0; // TODO: Fill this in?
136  msg.rootdisp = 0; // TODO: Fill this in?
137  msg.refid = m_iface.udp()->ipaddr().value;
138  msg.ref = m_reftime;
139  msg.org = query.xmt;
140  msg.rec = rxtime;
141  msg.xmt = ntp_now();
142  wr->write_obj(msg);
143  ok = wr->write_finalize();
144  } else if (DEBUG_VERBOSE > 1) {
145  Log(DEBUG, "NtpClient: send_reply blocked");
146  }
147  return ok;
148 }
149 
150 bool Client::send_query() {
151  if (DEBUG_VERBOSE > 0) Log(DEBUG, "NtpClient: send_query");
152  auto wr = m_iface.open_write(Header::HEADER_LEN);
153  bool ok = false;
154  if (wr) {
155  // Formulate and send a query to the server.
156  Header msg;
157  msg.lvm = m_leap | Header::VERSION_4 | Header::MODE_CLIENT;
158  msg.stratum = m_stratum;
159  msg.poll = m_rate;
160  msg.precision = Header::TIME_1MSEC;
161  msg.rootdelay = 0;
162  msg.rootdisp = 0;
163  msg.refid = m_iface.udp()->ipaddr().value;
164  msg.ref = m_reftime;
165  msg.org = 0;
166  msg.rec = 0;
167  msg.xmt = ntp_now();
168  wr->write_obj(msg);
169  ok = wr->write_finalize();
170  } else if (DEBUG_VERBOSE > 1) {
171  Log(DEBUG, "NtpClient: send_query blocked");
172  }
173  return ok;
174 }
175 
176 u64 Client::ntp_now() const {
177  return to_ntp(m_refclk->clock_now());
178 }
179 
180 u64 Client::to_ntp(const satcat5::ptp::Time& t) const {
181  // Convert the provided time to NTP format (seconds + fraction).
182  // This conversion is lossy, but correctly handles rollover.
183  u64 sec = u64(t.round_secs()) + NTP_OFFSET_SEC;
184  u64 frac = u64(t.round_nsec()) * 18446744073ull; // 2^64 / 1e9
185  return (sec << 32) + (frac >> 32);
186 }
187 
188 Time Client::to_ptp(u64 t) const {
189  // Convert NTP timestamp to seconds and nanoseconds.
190  s64 secs = s64((t >> 32) - NTP_OFFSET_SEC);
191  u64 nsec = ((t & 0xFFFFFFFFu) * 1000000000ull) >> 32;
192  // Infer era number by comparing against current system time.
193  // (NTP rollover every 2^32 seconds, or about 136 years.)
194  const s64 ROLLOVER = (1ull << 32);
195  s64 ref = m_refclk->clock_now().field_secs();
196  s64 era = satcat5::util::div_round(ref - secs, ROLLOVER);
197  // Convert to PTP timestamp.
198  return Time(secs + era * ROLLOVER, nsec);
199 }
Limited read of next N bytes.
Definition: io_readable.h:255
The Log class creates and formats one log message.
Definition: log.h:195
Log & write(const char *str)
Formatting methods for various data types.
Definition: log.cc:198
void add(satcat5::net::Protocol *proto)
Register a Protocol object.
Definition: net_dispatch.h:55
void remove(satcat5::net::Protocol *proto)
Unregister a Protocol object.
Definition: net_dispatch.h:59
Client and/or server for the Network Time Protocol (NTP).
Definition: ntp_client.h:47
void client_connect(const satcat5::ip::Addr &server, s8 poll_rate=satcat5::ntp::Header::TIME_1MIN)
Enable client mode by connecting to the specified server.
Definition: ntp_client.cc:57
void timer_event() override
Child class MUST override this method.
Definition: ntp_client.cc:97
void client_close()
Close the connection to a remote server.
Definition: ntp_client.cc:63
void client_set_rate(s8 poll_rate)
Set the client polling rate.
Definition: ntp_client.cc:68
satcat5::ptp::Time to_ptp(u64 t) const
Convert NTP timestamp to PTP format.
Definition: ntp_client.cc:188
u64 to_ntp(const satcat5::ptp::Time &t) const
Convert any PTP timestamp to NTP format.
Definition: ntp_client.cc:180
u64 ntp_now() const
Get the current time in NTP format.
Definition: ntp_client.cc:176
void frame_rcvd(satcat5::io::LimitedRead &src) override
Dispatch calls frame_rcvd(...) for each incoming frame with with a matching net::Type value.
Definition: ntp_client.cc:73
void timer_every(unsigned msec)
Configure a repeating notification every X milliseconds.
Definition: polling.cc:321
void notify_callbacks(const satcat5::ptp::Measurement &meas)
Notify all Callback objects of a new Measurement.
Definition: ptp_source.cc:13
High-precision timestamp for use with PTP / IEEE1588.
Definition: ptp_time.h:41
s64 field_secs() const
Read the "seconds" field without intermediate rounding.
Definition: ptp_time.h:62
u32 round_nsec() const
Return "nanoseconds" field after rounding to the nearest nanosecond.
Definition: ptp_time.h:78
s64 round_secs() const
Return "seconds" field after rounding to the nearest nanosecond.
Definition: ptp_time.h:75
Generic interface to a numerically-controlled reference clock.
Definition: ptp_tracking.h:52
virtual satcat5::ptp::Time clock_now()=0
Return the current time if available, TIME_ZERO otherwise.
satcat5::io::Writeable * open_write(unsigned len) override
Open a new frame to the designated address and type.
Definition: udp_core.cc:71
void connect(const satcat5::udp::Addr &dstaddr, const satcat5::eth::MacAddr &dstmac, const satcat5::udp::Port &dstport, const satcat5::udp::Port &srcport=satcat5::udp::PORT_NONE, const satcat5::eth::VlanTag &vtag=satcat5::eth::VTAG_NONE)
Manual address resolution (user supplies IP + MAC).
Definition: udp_core.cc:30
void close() override
Close any open connections and revert to idle.
Definition: udp_core.h:102
Dispatcher sorts incoming UDP messages by port index.
Definition: udp_dispatch.h:20
satcat5::io::Writeable * open_reply(const satcat5::net::Type &type, unsigned nbytes) override
Reply to the sender of the most recent received message.
Definition: udp_dispatch.cc:69
Diagnostic logging to UART and/or Ethernet ports.
constexpr s8 DEBUG
Define basic priority codes for log messages.
Definition: log.h:109
Closed-loop time-tracking clocks and tracking control.
IPv4 address is a 32-bit unsigned integer.
Definition: ip_core.h:15
u32 value
Raw access to the underlying representation.
Definition: ip_core.h:17
u16 value
Raw access to the underlying representation.
Definition: ip_core.h:121
Multipurpose filter for matching fields in network packets.
Definition: net_type.h:38
Message headers for the Network Time Protocol (NTP / IETF RFC-5905).
Definition: ntp_header.h:20
u8 vn() const
Accessors for splitting LI, VN, and Mode fields.
Definition: ntp_header.h:134
u64 org
T1 (Client transmit time)
Definition: ntp_header.h:30
u8 mode() const
Accessors for splitting LI, VN, and Mode fields.
Definition: ntp_header.h:135
u64 xmt
T3 (Server transmit time)
Definition: ntp_header.h:32
static constexpr unsigned HEADER_LEN
The basic header is exactly 12 words = 48 bytes long.
Definition: ntp_header.h:35
u8 li() const
Accessors for splitting LI, VN, and Mode fields.
Definition: ntp_header.h:133
u64 ref
Time of last sync to parent.
Definition: ntp_header.h:29
u32 rootdelay
Round trip delay to grandmaster.
Definition: ntp_header.h:26
bool read_from(satcat5::io::Readable *rd)
Read this header from a data source.
Definition: ntp_header.cc:27
u32 rootdisp
Total dispersion to grandmaster.
Definition: ntp_header.h:27
u8 stratum
Hops to grandmaster (1-15)
Definition: ntp_header.h:23
s8 precision
Precision = 2^N seconds.
Definition: ntp_header.h:25
u64 rec
T2 (Server receive time)
Definition: ntp_header.h:31
u32 refid
Server-ID or KoD code.
Definition: ntp_header.h:28
s8 poll
Interval = 2^N seconds.
Definition: ntp_header.h:24
u8 lvm
Combined LI + VN + Mode.
Definition: ntp_header.h:22
Timestamps and metadata for a two-way time-transfer handshake.
satcat5::ptp::Time t1
Timestamp T1 (A to B / Tx)
satcat5::ptp::Time t2
Timestamp T2 (A to B / Rx)
satcat5::ptp::Time t4
Timestamp T4 (B to A / Rx)
satcat5::ptp::Time t3
Timestamp T3 (B to A / Tx)
Miscellaneous mathematical utility functions.