diff --git a/CMakeLists.txt b/CMakeLists.txt index e0a1feb..d58f3d0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,12 +19,19 @@ add_compile_options(-DMBED_TRACE_MAX_LEVEL=TRACE_LEVEL_DEBUG) endif() +# Create SNTP project +project(sntp) +file(GLOB SNTP_FILES "${PROJECT_SOURCE_DIR}/third_party/sntp/*.cpp") +add_library(sntp STATIC ${SNTP_FILES}) +target_link_libraries(sntp PRIVATE mbed-netsocket) +target_include_directories(sntp PUBLIC "${PROJECT_SOURCE_DIR}/third_party/sntp/") + # Create Tardis project project(Tardis) file(GLOB SRC_FILES "${PROJECT_SOURCE_DIR}/src/*.cpp") add_executable(Tardis ${SRC_FILES}) target_include_directories(Tardis PRIVATE "${PROJECT_SOURCE_DIR}/include") -target_link_libraries(Tardis PRIVATE mbed-netsocket mbed-os mbed-storage-qspif mbed-usb-msd mbed-storage-littlefs-v2) +target_link_libraries(Tardis PRIVATE mbed-netsocket mbed-os mbed-storage-qspif mbed-usb-msd mbed-storage-littlefs-v2 sntp) target_compile_options(Tardis PRIVATE -Wall -Wextra -Wpedantic -Werror) mbed_set_post_build(Tardis) diff --git a/include/Clock.h b/include/Clock.h new file mode 100644 index 0000000..2ed163b --- /dev/null +++ b/include/Clock.h @@ -0,0 +1,22 @@ +/* +SPDX-License-Identifier: MIT +Copyright (c) 2023 John Watts and the LuminaSensum contributors +*/ + +#ifndef CLOCK_H +#define CLOCK_H + +#include "NetworkInterface.h" +#include "sntp.h" +#include + +void clock_setup(void); + +struct timeval clock_time(void); + +SNTPError clock_sync(NetworkInterface &net); + +const char *clock_timestring(const char *format); +const char *clock_iso8601string(void); + +#endif diff --git a/include/NetworkHandler.h b/include/NetworkHandler.h index 0e86c0d..e70048b 100644 --- a/include/NetworkHandler.h +++ b/include/NetworkHandler.h @@ -12,5 +12,6 @@ void network_disconnect(void); void network_init(void); bool network_is_connected(void); +NetworkInterface *network_interface(void); #endif diff --git a/src/Clock.cpp b/src/Clock.cpp new file mode 100644 index 0000000..93a1f8c --- /dev/null +++ b/src/Clock.cpp @@ -0,0 +1,50 @@ +/* +SPDX-License-Identifier: MIT +Copyright (c) 2023 John Watts and the LuminaSensum contributors +*/ + +#include "Clock.h" +#include "mbed.h" + +void clock_setup(void) { + // mbed reads the RTC at boot, do nothing for now +} + +struct timeval clock_time() { + struct timeval timeval; + timeval.tv_sec = time(NULL); + timeval.tv_usec = 0; + return timeval; +} + +SNTPError clock_sync(NetworkInterface &net) { + struct timeval time; + SNTPError err; + err = sntp(net, "time.google.com", 123, &time); + if (err != SNTPSuccess) { + return err; + } + set_time(time.tv_sec); + return SNTPSuccess; +} + +const char *clock_timestring(const char *format) { + struct timeval time = clock_time(); + struct tm *local_time = localtime(&time.tv_sec); + + static char buffer[64]; + size_t buffer_size = sizeof(buffer); + size_t buffer_written; + buffer_written = strftime(buffer, buffer_size, format, local_time); + + if (buffer_written == 0) { + // It overflowed, so return something useful + return "(time overflow)"; + } else { + return buffer; + } +} + +const char *clock_iso8601string(void) { + return clock_timestring("%Y-%m-%dT%H:%M:%S"); +} diff --git a/src/NetworkHandler.cpp b/src/NetworkHandler.cpp index 5ce289e..24d17c0 100644 --- a/src/NetworkHandler.cpp +++ b/src/NetworkHandler.cpp @@ -66,3 +66,8 @@ // function to check weather the network is connected bool network_is_connected(void) { return is_connected; } + +// allow other code to grab the network interface and use it +// do not hold on to this interface in case it changes + +NetworkInterface *network_interface(void) { return ð } diff --git a/src/main.cpp b/src/main.cpp index fe8f7b7..15c07db 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,6 +5,7 @@ */ #include "ButtonThread.h" +#include "Clock.h" #include "Filesystem.h" #include "FlashErase.h" #include "MainBD.h" @@ -46,12 +47,16 @@ tr_info("Project Tardis"); + clock_setup(); + tr_info("The time is %s", clock_iso8601string()); + mountFilesystem(mainBD, mainFS); Thread buttonThread; buttonThread.start(buttonTask); network_init(); + network_connect(); while (true) { int presses = waitForPresses(600s); @@ -66,6 +71,25 @@ unmountFilesystem(mainBD, mainFS); doErase(mainBD); } + + if (presses == 3) { + if (!network_is_connected()) { + tr_err("Can't sync clock without network " + "connection!"); + continue; + } + NetworkInterface *net = network_interface(); + tr_info("Syncing clock..."); + tr_info("The current time is %s", + clock_iso8601string()); + SNTPError err = clock_sync(*net); + if (err != SNTPSuccess) { + tr_err("Failed to sync clock, error %i", err); + } else { + tr_info("The new time is %s", + clock_iso8601string()); + } + } } unmountFilesystem(mainBD, mainFS); diff --git a/third_party/sntp/sntp.cpp b/third_party/sntp/sntp.cpp new file mode 100644 index 0000000..d956d72 --- /dev/null +++ b/third_party/sntp/sntp.cpp @@ -0,0 +1,155 @@ +/*- 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; +} diff --git a/third_party/sntp/sntp.h b/third_party/sntp/sntp.h new file mode 100644 index 0000000..db63acf --- /dev/null +++ b/third_party/sntp/sntp.h @@ -0,0 +1,25 @@ +/*- 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 */ +#ifndef SNTP_H +#define SNTP_H + +#include "NetworkInterface.h" +#include + +enum SNTPError { + SNTPSuccess = 0, + SNTPDnsError = 1, + SNTPConnectError = 2, + SNTPOpenError = 3, + SNTPSendError = 4, + SNTPReceiveError = 5, +}; + +SNTPError sntp(NetworkInterface &net, const char *server, unsigned int port, + struct timeval *tv); + +#endif