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(¤t_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