1 /*
2  * Copyright (c) 2022-2024 Martin Jäger <martin@libre.solar>
3  * Copyright (c) 2022-2024 tado GmbH
4  *
5  * SPDX-License-Identifier: Apache-2.0
6  */
7 
8 #include "lorawan_services.h"
9 #include "../lw_priv.h"
10 
11 #include <LoRaMac.h>
12 #include <zephyr/lorawan/lorawan.h>
13 #include <zephyr/logging/log.h>
14 #include <zephyr/random/random.h>
15 #include <zephyr/sys/byteorder.h>
16 
17 #include <stdio.h>
18 
19 LOG_MODULE_REGISTER(lorawan_multicast, CONFIG_LORAWAN_SERVICES_LOG_LEVEL);
20 
21 /**
22  * Version of LoRaWAN Remote Multicast Setup Specification
23  *
24  * This implementation only supports TS005-1.0.0.
25  */
26 #define MULTICAST_PACKAGE_VERSION 1
27 
28 /**
29  * Maximum expected number of multicast commands per packet
30  *
31  * The standard states "A message MAY carry more than one command". Even though this was not
32  * observed during testing, space for up to 3 packages is reserved.
33  */
34 #define MAX_MULTICAST_CMDS_PER_PACKAGE 3
35 
36 /** Maximum length of multicast answers */
37 #define MAX_MULTICAST_ANS_LEN 5
38 
39 enum multicast_commands {
40 	MULTICAST_CMD_PKG_VERSION = 0x00,
41 	MULTICAST_CMD_MC_GROUP_STATUS = 0x01,
42 	MULTICAST_CMD_MC_GROUP_SETUP = 0x02,
43 	MULTICAST_CMD_MC_GROUP_DELETE = 0x03,
44 	MULTICAST_CMD_MC_CLASS_C_SESSION = 0x04,
45 	MULTICAST_CMD_MC_CLASS_B_SESSION = 0x05,
46 };
47 
48 struct multicast_context {
49 	struct k_work_delayable session_start_work;
50 	struct k_work_delayable session_stop_work;
51 };
52 
53 static struct multicast_context ctx[LORAMAC_MAX_MC_CTX];
54 
multicast_session_start(struct k_work * work)55 static void multicast_session_start(struct k_work *work)
56 {
57 	int ret;
58 
59 	ret = lorawan_services_class_c_start();
60 	if (ret < 0) {
61 		LOG_WRN("Failed to switch to class C: %d. Retrying in 1s.", ret);
62 		lorawan_services_reschedule_work(k_work_delayable_from_work(work), K_SECONDS(1));
63 	}
64 }
65 
multicast_session_stop(struct k_work * work)66 static void multicast_session_stop(struct k_work *work)
67 {
68 	int ret;
69 
70 	ret = lorawan_services_class_c_stop();
71 	if (ret < 0) {
72 		LOG_WRN("Failed to revert to class A: %d. Retrying in 1s.", ret);
73 		lorawan_services_reschedule_work(k_work_delayable_from_work(work), K_SECONDS(1));
74 	}
75 }
76 
77 /**
78  * Schedule Class C session if valid timing is found
79  *
80  * @returns time to start (negative in case of missed start)
81  */
multicast_schedule_class_c_session(uint8_t id,uint32_t session_time,uint32_t session_timeout)82 static int32_t multicast_schedule_class_c_session(uint8_t id, uint32_t session_time,
83 						  uint32_t session_timeout)
84 {
85 	uint32_t current_time;
86 	int32_t time_to_start;
87 	int err;
88 
89 	err = lorawan_clock_sync_get(&current_time);
90 	time_to_start = session_time - current_time;
91 
92 	if (err != 0 || time_to_start > 0xFFFFFF) {
93 		LOG_ERR("Clocks not synchronized, cannot schedule class C session");
94 
95 		/* truncate value to indicates that clocks are out of sync */
96 		time_to_start = 0xFFFFFF;
97 	} else if (time_to_start >= 0) {
98 		LOG_DBG("Starting class C session in %d s", time_to_start);
99 
100 		lorawan_services_reschedule_work(&ctx[id].session_start_work,
101 						 K_SECONDS(time_to_start));
102 
103 		lorawan_services_reschedule_work(&ctx[id].session_stop_work,
104 						 K_SECONDS(time_to_start + session_timeout));
105 	}
106 
107 	return time_to_start;
108 }
109 
multicast_package_callback(uint8_t port,uint8_t flags,int16_t rssi,int8_t snr,uint8_t len,const uint8_t * rx_buf)110 static void multicast_package_callback(uint8_t port, uint8_t flags, int16_t rssi, int8_t snr,
111 				       uint8_t len, const uint8_t *rx_buf)
112 {
113 	uint8_t tx_buf[MAX_MULTICAST_CMDS_PER_PACKAGE * MAX_MULTICAST_ANS_LEN];
114 	uint8_t tx_pos = 0;
115 	uint8_t rx_pos = 0;
116 
117 	__ASSERT(port == LORAWAN_PORT_MULTICAST_SETUP, "Wrong port %d", port);
118 
119 	while (rx_pos < len) {
120 		uint8_t command_id = rx_buf[rx_pos++];
121 
122 		if (sizeof(tx_buf) - tx_pos < MAX_MULTICAST_ANS_LEN) {
123 			LOG_ERR("insufficient tx_buf size, some requests discarded");
124 			break;
125 		}
126 
127 		switch (command_id) {
128 		case MULTICAST_CMD_PKG_VERSION:
129 			tx_buf[tx_pos++] = MULTICAST_CMD_PKG_VERSION;
130 			tx_buf[tx_pos++] = LORAWAN_PACKAGE_ID_REMOTE_MULTICAST_SETUP;
131 			tx_buf[tx_pos++] = MULTICAST_PACKAGE_VERSION;
132 			LOG_DBG("PackageVersionReq");
133 			break;
134 		case MULTICAST_CMD_MC_GROUP_STATUS:
135 			LOG_ERR("McGroupStatusReq not implemented");
136 			return;
137 		case MULTICAST_CMD_MC_GROUP_SETUP: {
138 			uint8_t id = rx_buf[rx_pos++] & 0x03;
139 			McChannelParams_t channel = {
140 				.IsRemotelySetup = true,
141 				.IsEnabled = true,
142 				.GroupID = (AddressIdentifier_t)id,
143 				.RxParams = {0},
144 			};
145 
146 			channel.Address = sys_get_le32(rx_buf + rx_pos);
147 			rx_pos += sizeof(uint32_t);
148 
149 			/* the key is copied in LoRaMacMcChannelSetup (cast to discard const) */
150 			channel.McKeys.McKeyE = (uint8_t *)rx_buf + rx_pos;
151 			rx_pos += 16;
152 
153 			channel.FCountMin = sys_get_le32(rx_buf + rx_pos);
154 			rx_pos += sizeof(uint32_t);
155 
156 			channel.FCountMax = sys_get_le32(rx_buf + rx_pos);
157 			rx_pos += sizeof(uint32_t);
158 
159 			LOG_DBG("McGroupSetupReq id: %u, addr: 0x%.8X, fcnt_min: %u, fcnt_max: %u",
160 				id, channel.Address, channel.FCountMin, channel.FCountMax);
161 
162 			LoRaMacStatus_t ret = LoRaMacMcChannelSetup(&channel);
163 
164 			tx_buf[tx_pos++] = MULTICAST_CMD_MC_GROUP_SETUP;
165 			if (ret == LORAMAC_STATUS_OK) {
166 				tx_buf[tx_pos++] = id;
167 			} else if (ret == LORAMAC_STATUS_MC_GROUP_UNDEFINED) {
168 				/* set IDerror flag */
169 				tx_buf[tx_pos++] = (1U << 2) | id;
170 			} else {
171 				LOG_ERR("McGroupSetupReq failed: %s", lorawan_status2str(ret));
172 				return;
173 			}
174 			break;
175 		}
176 		case MULTICAST_CMD_MC_GROUP_DELETE: {
177 			uint8_t id = rx_buf[rx_pos++] & 0x03;
178 
179 			LoRaMacStatus_t ret = LoRaMacMcChannelDelete((AddressIdentifier_t)id);
180 
181 			LOG_DBG("McGroupDeleteReq id: %d", id);
182 
183 			tx_buf[tx_pos++] = MULTICAST_CMD_MC_GROUP_DELETE;
184 			if (ret == LORAMAC_STATUS_OK) {
185 				tx_buf[tx_pos++] = id;
186 			} else if (ret == LORAMAC_STATUS_MC_GROUP_UNDEFINED) {
187 				/* set McGroupUndefined flag */
188 				tx_buf[tx_pos++] = (1U << 2) | id;
189 			} else {
190 				LOG_ERR("McGroupDeleteReq failed: %s", lorawan_status2str(ret));
191 				return;
192 			}
193 			break;
194 		}
195 		case MULTICAST_CMD_MC_CLASS_C_SESSION: {
196 			uint32_t session_time;
197 			uint32_t session_timeout;
198 			uint8_t status = 0x00;
199 			uint8_t id = rx_buf[rx_pos++] & 0x03;
200 			McRxParams_t rx_params;
201 
202 			session_time = sys_get_le32(rx_buf + rx_pos);
203 			rx_pos += sizeof(uint32_t);
204 
205 			session_timeout = 1U << (rx_buf[rx_pos++] & 0x0F);
206 
207 			rx_params.Class = CLASS_C;
208 
209 			rx_params.Params.ClassC.Frequency = sys_get_le24(rx_buf + rx_pos) * 100;
210 			rx_pos += 3;
211 
212 			rx_params.Params.ClassC.Datarate = rx_buf[rx_pos++];
213 
214 			LOG_DBG("McClassCSessionReq time: %u, timeout: %u, freq: %u, DR: %d",
215 				session_time, session_timeout, rx_params.Params.ClassC.Frequency,
216 				rx_params.Params.ClassC.Datarate);
217 
218 			LoRaMacStatus_t ret = LoRaMacMcChannelSetupRxParams((AddressIdentifier_t)id,
219 									    &rx_params, &status);
220 
221 			tx_buf[tx_pos++] = MULTICAST_CMD_MC_CLASS_C_SESSION;
222 			if (ret == LORAMAC_STATUS_OK) {
223 				int32_t time_to_start;
224 
225 				time_to_start = multicast_schedule_class_c_session(id, session_time,
226 										   session_timeout);
227 				if (time_to_start >= 0) {
228 					tx_buf[tx_pos++] = status;
229 					sys_put_le24(time_to_start, tx_buf + tx_pos);
230 					tx_pos += 3;
231 				} else {
232 					LOG_ERR("Missed class C session start at %d in %d s",
233 						session_time, time_to_start);
234 					/* set StartMissed flag */
235 					tx_buf[tx_pos++] = (1U << 5) | status;
236 				}
237 			} else {
238 				LOG_ERR("McClassCSessionReq failed: %s", lorawan_status2str(ret));
239 				if (ret == LORAMAC_STATUS_MC_GROUP_UNDEFINED) {
240 					/* set McGroupUndefined flag */
241 					tx_buf[tx_pos++] = (1U << 4) | status;
242 				} else if (ret == LORAMAC_STATUS_FREQ_AND_DR_INVALID) {
243 					/* set FreqError and DR Error flags */
244 					tx_buf[tx_pos++] = (3U << 2) | status;
245 					return;
246 				}
247 			}
248 			break;
249 		}
250 		case MULTICAST_CMD_MC_CLASS_B_SESSION:
251 			LOG_ERR("McClassBSessionReq not implemented");
252 			return;
253 		default:
254 			return;
255 		}
256 	}
257 
258 	if (tx_pos > 0) {
259 		/* Random delay 2+-1 seconds according to RP002-1.0.3, chapter 2.3 */
260 		uint32_t delay = 1 + sys_rand32_get() % 3;
261 
262 		lorawan_services_schedule_uplink(LORAWAN_PORT_MULTICAST_SETUP, tx_buf, tx_pos,
263 						 delay);
264 	}
265 }
266 
267 static struct lorawan_downlink_cb downlink_cb = {
268 	.port = (uint8_t)LORAWAN_PORT_MULTICAST_SETUP,
269 	.cb = multicast_package_callback,
270 };
271 
multicast_init(void)272 static int multicast_init(void)
273 {
274 	for (int i = 0; i < ARRAY_SIZE(ctx); i++) {
275 		k_work_init_delayable(&ctx[i].session_start_work, multicast_session_start);
276 		k_work_init_delayable(&ctx[i].session_stop_work, multicast_session_stop);
277 	}
278 
279 	lorawan_register_downlink_callback(&downlink_cb);
280 
281 	return 0;
282 }
283 
284 /* initialization must be after lorawan_init in lorawan.c */
285 SYS_INIT(multicast_init, POST_KERNEL, 1);
286