diff --git a/include/zephyr/lorawan/lorawan.h b/include/zephyr/lorawan/lorawan.h index 9ce3643f890..e37f7220720 100644 --- a/include/zephyr/lorawan/lorawan.h +++ b/include/zephyr/lorawan/lorawan.h @@ -318,6 +318,38 @@ void lorawan_get_payload_sizes(uint8_t *max_next_payload_size, */ int lorawan_set_region(enum lorawan_region region); +#ifdef CONFIG_LORAWAN_APP_CLOCK_SYNC + +/** + * @brief Run Application Layer Clock Synchronization service + * + * This service sends out its current time in a regular interval (configurable + * via Kconfig) and receives a correction offset from the application server if + * the clock deviation is considered too large. + * + * Clock synchronization is required for firmware upgrades over multicast + * sessions, but can also be used independent of a FUOTA process. + * + * @return 0 if successful, negative errno otherwise. + */ +int lorawan_clock_sync_run(void); + +/** + * @brief Retrieve the current synchronized time + * + * This function uses the GPS epoch format, as used in all LoRaWAN services. + * + * The GPS epoch started on 1980-01-06T00:00:00Z, but has since diverged + * from UTC, as it does not consider corrections like leap seconds. + * + * @param gps_time Synchronized time in GPS epoch format truncated to 32-bit. + * + * @return 0 if successful, -EAGAIN if the clock is not yet synchronized. + */ +int lorawan_clock_sync_get(uint32_t *gps_time); + +#endif /* CONFIG_LORAWAN_APP_CLOCK_SYNC */ + #ifdef __cplusplus } #endif diff --git a/subsys/lorawan/services/CMakeLists.txt b/subsys/lorawan/services/CMakeLists.txt index 7fef9d329b2..3ace16d75bf 100644 --- a/subsys/lorawan/services/CMakeLists.txt +++ b/subsys/lorawan/services/CMakeLists.txt @@ -1,3 +1,4 @@ # SPDX-License-Identifier: Apache-2.0 +zephyr_library_sources_ifdef(CONFIG_LORAWAN_APP_CLOCK_SYNC clock_sync.c) zephyr_library_sources_ifdef(CONFIG_LORAWAN_SERVICES lorawan_services.c) diff --git a/subsys/lorawan/services/Kconfig b/subsys/lorawan/services/Kconfig index b7178124128..face2bf5d5c 100644 --- a/subsys/lorawan/services/Kconfig +++ b/subsys/lorawan/services/Kconfig @@ -33,4 +33,25 @@ config LORAWAN_SERVICES_THREAD_PRIORITY help Priority of the thread running LoRaWAN background services. +config LORAWAN_APP_CLOCK_SYNC + bool "Application Layer Clock Synchronization" + help + Enables the LoRaWAN Application Layer Clock Synchronization service + according to LoRa Alliance TS003-2.0.0. + + The service uses the default port 202. + +config LORAWAN_APP_CLOCK_SYNC_PERIODICITY + int "Application Layer Clock Synchronization periodicity" + depends on LORAWAN_APP_CLOCK_SYNC + range 128 4194304 + default 86400 + help + Initial setting for clock synchronization periodicity in seconds. + + The value can be updated remotely by the application server within a + range from 128 (0x80) to 4194304 (0x400000). + + Default setting: 24h. + endif # LORAWAN_SERVICES diff --git a/subsys/lorawan/services/clock_sync.c b/subsys/lorawan/services/clock_sync.c new file mode 100644 index 00000000000..2f627d30dea --- /dev/null +++ b/subsys/lorawan/services/clock_sync.c @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2022 Martin Jäger + * Copyright (c) 2022 tado GmbH + * + * Parts of this implementation were inspired by LmhpClockSync.c from the + * LoRaMac-node firmware repository https://github.com/Lora-net/LoRaMac-node + * written by Miguel Luis (Semtech). + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "lorawan_services.h" + +#include +#include +#include +#include +#include + +LOG_MODULE_REGISTER(lorawan_clock_sync, CONFIG_LORAWAN_SERVICES_LOG_LEVEL); + +/** + * Version of LoRaWAN Application Layer Clock Synchronization Specification + * + * This implementation only supports TS003-2.0.0, as the previous revision TS003-1.0.0 + * requested to temporarily disable ADR and and set nb_trans to 1. This causes issues on the + * server side and is not recommended anymore. + */ +#define CLOCK_SYNC_PACKAGE_VERSION 2 + +/* Maximum length of clock sync answers */ +#define MAX_CLOCK_SYNC_ANS_LEN 6 + +/* Delay between consecutive transmissions of AppTimeReq */ +#define CLOCK_RESYNC_DELAY 10 + +enum clock_sync_commands { + CLOCK_SYNC_CMD_PKG_VERSION = 0x00, + CLOCK_SYNC_CMD_APP_TIME = 0x01, + CLOCK_SYNC_CMD_DEVICE_APP_TIME_PERIODICITY = 0x02, + CLOCK_SYNC_CMD_FORCE_DEVICE_RESYNC = 0x03, +}; + +struct clock_sync_context { + /** Work item for regular (re-)sync requests (uplink messages) */ + struct k_work_delayable resync_work; + /** Continuously incremented token to map clock sync answers and requests */ + uint8_t req_token; + /** Number of requested clock sync requests left to be transmitted */ + uint8_t nb_transmissions; + /** + * Offset to be added to system uptime to get GPS time (as used by LoRaWAN) + */ + uint32_t time_offset; + /** + * AppTimeReq retransmission interval in seconds + * + * Valid range between 128 (0x80) and 8388608 (0x800000) + */ + uint32_t periodicity; + /** Indication if at least one valid time correction was received */ + bool synchronized; +}; + +static struct clock_sync_context ctx; + +/** + * Writes the DeviceTime into the buffer. + * + * @returns number of bytes written or -ENOSPC in case of error + */ +static int clock_sync_serialize_device_time(uint8_t *buf, size_t size) +{ + uint32_t device_time = k_uptime_get() / MSEC_PER_SEC + ctx.time_offset; + + if (size < sizeof(uint32_t)) { + return -ENOSPC; + } + + buf[0] = (device_time >> 0) & 0xFF; + buf[1] = (device_time >> 8) & 0xFF; + buf[2] = (device_time >> 16) & 0xFF; + buf[3] = (device_time >> 24) & 0xFF; + + return sizeof(uint32_t); +} + +static void clock_sync_package_callback(uint8_t port, bool data_pending, int16_t rssi, int8_t snr, + uint8_t len, const uint8_t *rx_buf) +{ + uint8_t tx_buf[3 * MAX_CLOCK_SYNC_ANS_LEN]; + uint8_t tx_pos = 0; + uint8_t rx_pos = 0; + + __ASSERT(port == LORAWAN_PORT_CLOCK_SYNC, "Wrong port %d", port); + + while (rx_pos < len) { + uint8_t command_id = rx_buf[rx_pos++]; + + if (sizeof(tx_buf) - tx_pos < MAX_CLOCK_SYNC_ANS_LEN) { + LOG_ERR("insufficient tx_buf size, some requests discarded"); + break; + } + + switch (command_id) { + case CLOCK_SYNC_CMD_PKG_VERSION: + tx_buf[tx_pos++] = CLOCK_SYNC_CMD_PKG_VERSION; + tx_buf[tx_pos++] = LORAWAN_PACKAGE_ID_CLOCK_SYNC; + tx_buf[tx_pos++] = CLOCK_SYNC_PACKAGE_VERSION; + LOG_DBG("PackageVersionReq"); + break; + case CLOCK_SYNC_CMD_APP_TIME: { + /* answer from application server */ + int32_t time_correction; + + ctx.nb_transmissions = 0; + + time_correction = rx_buf[rx_pos++]; + time_correction += rx_buf[rx_pos++] << 8; + time_correction += rx_buf[rx_pos++] << 16; + time_correction += rx_buf[rx_pos++] << 24; + + uint8_t token = rx_buf[rx_pos++] & 0x0F; + + if (token == ctx.req_token) { + ctx.time_offset += time_correction; + ctx.req_token = (ctx.req_token + 1) % 16; + ctx.synchronized = true; + + LOG_DBG("AppTimeAns time_correction %d (token %d)", + time_correction, token); + } else { + LOG_WRN("AppTimeAns with outdated token %d", token); + } + break; + } + case CLOCK_SYNC_CMD_DEVICE_APP_TIME_PERIODICITY: { + uint8_t period = rx_buf[rx_pos++] & 0x0F; + + ctx.periodicity = 1U << (period + 7); + + tx_buf[tx_pos++] = CLOCK_SYNC_CMD_DEVICE_APP_TIME_PERIODICITY; + tx_buf[tx_pos++] = 0x00; /* Status: OK */ + + tx_pos += clock_sync_serialize_device_time(tx_buf + tx_pos, + sizeof(tx_buf) - tx_pos); + + LOG_DBG("DeviceAppTimePeriodicityReq period: %u", period); + break; + } + case CLOCK_SYNC_CMD_FORCE_DEVICE_RESYNC: { + uint8_t nb_transmissions = rx_buf[rx_pos++] & 0x07; + + if (nb_transmissions != 0) { + ctx.nb_transmissions = nb_transmissions; + lorawan_services_reschedule_work(&ctx.resync_work, K_NO_WAIT); + } + + LOG_DBG("ForceDeviceResyncCmd nb_transmissions: %u", nb_transmissions); + break; + } + default: + return; + } + } + + if (tx_pos > 0) { + lorawan_services_schedule_uplink(LORAWAN_PORT_CLOCK_SYNC, tx_buf, tx_pos, 0); + } +} + +static int clock_sync_app_time_req(void) +{ + uint8_t tx_pos = 0; + uint8_t tx_buf[6]; + + tx_buf[tx_pos++] = CLOCK_SYNC_CMD_APP_TIME; + tx_pos += clock_sync_serialize_device_time(tx_buf + tx_pos, + sizeof(tx_buf) - tx_pos); + + /* Param: AnsRequired = 0 | TokenReq */ + tx_buf[tx_pos++] = ctx.req_token; + + LOG_DBG("Sending clock sync AppTimeReq (token %d)", ctx.req_token); + + lorawan_services_schedule_uplink(LORAWAN_PORT_CLOCK_SYNC, tx_buf, tx_pos, 0); + + if (ctx.nb_transmissions > 0) { + ctx.nb_transmissions--; + lorawan_services_reschedule_work(&ctx.resync_work, K_SECONDS(CLOCK_RESYNC_DELAY)); + } + + return 0; +} + +static void clock_sync_resync_handler(struct k_work *work) +{ + uint32_t periodicity; + + clock_sync_app_time_req(); + + /* Add +-30s jitter to actual periodicity as required */ + periodicity = ctx.periodicity - 30 + sys_rand32_get() % 61; + + lorawan_services_reschedule_work(&ctx.resync_work, K_SECONDS(periodicity)); +} + +int lorawan_clock_sync_get(uint32_t *gps_time) +{ + __ASSERT(gps_time != NULL, "gps_time parameter is required"); + + if (ctx.synchronized) { + *gps_time = (uint32_t)(k_uptime_get() / MSEC_PER_SEC + ctx.time_offset); + return 0; + } else { + return -EAGAIN; + } +} + +static struct lorawan_downlink_cb downlink_cb = { + .port = (uint8_t)LORAWAN_PORT_CLOCK_SYNC, + .cb = clock_sync_package_callback +}; + +int lorawan_clock_sync_run(void) +{ + ctx.periodicity = CONFIG_LORAWAN_APP_CLOCK_SYNC_PERIODICITY; + + lorawan_register_downlink_callback(&downlink_cb); + + k_work_init_delayable(&ctx.resync_work, clock_sync_resync_handler); + lorawan_services_reschedule_work(&ctx.resync_work, K_NO_WAIT); + + return 0; +}