diff --git a/Makefile b/Makefile index 9ac82ff..231a999 100644 --- a/Makefile +++ b/Makefile @@ -28,13 +28,13 @@ INCLUDES := source #------------------------------------------------------------------------------- # options for code generation #------------------------------------------------------------------------------- -CFLAGS := -Wall -Wextra -Wundef -Wshadow -Wpointer-arith -Wcast-align \ +CFLAGS := -Wall -Wextra -Wundef -Wpointer-arith -Wcast-align \ -O2 -fipa-pta -pipe -ffunction-sections \ $(MACHDEP) CFLAGS += $(INCLUDE) -D__WIIU__ -D__WUT__ -D__WUPS__ -CXXFLAGS := $(CFLAGS) +CXXFLAGS := $(CFLAGS) -std=c++23 ASFLAGS := -g $(ARCH) LDFLAGS = -g $(ARCH) $(RPXSPECS) -Wl,-Map,$(notdir $*.map) $(WUPSSPECS) diff --git a/source/main.cpp b/source/main.cpp index 98e80ca..bff29a2 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -1,318 +1,794 @@ +// SPDX-License-Identifier: MIT + +// standard headers +#include +#include +#include +#include +#include +#include +#include +#include // invoke() +#include +#include // unique_ptr<> +#include // accumulate() +#include +#include // ranges::zip() +#include +#include +#include +#include +#include +#include // pair<> +#include + +// unix headers #include -#include #include #include -#include +#include #include +#include #include -#include -#include -#include -#include +// WUT/WUPS headers #include #include #include -#include #include #include -#include #include +#include -#include -#include -#include -#include +#include "ntp.hpp" -#define SYNCING_ENABLED_CONFIG_ID "enabledSync" -#define DST_ENABLED_CONFIG_ID "enabledDST" -#define NOTIFY_ENABLED_CONFIG_ID "enabledNotify" -#define OFFSET_HOURS_CONFIG_ID "offsetHours" -#define OFFSET_MINUTES_CONFIG_ID "offsetMinutes" -// Seconds between 1900 (NTP epoch) and 2000 (Wii U epoch) -#define NTP_TIMESTAMP_DELTA 3155673600llu + +using namespace std::literals; + + +#define PLUGIN_NAME "Wii U Time Sync" + +#define CFG_HOURS "hours" +#define CFG_MINUTES "minutes" +#define CFG_MSG_DURATION "msg_duration" +#define CFG_NOTIFY "notify" +#define CFG_SERVER "server" +#define CFG_SYNC "sync" +#define CFG_TOLERANCE "tolerance" // Important plugin information. -WUPS_PLUGIN_NAME("Wii U Time Sync"); +WUPS_PLUGIN_NAME(PLUGIN_NAME); WUPS_PLUGIN_DESCRIPTION("A plugin that synchronizes a Wii U's clock to the Internet."); WUPS_PLUGIN_VERSION("v1.1.0"); -WUPS_PLUGIN_AUTHOR("Nightkingale"); +WUPS_PLUGIN_AUTHOR("Nightkingale, Daniel K. O."); WUPS_PLUGIN_LICENSE("MIT"); WUPS_USE_WUT_DEVOPTAB(); -WUPS_USE_STORAGE("Wii U Time Sync"); +WUPS_USE_STORAGE(PLUGIN_NAME); + + +namespace cfg { + int hours = 0; + int minutes = 0; + int msg_duration = 5; + bool notify = true; + char server[512] = "pool.ntp.org"; + bool sync = false; + int tolerance = 200; + + OSTime offset = 0; // combines hours and minutes offsets +} + + +std::atomic in_progress = false; + + +// RAII type that handles the in_progress flag. + +struct progress_error : std::runtime_error { + progress_error() : + std::runtime_error{"progress_error"} + {} +}; + +struct progress_guard { + progress_guard() + { + bool expected_progress = false; + if (!in_progress.compare_exchange_strong(expected_progress, true)) + throw progress_error{}; + } + + ~progress_guard() + { + in_progress = false; + } +}; + + +// The code below implements a wrapper for std::async() that respects a thread limit. + +std::counting_semaphore async_limit{6}; + + +template +struct semaphore_releaser { + Sem& s; + + semaphore_releaser(Sem& s) : + s(s) + {} + + ~semaphore_releaser() + { + s.release(); + } +}; + + +template +[[nodiscard]] +std::future, std::decay_t...>> +limited_async(Func&& func, + Args&&... args) +{ + async_limit.acquire(); + + try { + return std::async(std::launch::async, + [](auto&& f, auto&&... a) -> auto + { + semaphore_releaser guard{async_limit}; + return std::invoke(std::forward(f), + std::forward(a)...); + }, + std::forward(func), + std::forward(args)...); + } + catch (...) { + async_limit.release(); + throw; + } +} + + +#ifdef __WUT__ +// These can usually be found in , but WUT does not provide them. + +constexpr +std::uint64_t +htobe64(std::uint64_t x) +{ + if constexpr (std::endian::native == std::endian::big) + return x; + else + return std::byteswap(x); +} + + +constexpr +std::uint64_t +be64toh(std::uint64_t x) +{ + return htobe64(x); +} + +#endif + + +void +report_error(const std::string& arg) +{ + std::string msg = "[" PLUGIN_NAME "] " + arg; + NotificationModule_AddErrorNotificationEx(msg.c_str(), + cfg::msg_duration, + 1, + {255, 255, 255, 255}, + {160, 32, 32, 255}, + nullptr, + nullptr); +} + + +void +report_info(const std::string& arg) +{ + if (!cfg::notify) + return; + + std::string msg = "[" PLUGIN_NAME "] " + arg; + NotificationModule_AddInfoNotificationEx(msg.c_str(), + cfg::msg_duration, + {255, 255, 255, 255}, + {32, 32, 160, 255}, + nullptr, + nullptr); +} + + +void +report_success(const std::string& arg) +{ + if (!cfg::notify) + return; + + std::string msg = "[" PLUGIN_NAME "] " + arg; + NotificationModule_AddInfoNotificationEx(msg.c_str(), + cfg::msg_duration, + {255, 255, 255, 255}, + {32, 160, 32, 255}, + nullptr, + nullptr); +} + + +// Wrapper for strerror_r() +std::string +errno_to_string(int e) +{ + char buf[100]; + strerror_r(e, buf, sizeof buf); + return buf; +} + + +OSTime +get_utc_time() +{ + return OSGetTime() - cfg::offset; +} + + +double +ntp_to_double(ntp::timestamp t) +{ + return std::ldexp(static_cast(t), -32); +} + + +ntp::timestamp +double_to_ntp(double t) +{ + return std::ldexp(t, 32); +} + + +OSTime +ntp_to_wiiu(ntp::timestamp t) +{ + // Change t from NTP epoch (1900) to Wii U epoch (2000). + // There are 24 leap years in this period. + constexpr std::uint64_t seconds_per_day = 24 * 60 * 60; + constexpr std::uint64_t seconds_offset = seconds_per_day * (100 * 365 + 24); + t -= seconds_offset << 32; + + // Convert from u32.32 to Wii U ticks count. + double dt = ntp_to_double(t); + + // Note: do the conversion in floating point to avoid overflows. + OSTime r = dt * OSTimerClockSpeed; + + return r; +} + + +ntp::timestamp +wiiu_to_ntp(OSTime t) +{ + // Convert from Wii U ticks to seconds. + // Note: do the conversion in floating point to avoid overflows. + double dt = static_cast(t) / OSTimerClockSpeed; + ntp::timestamp r = double_to_ntp(dt); + + // Change r from Wii U epoch (2000) to NTP epoch (1900). + constexpr std::uint64_t seconds_per_day = 24 * 60 * 60; + constexpr std::uint64_t seconds_offset = seconds_per_day * (100 * 365 + 24); + r += seconds_offset << 32; + + return r; +} + + +std::string +to_string(const struct sockaddr_in& addr) +{ + char buf[32]; + return inet_ntop(addr.sin_family, &addr.sin_addr, + buf, sizeof buf); +} + + +std::string +seconds_to_human(double s) +{ + char buf[64]; + + if (std::fabs(s) < 2) // less than 2 seconds + std::snprintf(buf, sizeof buf, "%.3f ms", 1000 * s); + else if (std::fabs(s) < 2 * 60) // less than 2 minutes + std::snprintf(buf, sizeof buf, "%.1f s", s); + else if (std::fabs(s) < 2 * 60 * 60) // less than 2 hours + std::snprintf(buf, sizeof buf, "%.1f min", s / 60); + else if (std::fabs(s) < 2 * 24 * 60 * 60) // less than 2 days + std::snprintf(buf, sizeof buf, "%.1f hrs", s / (60 * 60)); + else + std::snprintf(buf, sizeof buf, "%.1f days", s / (24 * 60 * 60)); + + return buf; +} + + +std::string +format_wiiu_time(OSTime wt) +{ + OSCalendarTime cal; + OSTicksToCalendarTime(wt, &cal); + char buffer[256]; + std::snprintf(buffer, sizeof buffer, + "%04d-%02d-%02d %02d:%02d:%02d.%03d", + cal.tm_year, cal.tm_mon + 1, cal.tm_mday, + cal.tm_hour, cal.tm_min, cal.tm_sec, cal.tm_msec); + return buffer; +} -bool enabledSync = false; -bool enabledDST = false; -bool enabledNotify = true; -int offsetHours = 0; -int offsetMinutes = 0; -// From https://github.com/lettier/ntpclient/blob/master/source/c/main.c -typedef struct +std::string +format_ntp(ntp::timestamp t) { - uint8_t li_vn_mode; // Eight bits. li, vn, and mode. - // li. Two bits. Leap indicator. - // vn. Three bits. Version number of the protocol. - // mode. Three bits. Client will pick mode 3 for client. + OSTime wt = ntp_to_wiiu(t); + return format_wiiu_time(wt); +} - uint8_t stratum; // Eight bits. Stratum level of the local clock. - uint8_t poll; // Eight bits. Maximum interval between successive messages. - uint8_t precision; // Eight bits. Precision of the local clock. - uint32_t rootDelay; // 32 bits. Total round trip delay time. - uint32_t rootDispersion; // 32 bits. Max error aloud from primary clock source. - uint32_t refId; // 32 bits. Reference clock identifier. +std::vector +split(const std::string& input, + const std::string& separators) +{ + using std::string; - uint32_t refTm_s; // 32 bits. Reference time-stamp seconds. - uint32_t refTm_f; // 32 bits. Reference time-stamp fraction of a second. + std::vector result; - uint32_t origTm_s; // 32 bits. Originate time-stamp seconds. - uint32_t origTm_f; // 32 bits. Originate time-stamp fraction of a second. + string::size_type start = 0; + while (start != string::npos) { + auto finish = input.find_first_of(separators, start); + result.push_back(input.substr(start, finish - start)); + start = input.find_first_not_of(separators, finish); + } - uint32_t rxTm_s; // 32 bits. Received time-stamp seconds. - uint32_t rxTm_f; // 32 bits. Received time-stamp fraction of a second. + return result; +} - uint32_t txTm_s; // 32 bits and the most important field the client cares about. Transmit time-stamp seconds. - uint32_t txTm_f; // 32 bits. Transmit time-stamp fraction of a second. -} ntp_packet; // Total: 384 bits or 48 bytes. +extern "C" int32_t CCRSysSetSystemTime(OSTime time); // from nn_ccr +extern "C" BOOL __OSSetAbsoluteSystemTime(OSTime time); // from coreinit -extern "C" int32_t CCRSysSetSystemTime(OSTime time); -extern "C" BOOL __OSSetAbsoluteSystemTime(OSTime time); -bool SetSystemTime(OSTime time) +bool +apply_clock_correction(double correction) { + OSTime correction_ticks = correction * OSTimerClockSpeed; + + OSTime now = OSGetTime(); + OSTime corrected = now + correction_ticks; + nn::pdm::NotifySetTimeBeginEvent(); - if (CCRSysSetSystemTime(time) != 0) { + if (CCRSysSetSystemTime(corrected)) { nn::pdm::NotifySetTimeEndEvent(); return false; } - BOOL res = __OSSetAbsoluteSystemTime(time); + bool res = __OSSetAbsoluteSystemTime(corrected); nn::pdm::NotifySetTimeEndEvent(); - return res != FALSE; + return res; } -OSTime NTPGetTime(const char* hostname) -{ - ntp_packet packet; - memset(&packet, 0, sizeof(packet)); - // Set the first byte's bits to 00,011,011 for li = 0, vn = 3, and mode = 3. The rest will be left set to zero. - packet.li_vn_mode = 0x1b; +// RAII class to close down a socket +struct socket_guard { + int fd; - // Create a socket - int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); - if (sockfd < 0) { - return 0; + socket_guard(int ns, int st, int pr) : + fd{socket(ns, st, pr)} + {} + + ~socket_guard() + { + if (fd != -1) + close(); } - // Get host address by name - struct hostent* server = gethostbyname(hostname); - if (!server) { - return 0; + void + close() + { + ::close(fd); + fd = -1; } +}; - // Prepare socket address - struct sockaddr_in serv_addr; - memset(&serv_addr, 0, sizeof(serv_addr)); - serv_addr.sin_family = AF_INET; - // Copy the server's IP address to the server address structure. - memcpy(&serv_addr.sin_addr.s_addr, server->h_addr, server->h_length); +// Note: hardcoded for IPv4, the Wii U doesn't have IPv6. +std::pair +ntp_query(struct sockaddr_in address) +{ + socket_guard s{PF_INET, SOCK_DGRAM, IPPROTO_UDP}; + if (s.fd == -1) + throw std::runtime_error{"unable to create socket"}; - // Convert the port number integer to network big-endian style and save it to the server address structure. - serv_addr.sin_port = htons(123); // UDP port + connect(s.fd, reinterpret_cast(&address), sizeof address); - // Call up the server using its IP address and port number. - if (connect(sockfd, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) < 0) { - close(sockfd); - return 0; - } + ntp::packet packet; + packet.version(4); + packet.mode(ntp::packet::mode::client); - // Send it the NTP packet it wants. If n == -1, it failed. - if (write(sockfd, &packet, sizeof(packet)) < 0) { - close(sockfd); - return 0; + + unsigned num_send_tries = 0; + try_again_send: + + ntp::timestamp t1 = wiiu_to_ntp(get_utc_time()); + packet.transmit_time = htobe64(t1); + + if (send(s.fd, &packet, sizeof packet, 0) == -1) { + int e = errno; + if (e != ENOMEM) + throw std::runtime_error{"unable to send NTP request: "s + errno_to_string(e)}; + if (++num_send_tries < 4) { + std::this_thread::sleep_for(100ms); + goto try_again_send; + } else + throw std::runtime_error{"no resources for send(), too many retries"}; } - // Wait and receive the packet back from the server. If n == -1, it failed. - if (read(sockfd, &packet, sizeof(packet)) < 0) { - close(sockfd); - return 0; + struct timeval timeout = { 4, 0 }; + fd_set read_set; + + + unsigned num_select_tries = 0; + try_again_select: + + FD_ZERO(&read_set); + FD_SET(s.fd, &read_set); + + if (select(s.fd + 1, &read_set, nullptr, nullptr, &timeout) == -1) { + // Wii U's OS can only handle 16 concurrent select() calls, + // so we may need to try again later. + int e = errno; + if (e != ENOMEM) + throw std::runtime_error{"select() failed: "s + errno_to_string(e)}; + if (++num_select_tries < 4) { + std::this_thread::sleep_for(10ms); + goto try_again_select; + } else + throw std::runtime_error{"no resources for select(), too many retries"}; } - // Close the socket - close(sockfd); - - // These two fields contain the time-stamp seconds as the packet left the NTP server. - // The number of seconds correspond to the seconds passed since 1900. - // ntohl() converts the bit/byte order from the network's to host's "endianness". - packet.txTm_s = ntohl(packet.txTm_s); // Time-stamp seconds. - packet.txTm_f = ntohl(packet.txTm_f); // Time-stamp fraction of a second. - - OSTime tick = 0; - // Convert seconds to ticks and adjust timestamp - tick += OSSecondsToTicks(packet.txTm_s - NTP_TIMESTAMP_DELTA); - // Convert fraction to ticks - tick += OSNanosecondsToTicks((packet.txTm_f * 1000000000llu) >> 32); - return tick; + + if (!FD_ISSET(s.fd, &read_set)) + throw std::runtime_error{"timeout reached"}; + + if (recv(s.fd, &packet, sizeof packet, 0) < 48) + throw std::runtime_error{"invalid NTP response"}; + + ntp::timestamp t4 = wiiu_to_ntp(get_utc_time()); + + ntp::timestamp t1_copy = be64toh(packet.origin_time); + if (t1 != t1_copy) + throw std::runtime_error{"NTP response does not match request: ["s + + format_ntp(t1) + "] vs ["s + + format_ntp(t1_copy) + "]"s}; + + // when our request arrived at the server + ntp::timestamp t2 = be64toh(packet.receive_time); + // when the server sent out a response + ntp::timestamp t3 = be64toh(packet.transmit_time); + + double roundtrip = ntp_to_double((t4 - t1) - (t3 - t2)); + double latency = roundtrip / 2; + + // t4 + correction = t3 + latency + double correction = ntp_to_double(t3) + latency - ntp_to_double(t4); + + return { correction, latency }; } -void updateTime() { - uint64_t roundtripStart = 0; - uint64_t roundtripEnd = 0; - // Get the time from the server. - roundtripStart = OSGetTime(); - OSTime time = NTPGetTime("pool.ntp.org"); // Connect to the time server. - roundtripEnd = OSGetTime(); +// Wrapper for getaddrinfo(), hardcoded for IPv4 + +struct addrinfo_query { + int flags = 0; + int family = AF_UNSPEC; + int socktype = 0; + int protocol = 0; +}; + + +struct addrinfo_result { + int family; + int socktype; + int protocol; + struct sockaddr_in address; + std::optional canonname; +}; + - if (time == 0) { - return; // Probably didn't connect correctly. +std::vector +get_address_info(const std::optional& name, + const std::optional& port = {}, + std::optional query = {}) +{ + // RAII: unique_ptr is used to invoke freeaddrinfo() on function exit + std::unique_ptr + info; + + { + struct addrinfo hints; + const struct addrinfo *hints_ptr = nullptr; + + if (query) { + hints_ptr = &hints; + std::memset(&hints, 0, sizeof hints); + hints.ai_flags = query->flags; + hints.ai_family = query->family; + hints.ai_socktype = query->socktype; + hints.ai_protocol = query->protocol; + } + + struct addrinfo* raw_info = nullptr; + int err = getaddrinfo(name ? name->c_str() : nullptr, + port ? port->c_str() : nullptr, + hints_ptr, + &raw_info); + if (err) + throw std::runtime_error{gai_strerror(err)}; + + info.reset(raw_info); // put it in the smart pointer } - // Calculate the roundtrip time. - uint64_t roundtrip = roundtripEnd - roundtripStart; + std::vector result; + + // walk through the linked list + for (auto a = info.get(); a; a = a->ai_next) { - // Calculate the time it took to get the time from the server. - uint64_t timeTook = roundtrip / 2; + // sanity check: Wii U only supports IPv4 + if (a->ai_addrlen != sizeof(struct sockaddr_in)) + throw std::logic_error{"getaddrinfo() returned invalid result"}; - // Subtract the time it took to get the time from the server. - time -= timeTook; + addrinfo_result item; + item.family = a->ai_family; + item.socktype = a->ai_socktype; + item.protocol = a->ai_protocol, + std::memcpy(&item.address, a->ai_addr, sizeof item.address); + if (a->ai_canonname) + item.canonname = a->ai_canonname; - if (offsetHours < 0) { - time -= OSSecondsToTicks(abs(offsetHours) * 60 * 60); - } else { - time += OSSecondsToTicks(offsetHours * 60 * 60); + result.push_back(std::move(item)); } - if (enabledDST) { - time += OSSecondsToTicks(60 * 60); // DST adds an hour. + return result; +} + + +// ordering operator, so we can put sockaddr_in inside a std::set. +constexpr +bool +operator <(const struct sockaddr_in& a, + const struct sockaddr_in& b) + noexcept +{ + return a.sin_addr.s_addr < b.sin_addr.s_addr; +} + + +void +update_time() +try +{ + progress_guard guard; + + cfg::offset = OSSecondsToTicks(cfg::minutes * 60); + if (cfg::hours < 0) + cfg::offset -= OSSecondsToTicks(-cfg::hours * 60 * 60); + else + cfg::offset += OSSecondsToTicks(cfg::hours * 60 * 60); + + std::vector servers = split(cfg::server, " \t,;"); + + addrinfo_query query = { + .family = AF_INET, + .socktype = SOCK_DGRAM, + .protocol = IPPROTO_UDP + }; + + // First, resolve all the names, in parallel. + // Some IP addresses might be duplicated when we use *.pool.ntp.org. + std::set addresses; + { + std::vector>> infos(servers.size()); + for (auto [info_vec, server] : std::views::zip(infos, servers)) + info_vec = limited_async(get_address_info, server, "123", query); + + for (auto& info_vec : infos) + try { + for (auto info : info_vec.get()) + addresses.insert(info.address); + } + catch (std::exception& e) { + report_error(e.what()); + } } - time += OSSecondsToTicks(offsetMinutes * 60); + // Launch all NTP queries in parallel. + std::vector>> results(addresses.size()); + for (auto [address, result] : std::views::zip(addresses, results)) + result = limited_async(ntp_query, address); + + // Now collect all results. + std::vector corrections; + for (auto [address, result] : std::views::zip(addresses, results)) + try { + auto [correction, latency] = result.get(); + corrections.push_back(correction); + report_info(to_string(address) + + ": correction = "s + seconds_to_human(correction) + + ", latency = "s + seconds_to_human(latency)); + } + catch (std::exception& e) { + report_error(to_string(address) + ": "s + e.what()); + } - OSTime currentTime = OSGetTime(); - int timeDifference = abs(time - currentTime); - if (static_cast(timeDifference) <= OSMillisecondsToTicks(250)) { - return; // Time difference is within 250 milliseconds, no need to update. + if (corrections.empty()) { + report_error("no NTP server could be used"); + return; } - SetSystemTime(time); // This finally sets the console time. + double avg_correction = std::accumulate(corrections.begin(), + corrections.end(), + 0.0) + / corrections.size(); - if (enabledNotify) { - NotificationModule_AddInfoNotification("The time has been changed based on your Internet connection."); + if (std::fabs(avg_correction) * 1000 <= cfg::tolerance) { + report_success("tolerating clock drift (correction is only " + + seconds_to_human(avg_correction) + ")"s); + return; + } + + if (cfg::sync) { + if (!apply_clock_correction(avg_correction)) { + report_error("failed to set system clock"); + return; + } } + + if (cfg::notify) + report_success("clock corrected by " + seconds_to_human(avg_correction)); +} +catch (progress_error&) { + report_info("skipping NTP task: already in progress"); } -INITIALIZE_PLUGIN() { + +INITIALIZE_PLUGIN() +{ WUPSStorageError storageRes = WUPS_OpenStorage(); // Check if the plugin's settings have been saved before. if (storageRes == WUPS_STORAGE_ERROR_SUCCESS) { - if ((storageRes = WUPS_GetBool(nullptr, SYNCING_ENABLED_CONFIG_ID, &enabledSync)) == WUPS_STORAGE_ERROR_NOT_FOUND) { - WUPS_StoreBool(nullptr, SYNCING_ENABLED_CONFIG_ID, enabledSync); - } + if (WUPS_GetBool(nullptr, CFG_SYNC, &cfg::sync) == WUPS_STORAGE_ERROR_NOT_FOUND) + WUPS_StoreBool(nullptr, CFG_SYNC, cfg::sync); - if ((storageRes = WUPS_GetBool(nullptr, DST_ENABLED_CONFIG_ID, &enabledDST)) == WUPS_STORAGE_ERROR_NOT_FOUND) { - WUPS_StoreBool(nullptr, DST_ENABLED_CONFIG_ID, enabledDST); - } + if (WUPS_GetBool(nullptr, CFG_NOTIFY, &cfg::notify) == WUPS_STORAGE_ERROR_NOT_FOUND) + WUPS_StoreBool(nullptr, CFG_NOTIFY, cfg::notify); - if ((storageRes = WUPS_GetBool(nullptr, NOTIFY_ENABLED_CONFIG_ID, &enabledNotify)) == WUPS_STORAGE_ERROR_NOT_FOUND) { - WUPS_StoreBool(nullptr, NOTIFY_ENABLED_CONFIG_ID, enabledNotify); - } + if (WUPS_GetInt(nullptr, CFG_MSG_DURATION, &cfg::msg_duration) == WUPS_STORAGE_ERROR_NOT_FOUND) + WUPS_StoreInt(nullptr, CFG_MSG_DURATION, cfg::msg_duration); - if ((storageRes = WUPS_GetInt(nullptr, OFFSET_HOURS_CONFIG_ID, &offsetHours)) == WUPS_STORAGE_ERROR_NOT_FOUND) { - WUPS_StoreInt(nullptr, OFFSET_HOURS_CONFIG_ID, offsetHours); - } + if (WUPS_GetInt(nullptr, CFG_HOURS, &cfg::hours) == WUPS_STORAGE_ERROR_NOT_FOUND) + WUPS_StoreInt(nullptr, CFG_HOURS, cfg::hours); - if ((storageRes = WUPS_GetInt(nullptr, OFFSET_MINUTES_CONFIG_ID, &offsetMinutes)) == WUPS_STORAGE_ERROR_NOT_FOUND) { - WUPS_StoreInt(nullptr, OFFSET_MINUTES_CONFIG_ID, offsetMinutes); - } + if (WUPS_GetInt(nullptr, CFG_MINUTES, &cfg::minutes) == WUPS_STORAGE_ERROR_NOT_FOUND) + WUPS_StoreInt(nullptr, CFG_MINUTES, cfg::minutes); - NotificationModule_InitLibrary(); // Set up for notifications. - WUPS_CloseStorage(); // Close the storage. - } + if (WUPS_GetInt(nullptr, CFG_TOLERANCE, &cfg::tolerance) == WUPS_STORAGE_ERROR_NOT_FOUND) + WUPS_StoreInt(nullptr, CFG_TOLERANCE, cfg::tolerance); + + if (WUPS_GetString(nullptr, CFG_SERVER, cfg::server, sizeof cfg::server) + == WUPS_STORAGE_ERROR_NOT_FOUND) + WUPS_StoreString(nullptr, CFG_SERVER, cfg::server); - if (enabledSync) { - updateTime(); // Update time when plugin is loaded. + WUPS_CloseStorage(); } -} -void syncingEnabled(ConfigItemBoolean *item, bool value) -{ - (void)item; - // If false, bro is literally a time traveler! - WUPS_StoreBool(nullptr, SYNCING_ENABLED_CONFIG_ID, value); - enabledSync = value; -} + NotificationModule_InitLibrary(); // Set up for notifications. -void savingsEnabled(ConfigItemBoolean *item, bool value) -{ - (void)item; - WUPS_StoreBool(nullptr, DST_ENABLED_CONFIG_ID, value); - enabledDST = value; + if (cfg::sync) + update_time(); // Update time when plugin is loaded. } -void notifyEnabled(ConfigItemBoolean *item, bool value) -{ - (void)item; - WUPS_StoreBool(nullptr, NOTIFY_ENABLED_CONFIG_ID, value); - enabledNotify = value; -} -void onHourOffsetChanged(ConfigItemIntegerRange *item, int32_t offset) +WUPS_GET_CONFIG() { - (void)item; - WUPS_StoreInt(nullptr, OFFSET_HOURS_CONFIG_ID, offset); - offsetHours = offset; -} - -void onMinuteOffsetChanged(ConfigItemIntegerRange *item, int32_t offset) -{ - (void)item; - WUPS_StoreInt(nullptr, OFFSET_MINUTES_CONFIG_ID, offset); - offsetMinutes = offset; -} - -WUPS_GET_CONFIG() { - if (WUPS_OpenStorage() != WUPS_STORAGE_ERROR_SUCCESS) { + if (WUPS_OpenStorage() != WUPS_STORAGE_ERROR_SUCCESS) return 0; - } WUPSConfigHandle settings; - WUPSConfig_CreateHandled(&settings, "Wii U Time Sync"); + WUPSConfig_CreateHandled(&settings, PLUGIN_NAME); WUPSConfigCategoryHandle config; WUPSConfig_AddCategoryByNameHandled(settings, "Configuration", &config); WUPSConfigCategoryHandle preview; WUPSConfig_AddCategoryByNameHandled(settings, "Preview Time", &preview); - WUPSConfigItemBoolean_AddToCategoryHandled(settings, config, "enabledSync", "Syncing Enabled", enabledSync, &syncingEnabled); - WUPSConfigItemBoolean_AddToCategoryHandled(settings, config, "enabledDST", "Daylight Savings", enabledDST, &savingsEnabled); - WUPSConfigItemBoolean_AddToCategoryHandled(settings, config, "enabledNotify", "Receive Notifications", enabledNotify, ¬ifyEnabled); - WUPSConfigItemIntegerRange_AddToCategoryHandled(settings, config, "offsetHours", "Time Offset (hours)", offsetHours, -12, 14, &onHourOffsetChanged); - WUPSConfigItemIntegerRange_AddToCategoryHandled(settings, config, "offsetMinutes", "Time Offset (minutes)", offsetMinutes, 0, 59, &onMinuteOffsetChanged); - - OSCalendarTime ct; - OSTicksToCalendarTime(OSGetTime(), &ct); - char timeString[256]; - snprintf(timeString, 255, "Current Time: %04d-%02d-%02d %02d:%02d:%02d:%04d:%04d\n", ct.tm_year, ct.tm_mon + 1, ct.tm_mday, ct.tm_hour, ct.tm_min, ct.tm_sec, ct.tm_msec, ct.tm_usec); - WUPSConfigItemStub_AddToCategoryHandled(settings, preview, "time", timeString); + WUPSConfigItemBoolean_AddToCategoryHandled(settings, config, CFG_SYNC, + "Syncing Enabled", + cfg::sync, + [](ConfigItemBoolean*, bool value) + { + WUPS_StoreBool(nullptr, CFG_SYNC, value); + cfg::sync = value; + }); + WUPSConfigItemBoolean_AddToCategoryHandled(settings, config, CFG_NOTIFY, + "Show Notifications", + cfg::notify, + [](ConfigItemBoolean*, bool value) + { + WUPS_StoreBool(nullptr, CFG_NOTIFY, value); + cfg::notify = value; + }); + WUPSConfigItemIntegerRange_AddToCategoryHandled(settings, config, CFG_MSG_DURATION, + "Messages Duration (seconds)", + cfg::msg_duration, 0, 30, + [](ConfigItemIntegerRange*, int32_t value) + { + WUPS_StoreInt(nullptr, CFG_MSG_DURATION, + value); + cfg::msg_duration = value; + }); + WUPSConfigItemIntegerRange_AddToCategoryHandled(settings, config, CFG_HOURS, + "Hours Offset", + cfg::hours, -12, 14, + [](ConfigItemIntegerRange*, int32_t value) + { + WUPS_StoreInt(nullptr, CFG_HOURS, value); + cfg::hours = value; + }); + WUPSConfigItemIntegerRange_AddToCategoryHandled(settings, config, CFG_MINUTES, + "Minutes Offset", + cfg::minutes, 0, 59, + [](ConfigItemIntegerRange*, int32_t value) + { + WUPS_StoreInt(nullptr, CFG_MINUTES, + value); + cfg::minutes = value; + }); + WUPSConfigItemIntegerRange_AddToCategoryHandled(settings, config, CFG_TOLERANCE, + "Tolerance (milliseconds)", + cfg::tolerance, 0, 5000, + [](ConfigItemIntegerRange*, int32_t value) + { + WUPS_StoreInt(nullptr, CFG_TOLERANCE, + value); + cfg::tolerance = value; + }); + + // show current NTP server address, no way to change it. + std::string server = "NTP servers: "s + cfg::server; + WUPSConfigItemStub_AddToCategoryHandled(settings, config, CFG_SERVER, server.c_str()); + + WUPSConfigItemStub_AddToCategoryHandled(settings, preview, "time", + format_wiiu_time(OSGetTime()).c_str()); return settings; } -WUPS_CONFIG_CLOSED() { - if (enabledSync) { - std::thread updateTimeThread(updateTime); - updateTimeThread.detach(); // Update time when settings are closed. - } - + +WUPS_CONFIG_CLOSED() +{ + std::jthread update_time_thread(update_time); + update_time_thread.detach(); // Update time when settings are closed. + WUPS_CloseStorage(); // Save all changes. } diff --git a/source/ntp.hpp b/source/ntp.hpp new file mode 100644 index 0000000..451d44b --- /dev/null +++ b/source/ntp.hpp @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT + +#ifndef NTP_HPP +#define NTP_HPP + +#include + + +namespace ntp { + // For details, see https://www.ntp.org/reflib/rfc/rfc5905.txt + + // This is u32.32 fixed-point format, seconds since 1900-01-01. + using timestamp = std::uint64_t; + + // This is a u16.16 fixed-point format. + using short_timestamp = std::uint32_t; + + + // Note: all fields are big-endian + struct packet { + + enum class leap : std::uint8_t { + no_warning = 0 << 6, + one_more_second = 1 << 6, + one_less_second = 2 << 6, + unknown = 3 << 6 + }; + + enum class mode : std::uint8_t { + reserved = 0, + active = 1, + passive = 2, + client = 3, + server = 4, + broadcast = 5, + control = 6, + reserved_private = 7 + }; + + + // Note: all fields are zero-initialized by default constructor. + std::uint8_t lvm = 0; // leap, version and mode + std::uint8_t stratum = 0; // Stratum level of the local clock. + std::int8_t poll_exp = 0; // Maximum interval between successive messages. + std::int8_t precision_exp = 0; // Precision of the local clock. + + short_timestamp root_delay = 0; // Total round trip delay time to the reference clock. + short_timestamp root_dispersion = 0; // Total dispersion to the reference clock. + char reference_id[4] = {0, 0, 0, 0}; // Reference clock identifier. + + timestamp reference_time = 0; // Reference timestamp. + timestamp origin_time = 0; // Origin timestamp, aka T1. + timestamp receive_time = 0; // Receive timestamp, aka T2. + timestamp transmit_time = 0; // Transmit timestamp, aka T3. + + + void leap(leap x) + { + lvm = static_cast(x) | (lvm & 0b0011'1111); + } + + void version(unsigned v) + { + lvm = ((v << 3) & 0b0011'1000) | (lvm & 0b1100'0111); + } + + void mode(mode m) + { + lvm = static_cast(m) | (lvm & 0b1111'1000); + } + + }; + + static_assert(sizeof(packet) == 48); + +} // namespace ntp + + +#endif