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