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 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_get() / MSEC_PER_SEC + 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_package_callback(uint8_t port,bool data_pending,int16_t rssi,int8_t snr,uint8_t len,const uint8_t * rx_buf)88 static void clock_sync_package_callback(uint8_t port, bool data_pending, int16_t rssi, int8_t snr,
89 					uint8_t len, const uint8_t *rx_buf)
90 {
91 	uint8_t tx_buf[3 * MAX_CLOCK_SYNC_ANS_LEN];
92 	uint8_t tx_pos = 0;
93 	uint8_t rx_pos = 0;
94 
95 	__ASSERT(port == LORAWAN_PORT_CLOCK_SYNC, "Wrong port %d", port);
96 
97 	while (rx_pos < len) {
98 		uint8_t command_id = rx_buf[rx_pos++];
99 
100 		if (sizeof(tx_buf) - tx_pos < MAX_CLOCK_SYNC_ANS_LEN) {
101 			LOG_ERR("insufficient tx_buf size, some requests discarded");
102 			break;
103 		}
104 
105 		switch (command_id) {
106 		case CLOCK_SYNC_CMD_PKG_VERSION:
107 			tx_buf[tx_pos++] = CLOCK_SYNC_CMD_PKG_VERSION;
108 			tx_buf[tx_pos++] = LORAWAN_PACKAGE_ID_CLOCK_SYNC;
109 			tx_buf[tx_pos++] = CLOCK_SYNC_PACKAGE_VERSION;
110 			LOG_DBG("PackageVersionReq");
111 			break;
112 		case CLOCK_SYNC_CMD_APP_TIME: {
113 			/* answer from application server */
114 			int32_t time_correction;
115 
116 			ctx.nb_transmissions = 0;
117 
118 			time_correction = rx_buf[rx_pos++];
119 			time_correction	+= rx_buf[rx_pos++] << 8;
120 			time_correction	+= rx_buf[rx_pos++] << 16;
121 			time_correction	+= rx_buf[rx_pos++] << 24;
122 
123 			uint8_t token = rx_buf[rx_pos++] & 0x0F;
124 
125 			if (token == ctx.req_token) {
126 				ctx.time_offset += time_correction;
127 				ctx.req_token = (ctx.req_token + 1) % 16;
128 				ctx.synchronized = true;
129 
130 				LOG_DBG("AppTimeAns time_correction %d (token %d)",
131 					time_correction, token);
132 			} else {
133 				LOG_WRN("AppTimeAns with outdated token %d", token);
134 			}
135 			break;
136 		}
137 		case CLOCK_SYNC_CMD_DEVICE_APP_TIME_PERIODICITY: {
138 			uint8_t period = rx_buf[rx_pos++] & 0x0F;
139 
140 			ctx.periodicity = 1U << (period + 7);
141 
142 			tx_buf[tx_pos++] = CLOCK_SYNC_CMD_DEVICE_APP_TIME_PERIODICITY;
143 			tx_buf[tx_pos++] = 0x00; /* Status: OK */
144 
145 			tx_pos += clock_sync_serialize_device_time(tx_buf + tx_pos,
146 								   sizeof(tx_buf) - tx_pos);
147 
148 			LOG_DBG("DeviceAppTimePeriodicityReq period: %u", period);
149 			break;
150 		}
151 		case CLOCK_SYNC_CMD_FORCE_DEVICE_RESYNC: {
152 			uint8_t nb_transmissions = rx_buf[rx_pos++] & 0x07;
153 
154 			if (nb_transmissions != 0) {
155 				ctx.nb_transmissions = nb_transmissions;
156 				lorawan_services_reschedule_work(&ctx.resync_work, K_NO_WAIT);
157 			}
158 
159 			LOG_DBG("ForceDeviceResyncCmd nb_transmissions: %u", nb_transmissions);
160 			break;
161 		}
162 		default:
163 			return;
164 		}
165 	}
166 
167 	if (tx_pos > 0) {
168 		lorawan_services_schedule_uplink(LORAWAN_PORT_CLOCK_SYNC, tx_buf, tx_pos, 0);
169 	}
170 }
171 
clock_sync_app_time_req(void)172 static int clock_sync_app_time_req(void)
173 {
174 	uint8_t tx_pos = 0;
175 	uint8_t tx_buf[6];
176 
177 	tx_buf[tx_pos++] = CLOCK_SYNC_CMD_APP_TIME;
178 	tx_pos += clock_sync_serialize_device_time(tx_buf + tx_pos,
179 						       sizeof(tx_buf) - tx_pos);
180 
181 	/* Param: AnsRequired = 0 | TokenReq */
182 	tx_buf[tx_pos++] = ctx.req_token;
183 
184 	LOG_DBG("Sending clock sync AppTimeReq (token %d)", ctx.req_token);
185 
186 	lorawan_services_schedule_uplink(LORAWAN_PORT_CLOCK_SYNC, tx_buf, tx_pos, 0);
187 
188 	if (ctx.nb_transmissions > 0) {
189 		ctx.nb_transmissions--;
190 		lorawan_services_reschedule_work(&ctx.resync_work, K_SECONDS(CLOCK_RESYNC_DELAY));
191 	}
192 
193 	return 0;
194 }
195 
clock_sync_resync_handler(struct k_work * work)196 static void clock_sync_resync_handler(struct k_work *work)
197 {
198 	uint32_t periodicity;
199 
200 	clock_sync_app_time_req();
201 
202 	/* Add +-30s jitter to actual periodicity as required */
203 	periodicity = ctx.periodicity - 30 + sys_rand32_get() % 61;
204 
205 	lorawan_services_reschedule_work(&ctx.resync_work, K_SECONDS(periodicity));
206 }
207 
lorawan_clock_sync_get(uint32_t * gps_time)208 int lorawan_clock_sync_get(uint32_t *gps_time)
209 {
210 	__ASSERT(gps_time != NULL, "gps_time parameter is required");
211 
212 	if (ctx.synchronized) {
213 		*gps_time = (uint32_t)(k_uptime_get() / MSEC_PER_SEC + ctx.time_offset);
214 		return 0;
215 	} else {
216 		return -EAGAIN;
217 	}
218 }
219 
220 static struct lorawan_downlink_cb downlink_cb = {
221 	.port = (uint8_t)LORAWAN_PORT_CLOCK_SYNC,
222 	.cb = clock_sync_package_callback
223 };
224 
lorawan_clock_sync_run(void)225 int lorawan_clock_sync_run(void)
226 {
227 	ctx.periodicity = CONFIG_LORAWAN_APP_CLOCK_SYNC_PERIODICITY;
228 
229 	lorawan_register_downlink_callback(&downlink_cb);
230 
231 	k_work_init_delayable(&ctx.resync_work, clock_sync_resync_handler);
232 	lorawan_services_reschedule_work(&ctx.resync_work, K_NO_WAIT);
233 
234 	return 0;
235 }
236