| /* |
| * Copyright (c) 2022-2024 Martin Jäger <martin@libre.solar> |
| * Copyright (c) 2022-2024 tado GmbH |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #include "lorawan_services.h" |
| #include "../lw_priv.h" |
| |
| #include <LoRaMac.h> |
| #include <zephyr/lorawan/lorawan.h> |
| #include <zephyr/logging/log.h> |
| #include <zephyr/random/random.h> |
| #include <zephyr/sys/byteorder.h> |
| |
| #include <stdio.h> |
| |
| 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); |