aboutsummaryrefslogtreecommitdiffstats
path: root/lib/ClockTAI.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'lib/ClockTAI.cpp')
-rw-r--r--lib/ClockTAI.cpp562
1 files changed, 562 insertions, 0 deletions
diff --git a/lib/ClockTAI.cpp b/lib/ClockTAI.cpp
new file mode 100644
index 0000000..42497f4
--- /dev/null
+++ b/lib/ClockTAI.cpp
@@ -0,0 +1,562 @@
+/*
+ Copyright (C) 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
+ 2011, 2012 Her Majesty the Queen in Right of Canada (Communications
+ Research Center Canada)
+
+ Copyright (C) 2019
+ Matthias P. Braendli, matthias.braendli@mpb.li
+
+ http://www.opendigitalradio.org
+ */
+/*
+ This file is part of ODR-DabMux.
+
+ ODR-DabMux is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as
+ published by the Free Software Foundation, either version 3 of the
+ License, or (at your option) any later version.
+
+ ODR-DabMux is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with ODR-DabMux. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+/* This file downloads the TAI-UTC bulletins from the from IETF and parses them
+ * so that correct time can be communicated in EDI timestamps.
+ *
+ * This file contains self-test code that can be executed by running
+ * g++ -g -Wall -DTEST -DHAVE_CURL -std=c++11 -lcurl -pthread \
+ * ClockTAI.cpp Log.cpp RemoteControl.cpp -lboost_system -o taitest && ./taitest
+ */
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include "ClockTAI.h"
+#include "Log.h"
+
+#include <time.h>
+#include <stdio.h>
+#include <errno.h>
+#if SUPPORT_SETTING_CLOCK_TAI
+# include <sys/timex.h>
+#endif
+#ifdef HAVE_CURL
+# include <curl/curl.h>
+#endif
+#include <array>
+#include <string>
+#include <iostream>
+#include <algorithm>
+#include <regex>
+
+using namespace std;
+
+#ifdef TEST
+static bool wait_longer = true;
+#endif
+
+constexpr int download_retry_interval_hours = 1;
+
+// Offset between NTP time and POSIX time:
+// timestamp_unix = timestamp_ntp - ntp_unix_offset
+const int64_t ntp_unix_offset = 2208988800L;
+
+// leap seconds insertion bulletin is available from the IETF and in the TZ
+// distribution
+static array<const char*, 2> default_tai_urls = {
+ "https://www.ietf.org/timezones/data/leap-seconds.list",
+ "https://raw.githubusercontent.com/eggert/tz/master/leap-seconds.list",
+};
+
+// According to the Filesystem Hierarchy Standard, the data in
+// /var/tmp "must not be deleted when the system is booted."
+static const char *tai_cache_location = "/var/tmp/odr-dabmux-leap-seconds.cache";
+
+// read TAI offset from a valid bulletin in IETF format
+static int parse_ietf_bulletin(const std::string& bulletin)
+{
+ // Example Line:
+ // 3692217600 37 # 1 Jan 2017
+ //
+ // NTP timestamp<TAB>leap seconds<TAB># some comment
+ // The NTP timestamp starts at epoch 1.1.1900.
+ // The difference between NTP timestamps and unix epoch is 70
+ // years i.e. 2208988800 seconds
+
+ std::regex regex_bulletin(R"(([0-9]+)\s+([0-9]+)\s+#.*)");
+
+ time_t now = time(nullptr);
+
+ int tai_utc_offset = 0;
+
+ int tai_utc_offset_valid = false;
+
+ stringstream ss(bulletin);
+
+ /* We cannot just take the last line, because it might
+ * be in the future, announcing an upcoming leap second.
+ *
+ * So we need to look at the current date, and compare it
+ * with the date of the leap second.
+ */
+ for (string line; getline(ss, line); ) {
+
+ std::smatch bulletin_entry;
+
+ bool is_match = std::regex_search(line, bulletin_entry, regex_bulletin);
+ if (is_match) {
+ if (bulletin_entry.size() != 3) {
+ throw runtime_error(
+ "Incorrect number of matched TAI IETF bulletin entries");
+ }
+ const string bulletin_ntp_timestamp(bulletin_entry[1]);
+ const string bulletin_offset(bulletin_entry[2]);
+
+ const int64_t timestamp_unix =
+ std::atoll(bulletin_ntp_timestamp.c_str()) - ntp_unix_offset;
+
+ const int offset = std::atoi(bulletin_offset.c_str());
+ // Ignore entries announcing leap seconds in the future
+ if (timestamp_unix < now) {
+ tai_utc_offset = offset;
+ tai_utc_offset_valid = true;
+ }
+#if TEST
+ else {
+ cerr << "IETF Ignoring offset " << bulletin_offset <<
+ " at TS " << bulletin_ntp_timestamp <<
+ " in the future" << endl;
+ }
+#endif
+ }
+ }
+
+ if (not tai_utc_offset_valid) {
+ throw runtime_error("No data in TAI bulletin");
+ }
+
+ return tai_utc_offset;
+}
+
+
+struct bulletin_state {
+ bool valid = false;
+ int64_t expiry = 0;
+ int offset = 0;
+
+ bool usable() const { return valid and expiry > 0; }
+};
+
+static bulletin_state parse_bulletin(const string& bulletin)
+{
+ // The bulletin contains one line that specifies an expiration date
+ // in NTP time. If that point in time is in the future, we consider
+ // the bulletin valid.
+ //
+ // The entry looks like this:
+ //#@ 3707596800
+
+ bulletin_state ret;
+
+ std::regex regex_expiration(R"(#@\s+([0-9]+))");
+
+ time_t now = time(nullptr);
+
+ stringstream ss(bulletin);
+
+ for (string line; getline(ss, line); ) {
+ std::smatch bulletin_entry;
+
+ bool is_match = std::regex_search(line, bulletin_entry, regex_expiration);
+ if (is_match) {
+ if (bulletin_entry.size() != 2) {
+ throw runtime_error(
+ "Incorrect number of matched TAI IETF bulletin expiration");
+ }
+ const string expiry_data_str(bulletin_entry[1]);
+ const int64_t expiry_unix =
+ std::atoll(expiry_data_str.c_str()) - ntp_unix_offset;
+
+#ifdef TEST
+ etiLog.level(info) << "Bulletin expires in " << expiry_unix - now;
+#endif
+ ret.expiry = expiry_unix - now;
+ try {
+ ret.offset = parse_ietf_bulletin(bulletin);
+ ret.valid = true;
+ }
+ catch (const runtime_error& e) {
+ etiLog.level(warn) << "Bulletin expiry ok but parse error: " << e.what();
+ }
+ break;
+ }
+ }
+ return ret;
+}
+
+
+// callback that receives data from cURL
+static size_t fill_bulletin(char *ptr, size_t size, size_t nmemb, void *ctx)
+{
+ auto *bulletin = reinterpret_cast<stringstream*>(ctx);
+
+ size_t len = size * nmemb;
+ for (size_t i = 0; i < len; i++) {
+ *bulletin << ptr[i];
+ }
+ return len;
+}
+
+static string download_tai_utc_bulletin(const char* url)
+{
+ stringstream bulletin;
+
+#ifdef HAVE_CURL
+ CURL *curl;
+ CURLcode res;
+
+ curl = curl_easy_init();
+ if (curl) {
+ curl_easy_setopt(curl, CURLOPT_URL, url);
+ /* Tell libcurl to follow redirection */
+ curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fill_bulletin);
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, &bulletin);
+
+ res = curl_easy_perform(curl);
+ /* always cleanup ! */
+ curl_easy_cleanup(curl);
+
+ if (res != CURLE_OK) {
+ throw runtime_error( "TAI-UTC bulletin download failed: " +
+ string(curl_easy_strerror(res)));
+ }
+ }
+ return bulletin.str();
+#else
+ throw runtime_error("Cannot download TAI Clock information without cURL");
+#endif // HAVE_CURL
+}
+
+static string load_bulletin_from_file(const char* cache_filename)
+{
+ // Clear the bulletin
+ ifstream f(cache_filename);
+ if (not f.good()) {
+ return {};
+ }
+
+ stringstream ss;
+ ss << f.rdbuf();
+ f.close();
+
+ return ss.str();
+}
+
+ClockTAI::ClockTAI(const std::vector<std::string>& bulletin_urls) :
+ RemoteControllable("clocktai")
+{
+ RC_ADD_PARAMETER(expiry, "Number of seconds until TAI Bulletin expires");
+
+ if (bulletin_urls.empty()) {
+ etiLog.level(debug) << "Initialising default TAI Bulletin URLs";
+ for (const auto url : default_tai_urls) {
+ m_bulletin_urls.push_back(url);
+ }
+ }
+ else {
+ etiLog.level(debug) << "Initialising user-configured TAI Bulletin URLs";
+ m_bulletin_urls = bulletin_urls;
+ }
+
+ for (const auto url : m_bulletin_urls) {
+ etiLog.level(info) << "TAI Bulletin URL: '" << url << "'";
+ }
+}
+
+int ClockTAI::get_valid_offset()
+{
+ int offset = 0;
+ bool offset_valid = false;
+
+ std::unique_lock<std::mutex> lock(m_data_mutex);
+
+ const auto state = parse_bulletin(m_bulletin);
+ if (state.usable()) {
+#if TEST
+ etiLog.level(info) << "Bulletin already valid";
+#endif
+ offset = state.offset;
+ offset_valid = true;
+ }
+ else {
+ const auto cache_bulletin = load_bulletin_from_file(tai_cache_location);
+ const auto cache_state = parse_bulletin(cache_bulletin);
+
+ if (cache_state.usable()) {
+ m_bulletin = cache_bulletin;
+ offset = cache_state.offset;
+ offset_valid = true;
+#if TEST
+ etiLog.level(info) << "Bulletin from cache valid with offset=" << offset;
+#endif
+ }
+ else {
+ for (const auto url : m_bulletin_urls) {
+ try {
+#if TEST
+ etiLog.level(info) << "Load bulletin from " << url;
+#endif
+ const auto new_bulletin = download_tai_utc_bulletin(url.c_str());
+ const auto new_state = parse_bulletin(new_bulletin);
+ if (new_state.usable()) {
+ m_bulletin = new_bulletin;
+ offset = new_state.offset;
+ offset_valid = true;
+
+ etiLog.level(debug) << "Loaded valid TAI Bulletin from " <<
+ url << " giving offset=" << offset;
+ }
+ else {
+ etiLog.level(debug) << "Skipping invalid TAI bulletin from "
+ << url;
+ }
+ }
+ catch (const runtime_error& e) {
+ etiLog.level(warn) <<
+ "TAI-UTC offset could not be retrieved from " <<
+ url << " : " << e.what();
+ }
+
+ if (offset_valid) {
+ update_cache(tai_cache_location);
+ break;
+ }
+ }
+ }
+ }
+
+ if (offset_valid) {
+ // With the current evolution of the offset, we're probably going
+ // to reach 500 long after DAB gets replaced by another standard.
+ if (offset < 0 or offset > 500) {
+ stringstream ss;
+ ss << "TAI offset " << offset << " out of range";
+ throw range_error(ss.str());
+ }
+
+ return offset;
+ }
+ else {
+ // Try again later
+ throw download_failed();
+ }
+}
+
+
+int ClockTAI::get_offset()
+{
+ using namespace std::chrono;
+ const auto time_now = system_clock::now();
+
+ std::unique_lock<std::mutex> lock(m_data_mutex);
+
+ if (not m_offset_valid) {
+#ifdef TEST
+ // Assume we've downloaded it in the past:
+
+ m_offset = 37; // Valid in early 2017
+ m_offset_valid = true;
+
+ // Simulate requiring a new download
+ m_bulletin_download_time = time_now - hours(24 * 40);
+#else
+ // First time we run we must block until we know
+ // the offset
+ lock.unlock();
+ try {
+ m_offset = get_valid_offset();
+ }
+ catch (const download_failed&) {
+ throw runtime_error("Unable to download TAI bulletin");
+ }
+ lock.lock();
+ m_offset_valid = true;
+ m_bulletin_download_time = time_now;
+#endif
+ etiLog.level(info) <<
+ "Initialised TAI-UTC offset to " << m_offset << "s.";
+ }
+
+ if (time_now - m_bulletin_download_time > hours(24 * 31)) {
+ // Refresh if it's older than one month. Leap seconds are
+ // announced several months in advance
+ etiLog.level(debug) << "Trying to refresh TAI bulletin";
+
+ if (m_offset_future.valid()) {
+ auto state = m_offset_future.wait_for(seconds(0));
+ switch (state) {
+ case future_status::ready:
+ try {
+ m_offset = m_offset_future.get();
+ m_offset_valid = true;
+ m_bulletin_download_time = time_now;
+
+ etiLog.level(info) <<
+ "Updated TAI-UTC offset to " << m_offset << "s.";
+ }
+ catch (const download_failed&) {
+ etiLog.level(warn) <<
+ "TAI-UTC download failed, will retry in " <<
+ download_retry_interval_hours << " hour(s)";
+
+ m_bulletin_download_time += hours(download_retry_interval_hours);
+ }
+#ifdef TEST
+ wait_longer = false;
+#endif
+ break;
+
+ case future_status::deferred:
+ case future_status::timeout:
+ // Not ready yet
+#ifdef TEST
+ etiLog.level(debug) << " async not ready yet";
+#endif
+ break;
+ }
+ }
+ else {
+#ifdef TEST
+ etiLog.level(debug) << " Launch async";
+#endif
+ m_offset_future = async(launch::async, &ClockTAI::get_valid_offset, this);
+ }
+ }
+
+ return m_offset;
+}
+
+#if SUPPORT_SETTING_CLOCK_TAI
+int ClockTAI::update_local_tai_clock(int offset)
+{
+ struct timex timex_request;
+ timex_request.modes = ADJ_TAI;
+ timex_request.constant = offset;
+
+ int err = adjtimex(&timex_request);
+ if (err == -1) {
+ perror("adjtimex");
+ }
+
+ printf("adjtimex: %d, tai %d\n", err, timex_request.tai);
+
+ return err;
+}
+#endif
+
+void ClockTAI::update_cache(const char* cache_filename)
+{
+ ofstream f(cache_filename);
+ if (not f.good()) {
+ throw runtime_error("TAI-UTC bulletin open cache for writing");
+ }
+
+ f << m_bulletin;
+ f.close();
+}
+
+
+void ClockTAI::set_parameter(const string& parameter, const string& value)
+{
+ if (parameter == "expiry") {
+ throw ParameterError("Parameter '" + parameter +
+ "' is read-only in controllable " + get_rc_name());
+ }
+ else {
+ throw ParameterError("Parameter '" + parameter +
+ "' is not exported by controllable " + get_rc_name());
+ }
+}
+
+const string ClockTAI::get_parameter(const string& parameter) const
+{
+ if (parameter == "expiry") {
+ std::unique_lock<std::mutex> lock(m_data_mutex);
+ const int64_t expiry = parse_bulletin(m_bulletin).expiry;
+ if (expiry > 0) {
+ return to_string(expiry);
+ }
+ else {
+ return "Bulletin expired or invalid!";
+ }
+ }
+ else {
+ throw ParameterError("Parameter '" + parameter +
+ "' is not exported by controllable " + get_rc_name());
+ }
+}
+
+#if 0
+// Example testing code
+void debug_tai_clk()
+{
+ struct timespec rt_clk;
+
+ int err = clock_gettime(CLOCK_REALTIME, &rt_clk);
+ if (err) {
+ perror("REALTIME clock_gettime failed");
+ }
+
+ struct timespec tai_clk;
+
+ err = clock_gettime(CLOCK_TAI, &tai_clk);
+ if (err) {
+ perror("TAI clock_gettime failed");
+ }
+
+ printf("RT - TAI = %ld\n", rt_clk.tv_sec - tai_clk.tv_sec);
+
+
+ struct timex timex_request;
+ timex_request.modes = 0; // Do not set anything
+
+ err = adjtimex(&timex_request);
+ if (err == -1) {
+ perror("adjtimex");
+ }
+
+ printf("adjtimex: %d, tai %d\n", err, timex_request.tai);
+}
+#endif
+
+#if TEST
+int main(int argc, char **argv)
+{
+ using namespace std;
+
+ ClockTAI tai({});
+
+ while (wait_longer) {
+ try {
+ etiLog.level(info) <<
+ "Offset is " << tai.get_offset();
+ }
+ catch (const exception &e) {
+ etiLog.level(error) <<
+ "Exception " << e.what();
+ }
+
+ this_thread::sleep_for(chrono::seconds(2));
+ }
+
+ return 0;
+}
+#endif
+