/* * Copyright (c) 2022-2024 Martin Jäger * Copyright (c) 2022-2024 tado GmbH * * SPDX-License-Identifier: Apache-2.0 */ #include "lorawan_services.h" #include "../lw_priv.h" #include #include #include #include #include #include LOG_MODULE_REGISTER(lorawan_multicast, CONFIG_LORAWAN_SERVICES_LOG_LEVEL); /** * Version of LoRaWAN Remote Multicast Setup Specification * * This implementation only supports TS005-1.0.0. */ #define MULTICAST_PACKAGE_VERSION 1 /** * Maximum expected number of multicast commands per packet * * The standard states "A message MAY carry more than one command". Even though this was not * observed during testing, space for up to 3 packages is reserved. */ #define MAX_MULTICAST_CMDS_PER_PACKAGE 3 /** Maximum length of multicast answers */ #define MAX_MULTICAST_ANS_LEN 5 enum multicast_commands { MULTICAST_CMD_PKG_VERSION = 0x00, MULTICAST_CMD_MC_GROUP_STATUS = 0x01, MULTICAST_CMD_MC_GROUP_SETUP = 0x02, MULTICAST_CMD_MC_GROUP_DELETE = 0x03, MULTICAST_CMD_MC_CLASS_C_SESSION = 0x04, MULTICAST_CMD_MC_CLASS_B_SESSION = 0x05, }; struct multicast_context { struct k_work_delayable session_start_work; struct k_work_delayable session_stop_work; }; static struct multicast_context ctx[LORAMAC_MAX_MC_CTX]; static void multicast_session_start(struct k_work *work) { int ret; ret = lorawan_services_class_c_start(); if (ret < 0) { LOG_WRN("Failed to switch to class C: %d. Retrying in 1s.", ret); lorawan_services_reschedule_work(k_work_delayable_from_work(work), K_SECONDS(1)); } } static void multicast_session_stop(struct k_work *work) { int ret; ret = lorawan_services_class_c_stop(); if (ret < 0) { LOG_WRN("Failed to revert to class A: %d. Retrying in 1s.", ret); lorawan_services_reschedule_work(k_work_delayable_from_work(work), K_SECONDS(1)); } } /** * Schedule Class C session if valid timing is found * * @returns time to start (negative in case of missed start) */ static int32_t multicast_schedule_class_c_session(uint8_t id, uint32_t session_time, uint32_t session_timeout) { uint32_t current_time; int32_t time_to_start; int err; err = lorawan_clock_sync_get(¤t_time); time_to_start = session_time - current_time; if (err != 0 || time_to_start > 0xFFFFFF) { LOG_ERR("Clocks not synchronized, cannot schedule class C session"); /* truncate value to indicates that clocks are out of sync */ time_to_start = 0xFFFFFF; } else if (time_to_start >= 0) { LOG_DBG("Starting class C session in %d s", time_to_start); lorawan_services_reschedule_work(&ctx[id].session_start_work, K_SECONDS(time_to_start)); lorawan_services_reschedule_work(&ctx[id].session_stop_work, K_SECONDS(time_to_start + session_timeout)); } return time_to_start; } static void multicast_package_callback(uint8_t port, bool data_pending, int16_t rssi, int8_t snr, uint8_t len, const uint8_t *rx_buf) { uint8_t tx_buf[MAX_MULTICAST_CMDS_PER_PACKAGE * MAX_MULTICAST_ANS_LEN]; uint8_t tx_pos = 0; uint8_t rx_pos = 0; __ASSERT(port == LORAWAN_PORT_MULTICAST_SETUP, "Wrong port %d", port); while (rx_pos < len) { uint8_t command_id = rx_buf[rx_pos++]; if (sizeof(tx_buf) - tx_pos < MAX_MULTICAST_ANS_LEN) { LOG_ERR("insufficient tx_buf size, some requests discarded"); break; } switch (command_id) { case MULTICAST_CMD_PKG_VERSION: tx_buf[tx_pos++] = MULTICAST_CMD_PKG_VERSION; tx_buf[tx_pos++] = LORAWAN_PACKAGE_ID_REMOTE_MULTICAST_SETUP; tx_buf[tx_pos++] = MULTICAST_PACKAGE_VERSION; LOG_DBG("PackageVersionReq"); break; case MULTICAST_CMD_MC_GROUP_STATUS: LOG_ERR("McGroupStatusReq not implemented"); return; case MULTICAST_CMD_MC_GROUP_SETUP: { uint8_t id = rx_buf[rx_pos++] & 0x03; McChannelParams_t channel = { .IsRemotelySetup = true, .IsEnabled = true, .GroupID = (AddressIdentifier_t)id, .RxParams = {0}, }; channel.Address = sys_get_le32(rx_buf + rx_pos); rx_pos += sizeof(uint32_t); /* the key is copied in LoRaMacMcChannelSetup (cast to discard const) */ channel.McKeys.McKeyE = (uint8_t *)rx_buf + rx_pos; rx_pos += 16; channel.FCountMin = sys_get_le32(rx_buf + rx_pos); rx_pos += sizeof(uint32_t); channel.FCountMax = sys_get_le32(rx_buf + rx_pos); rx_pos += sizeof(uint32_t); LOG_DBG("McGroupSetupReq id: %u, addr: 0x%.8X, fcnt_min: %u, fcnt_max: %u", id, channel.Address, channel.FCountMin, channel.FCountMax); LoRaMacStatus_t ret = LoRaMacMcChannelSetup(&channel); tx_buf[tx_pos++] = MULTICAST_CMD_MC_GROUP_SETUP; if (ret == LORAMAC_STATUS_OK) { tx_buf[tx_pos++] = id; } else if (ret == LORAMAC_STATUS_MC_GROUP_UNDEFINED) { /* set IDerror flag */ tx_buf[tx_pos++] = (1U << 2) | id; } else { LOG_ERR("McGroupSetupReq failed: %s", lorawan_status2str(ret)); return; } break; } case MULTICAST_CMD_MC_GROUP_DELETE: { uint8_t id = rx_buf[rx_pos++] & 0x03; LoRaMacStatus_t ret = LoRaMacMcChannelDelete((AddressIdentifier_t)id); LOG_DBG("McGroupDeleteReq id: %d", id); tx_buf[tx_pos++] = MULTICAST_CMD_MC_GROUP_DELETE; if (ret == LORAMAC_STATUS_OK) { tx_buf[tx_pos++] = id; } else if (ret == LORAMAC_STATUS_MC_GROUP_UNDEFINED) { /* set McGroupUndefined flag */ tx_buf[tx_pos++] = (1U << 2) | id; } else { LOG_ERR("McGroupDeleteReq failed: %s", lorawan_status2str(ret)); return; } break; } case MULTICAST_CMD_MC_CLASS_C_SESSION: { uint32_t session_time; uint32_t session_timeout; uint8_t status = 0x00; uint8_t id = rx_buf[rx_pos++] & 0x03; McRxParams_t rx_params; session_time = sys_get_le32(rx_buf + rx_pos); rx_pos += sizeof(uint32_t); session_timeout = 1U << (rx_buf[rx_pos++] & 0x0F); rx_params.Class = CLASS_C; rx_params.Params.ClassC.Frequency = sys_get_le24(rx_buf + rx_pos) * 100; rx_pos += 3; rx_params.Params.ClassC.Datarate = rx_buf[rx_pos++]; LOG_DBG("McClassCSessionReq time: %u, timeout: %u, freq: %u, DR: %d", session_time, session_timeout, rx_params.Params.ClassC.Frequency, rx_params.Params.ClassC.Datarate); LoRaMacStatus_t ret = LoRaMacMcChannelSetupRxParams((AddressIdentifier_t)id, &rx_params, &status); tx_buf[tx_pos++] = MULTICAST_CMD_MC_CLASS_C_SESSION; if (ret == LORAMAC_STATUS_OK) { int32_t time_to_start; time_to_start = multicast_schedule_class_c_session(id, session_time, session_timeout); if (time_to_start >= 0) { tx_buf[tx_pos++] = status; sys_put_le24(time_to_start, tx_buf + tx_pos); tx_pos += 3; } else { LOG_ERR("Missed class C session start at %d in %d s", session_time, time_to_start); /* set StartMissed flag */ tx_buf[tx_pos++] = (1U << 5) | status; } } else { LOG_ERR("McClassCSessionReq failed: %s", lorawan_status2str(ret)); if (ret == LORAMAC_STATUS_MC_GROUP_UNDEFINED) { /* set McGroupUndefined flag */ tx_buf[tx_pos++] = (1U << 4) | status; } else if (ret == LORAMAC_STATUS_FREQ_AND_DR_INVALID) { /* set FreqError and DR Error flags */ tx_buf[tx_pos++] = (3U << 2) | status; return; } } break; } case MULTICAST_CMD_MC_CLASS_B_SESSION: LOG_ERR("McClassBSessionReq not implemented"); return; default: return; } } if (tx_pos > 0) { /* Random delay 2+-1 seconds according to RP002-1.0.3, chapter 2.3 */ uint32_t delay = 1 + sys_rand32_get() % 3; lorawan_services_schedule_uplink(LORAWAN_PORT_MULTICAST_SETUP, tx_buf, tx_pos, delay); } } static struct lorawan_downlink_cb downlink_cb = { .port = (uint8_t)LORAWAN_PORT_MULTICAST_SETUP, .cb = multicast_package_callback, }; static int multicast_init(void) { for (int i = 0; i < ARRAY_SIZE(ctx); i++) { k_work_init_delayable(&ctx[i].session_start_work, multicast_session_start); k_work_init_delayable(&ctx[i].session_stop_work, multicast_session_stop); } lorawan_register_downlink_callback(&downlink_cb); return 0; } /* initialization must be after lorawan_init in lorawan.c */ SYS_INIT(multicast_init, POST_KERNEL, 1);