1 /*
2 * Copyright (c) 2022 Martin Jäger <martin@libre.solar>
3 * Copyright (c) 2022 tado GmbH
4 *
5 * Parts of this implementation were inspired by LmhpClockSync.c from the
6 * LoRaMac-node firmware repository https://github.com/Lora-net/LoRaMac-node
7 * written by Miguel Luis (Semtech).
8 *
9 * SPDX-License-Identifier: Apache-2.0
10 */
11
12 #include "lorawan_services.h"
13
14 #include <LoRaMac.h>
15 #include <zephyr/kernel.h>
16 #include <zephyr/lorawan/lorawan.h>
17 #include <zephyr/logging/log.h>
18 #include <zephyr/random/random.h>
19
20 LOG_MODULE_REGISTER(lorawan_clock_sync, CONFIG_LORAWAN_SERVICES_LOG_LEVEL);
21
22 /**
23 * Version of LoRaWAN Application Layer Clock Synchronization Specification
24 *
25 * This implementation only supports TS003-2.0.0, as the previous revision TS003-1.0.0
26 * requested to temporarily disable ADR and set nb_trans to 1. This causes issues on the
27 * server side and is not recommended anymore.
28 */
29 #define CLOCK_SYNC_PACKAGE_VERSION 2
30
31 /* Maximum length of clock sync answers */
32 #define MAX_CLOCK_SYNC_ANS_LEN 6
33
34 /* Delay between consecutive transmissions of AppTimeReq */
35 #define CLOCK_RESYNC_DELAY 10
36
37 enum clock_sync_commands {
38 CLOCK_SYNC_CMD_PKG_VERSION = 0x00,
39 CLOCK_SYNC_CMD_APP_TIME = 0x01,
40 CLOCK_SYNC_CMD_DEVICE_APP_TIME_PERIODICITY = 0x02,
41 CLOCK_SYNC_CMD_FORCE_DEVICE_RESYNC = 0x03,
42 };
43
44 struct clock_sync_context {
45 /** Work item for regular (re-)sync requests (uplink messages) */
46 struct k_work_delayable resync_work;
47 /** Continuously incremented token to map clock sync answers and requests */
48 uint8_t req_token;
49 /** Number of requested clock sync requests left to be transmitted */
50 uint8_t nb_transmissions;
51 /**
52 * Offset to be added to system uptime to get GPS time (as used by LoRaWAN)
53 */
54 uint32_t time_offset;
55 /**
56 * AppTimeReq retransmission interval in seconds
57 *
58 * Valid range between 128 (0x80) and 8388608 (0x800000)
59 */
60 uint32_t periodicity;
61 /** Indication if at least one valid time correction was received */
62 bool synchronized;
63 };
64
65 static struct clock_sync_context ctx;
66
67 /**
68 * Writes the DeviceTime into the buffer.
69 *
70 * @returns number of bytes written or -ENOSPC in case of error
71 */
clock_sync_serialize_device_time(uint8_t * buf,size_t size)72 static int clock_sync_serialize_device_time(uint8_t *buf, size_t size)
73 {
74 uint32_t device_time = k_uptime_seconds() + ctx.time_offset;
75
76 if (size < sizeof(uint32_t)) {
77 return -ENOSPC;
78 }
79
80 buf[0] = (device_time >> 0) & 0xFF;
81 buf[1] = (device_time >> 8) & 0xFF;
82 buf[2] = (device_time >> 16) & 0xFF;
83 buf[3] = (device_time >> 24) & 0xFF;
84
85 return sizeof(uint32_t);
86 }
87
clock_sync_calc_periodicity(void)88 static inline k_timeout_t clock_sync_calc_periodicity(void)
89 {
90 /* add +-30s jitter to nominal periodicity as required by the spec */
91 return K_SECONDS(ctx.periodicity - 30 + sys_rand32_get() % 61);
92 }
93
clock_sync_package_callback(uint8_t port,bool data_pending,int16_t rssi,int8_t snr,uint8_t len,const uint8_t * rx_buf)94 static void clock_sync_package_callback(uint8_t port, bool data_pending, int16_t rssi, int8_t snr,
95 uint8_t len, const uint8_t *rx_buf)
96 {
97 uint8_t tx_buf[3 * MAX_CLOCK_SYNC_ANS_LEN];
98 uint8_t tx_pos = 0;
99 uint8_t rx_pos = 0;
100
101 __ASSERT(port == LORAWAN_PORT_CLOCK_SYNC, "Wrong port %d", port);
102
103 while (rx_pos < len) {
104 uint8_t command_id = rx_buf[rx_pos++];
105
106 if (sizeof(tx_buf) - tx_pos < MAX_CLOCK_SYNC_ANS_LEN) {
107 LOG_ERR("insufficient tx_buf size, some requests discarded");
108 break;
109 }
110
111 switch (command_id) {
112 case CLOCK_SYNC_CMD_PKG_VERSION:
113 tx_buf[tx_pos++] = CLOCK_SYNC_CMD_PKG_VERSION;
114 tx_buf[tx_pos++] = LORAWAN_PACKAGE_ID_CLOCK_SYNC;
115 tx_buf[tx_pos++] = CLOCK_SYNC_PACKAGE_VERSION;
116 LOG_DBG("PackageVersionReq");
117 break;
118 case CLOCK_SYNC_CMD_APP_TIME: {
119 /* answer from application server */
120 int32_t time_correction;
121
122 ctx.nb_transmissions = 0;
123
124 time_correction = rx_buf[rx_pos++];
125 time_correction += rx_buf[rx_pos++] << 8;
126 time_correction += rx_buf[rx_pos++] << 16;
127 time_correction += rx_buf[rx_pos++] << 24;
128
129 uint8_t token = rx_buf[rx_pos++] & 0x0F;
130
131 if (token == ctx.req_token) {
132 ctx.time_offset += time_correction;
133 ctx.req_token = (ctx.req_token + 1) % 16;
134 ctx.synchronized = true;
135
136 LOG_DBG("AppTimeAns time_correction %d (token %d)",
137 time_correction, token);
138 } else {
139 LOG_WRN("AppTimeAns with outdated token %d", token);
140 }
141 break;
142 }
143 case CLOCK_SYNC_CMD_DEVICE_APP_TIME_PERIODICITY: {
144 uint8_t period = rx_buf[rx_pos++] & 0x0F;
145
146 ctx.periodicity = 1U << (period + 7);
147
148 tx_buf[tx_pos++] = CLOCK_SYNC_CMD_DEVICE_APP_TIME_PERIODICITY;
149 tx_buf[tx_pos++] = 0x00; /* Status: OK */
150
151 tx_pos += clock_sync_serialize_device_time(tx_buf + tx_pos,
152 sizeof(tx_buf) - tx_pos);
153
154 lorawan_services_reschedule_work(&ctx.resync_work,
155 clock_sync_calc_periodicity());
156
157 LOG_DBG("DeviceAppTimePeriodicityReq period: %u", period);
158 break;
159 }
160 case CLOCK_SYNC_CMD_FORCE_DEVICE_RESYNC: {
161 uint8_t nb_transmissions = rx_buf[rx_pos++] & 0x07;
162
163 if (nb_transmissions != 0) {
164 ctx.nb_transmissions = nb_transmissions;
165 lorawan_services_reschedule_work(&ctx.resync_work, K_NO_WAIT);
166 }
167
168 LOG_DBG("ForceDeviceResyncCmd nb_transmissions: %u", nb_transmissions);
169 break;
170 }
171 default:
172 return;
173 }
174 }
175
176 if (tx_pos > 0) {
177 lorawan_services_schedule_uplink(LORAWAN_PORT_CLOCK_SYNC, tx_buf, tx_pos, 0);
178 }
179 }
180
clock_sync_app_time_req(void)181 static int clock_sync_app_time_req(void)
182 {
183 uint8_t tx_pos = 0;
184 uint8_t tx_buf[6];
185
186 if (lorawan_services_class_c_active() > 0) {
187 /* avoid disturbing the session and causing potential package loss */
188 LOG_DBG("AppTimeReq not sent because of active class C session");
189 return -EBUSY;
190 }
191
192 tx_buf[tx_pos++] = CLOCK_SYNC_CMD_APP_TIME;
193 tx_pos += clock_sync_serialize_device_time(tx_buf + tx_pos,
194 sizeof(tx_buf) - tx_pos);
195
196 /* Param: AnsRequired = 0 | TokenReq */
197 tx_buf[tx_pos++] = ctx.req_token;
198
199 LOG_DBG("Sending clock sync AppTimeReq (token %d)", ctx.req_token);
200
201 lorawan_services_schedule_uplink(LORAWAN_PORT_CLOCK_SYNC, tx_buf, tx_pos, 0);
202
203 return 0;
204 }
205
clock_sync_resync_handler(struct k_work * work)206 static void clock_sync_resync_handler(struct k_work *work)
207 {
208 clock_sync_app_time_req();
209
210 if (ctx.nb_transmissions > 0) {
211 ctx.nb_transmissions--;
212 lorawan_services_reschedule_work(&ctx.resync_work, K_SECONDS(CLOCK_RESYNC_DELAY));
213 } else {
214 lorawan_services_reschedule_work(&ctx.resync_work,
215 clock_sync_calc_periodicity());
216 }
217 }
218
lorawan_clock_sync_get(uint32_t * gps_time)219 int lorawan_clock_sync_get(uint32_t *gps_time)
220 {
221 __ASSERT(gps_time != NULL, "gps_time parameter is required");
222
223 if (ctx.synchronized) {
224 *gps_time = k_uptime_seconds() + ctx.time_offset;
225 return 0;
226 } else {
227 return -EAGAIN;
228 }
229 }
230
231 static struct lorawan_downlink_cb downlink_cb = {
232 .port = (uint8_t)LORAWAN_PORT_CLOCK_SYNC,
233 .cb = clock_sync_package_callback
234 };
235
lorawan_clock_sync_run(void)236 int lorawan_clock_sync_run(void)
237 {
238 ctx.periodicity = CONFIG_LORAWAN_APP_CLOCK_SYNC_PERIODICITY;
239
240 lorawan_register_downlink_callback(&downlink_cb);
241
242 k_work_init_delayable(&ctx.resync_work, clock_sync_resync_handler);
243 lorawan_services_reschedule_work(&ctx.resync_work, K_NO_WAIT);
244
245 return 0;
246 }
247