/** / _____) _ | | ( (____ _____ ____ _| |_ _____ ____| |__ \____ \| ___ | (_ _) ___ |/ ___) _ \ _____) ) ____| | | || |_| ____( (___| | | | (______/|_____)_|_|_| \__)_____)\____)_| |_| (C)2013 Semtech ___ _____ _ ___ _ _____ ___ ___ ___ ___ / __|_ _/_\ / __| |/ / __/ _ \| _ \/ __| __| \__ \ | |/ _ \ (__| ' <| _| (_) | / (__| _| |___/ |_/_/ \_\___|_|\_\_| \___/|_|_\\___|___| embedded.connectivity.solutions=============== Description: LoRaWAN stack layer that controls both MAC and PHY underneath License: Revised BSD License, see LICENSE.TXT file include in the project Maintainer: Miguel Luis ( Semtech ), Gregory Cristian ( Semtech ) and Daniel Jaeckle ( STACKFORCE ) Copyright (c) 2019, Arm Limited and affiliates. SPDX-License-Identifier: BSD-3-Clause */ #include "LoRaMacClassB.h" #include "mbed_assert.h" #include "stdio.h" #include "LoRaMacCrypto.h" #include "mbed.h" #include "mbed-trace/mbed_trace.h" #define TRACE_GROUP "LMAC" /*! * Limit ping slot mbed tracing to once per this period in millis */ #define CLASSB_PING_OPEN_DBG_TRACE_PERIOD 5000 /*! * Beacon reserved time in ms */ #define CLASSB_BEACON_RESERVED 2120 /*! * Ping slot length time in ms */ #define CLASSB_PING_SLOT_WINDOW 30 /*! * Invalid ping slot */ #define LORAMAC_INVALID_PING_SLOT -1 /*! * LoRaWAN Beacon Interval in milliseconds */ #define LORA_BEACON_INTERVAL_MILLIS (LORA_BEACON_INTERVAL*1000UL) LoRaMacClassB::LoRaMacClassB() : _lora_phy(NULL), _lora_time(NULL), _lora_crypto(NULL), _protocol_params(NULL), _open_rx_window(NULL), _close_rx_window(NULL), _beacon_event_cb(NULL) { memset(&_beacon, 0, sizeof(_beacon)); memset(&_ping, 0, sizeof(_ping)); memset(&_opstatus, 0, sizeof(_opstatus)); } lorawan_status_t LoRaMacClassB::initialize(LoRaWANTimeHandler *lora_time, LoRaPHY *lora_phy, LoRaMacCrypto *lora_crypto, loramac_protocol_params *params, mbed::Callback open_window, mbed::Callback close_window) { // Initialization should not be called more than once MBED_ASSERT(!_opstatus.initialized); if (!_opstatus.initialized) { MBED_ASSERT(lora_time); MBED_ASSERT(lora_crypto); MBED_ASSERT(params); MBED_ASSERT(lora_phy); MBED_ASSERT(open_window); MBED_ASSERT(close_window); _lora_time = lora_time; _lora_phy = lora_phy; _lora_crypto = lora_crypto; _protocol_params = params; _lora_time->init(_beacon_timer, mbed::callback(this, &LoRaMacClassB::open_beacon_window)); _lora_time->init(_ping_slot_timer, mbed::callback(this, &LoRaMacClassB::open_ping_slot)); _lora_time->init(_beacon_acq_timer, mbed::callback(this, &LoRaMacClassB::beacon_acquisition_timeout)); _lora_phy->get_beacon_rfu_size(_beacon.rfu1_size, _beacon.rfu2_size); _open_rx_window = open_window; _close_rx_window = close_window; _beacon.time_on_air = _lora_phy->compute_beacon_time_on_air(); _ping.slot[0].rx_config.rx_slot = RX_SLOT_WIN_UNICAST_PING_SLOT; for (uint8_t i = 1; i <= MBED_CONF_LORA_CLASS_B_MULTICAST_ADDRESS_MAX_COUNT; i++) { _ping.slot[i].rx_config.rx_slot = RX_SLOT_WIN_MULTICAST_PING_SLOT; } _opstatus.initialized = 1; } return LORAWAN_STATUS_OK; } lorawan_status_t LoRaMacClassB::enable(void) { if (!_opstatus.initialized) { return LORAWAN_STATUS_NOT_INITIALIZED; } else if (!_opstatus.beacon_found) { return LORAWAN_STATUS_NO_BEACON_FOUND; } else if (!_opstatus.ping_on) { // Initialize unicast ping slot address _ping.slot[0].address = protocol_params()->dev_addr; _opstatus.ping_on = 1; start_ping_slots(_beacon.beacon_time); } return LORAWAN_STATUS_OK; } lorawan_status_t LoRaMacClassB::disable(void) { _opstatus.beacon_on = 0; _opstatus.ping_on = 0; _lora_time->stop(_beacon_timer); _lora_time->stop(_ping_slot_timer); _lora_time->stop(_beacon_acq_timer); return LORAWAN_STATUS_OK; } lorawan_status_t LoRaMacClassB::enable_beacon_acquisition(mbed::Callback beacon_event_cb) { lorawan_status_t status = LORAWAN_STATUS_OK; if (!_opstatus.initialized) { status = LORAWAN_STATUS_NOT_INITIALIZED; } else if (!_opstatus.beacon_on) { _beacon_event_cb = beacon_event_cb; _beacon.beacon_time = 0; _opstatus.beacon_found = 0; _opstatus.beacon_on = 1; reset_window_expansion(); if (schedule_beacon_window()) { uint32_t acquisition_timeout = MBED_CONF_LORA_BEACON_ACQUISITION_NB_TRIALS * LORA_BEACON_INTERVAL_MILLIS; _lora_time->start(_beacon_acq_timer, acquisition_timeout); tr_debug("Enabled Beacon Acquisition"); } else { _opstatus.beacon_on = 0; status = LORAWAN_STATUS_NO_OP; // Cannot perform request operation } } return status; } void LoRaMacClassB::beacon_acquisition_timeout(void) { // Disable beacon acquisition _opstatus.beacon_on = 0; _lora_time->stop(_beacon_timer); _close_rx_window(RX_SLOT_WIN_BEACON); send_beacon_miss_indication(); } bool LoRaMacClassB::set_beacon_rx_config(uint32_t beacon_time, rx_config_params_t &rx_config) { bool status; rx_config.rx_slot = RX_SLOT_WIN_BEACON; rx_config.is_rx_continuous = beacon_time == 0; status = _lora_phy->compute_beacon_win_params(beacon_time, MBED_CONF_LORA_BEACON_PREAMBLE_LENGTH, MBED_CONF_LORA_MAX_SYS_RX_ERROR, &rx_config); if (status) { rx_config.window_timeout = MAX(_beacon.expansion.timeout, rx_config.window_timeout); } else { tr_error("Compute Beacon PHY parameters failed"); } return status; } bool LoRaMacClassB::schedule_beacon_window(void) { bool status = false; // Abort if beacon slot is disabled if (!_opstatus.beacon_on) { return false; } lorawan_gps_time_t current_time = get_gps_time(); // Receiver put in continuous mode when device time is not set if (current_time == 0) { status = set_beacon_rx_config(0, _beacon.rx_config); if (status) { open_beacon_window(); } } else { lorawan_gps_time_t next_beacon_time = ((current_time / LORA_BEACON_INTERVAL_MILLIS) + 1) * LORA_BEACON_INTERVAL_MILLIS; // Delay until next beacon lorawan_gps_time_t beacon_delay = next_beacon_time - current_time; // Compute beacon window receiver configuration status = set_beacon_rx_config(next_beacon_time / 1000, _beacon.rx_config); if (status) { // PHY layer computes window offset adjusting for minimum preamble, // timing errors, receiver wakeup time int32_t window_offset = _beacon.rx_config.window_offset; // Expand PHY offset by class b window expansion window_offset -= _beacon.expansion.movement; int32_t delay = beacon_delay + window_offset; // Open window now if delay elapsed if (delay < 0) { open_beacon_window(); } else { _lora_time->start(_beacon_timer, delay); } tr_debug("Next beacon time = %llu in %ld ms", next_beacon_time / 1000, delay); } } return status; } void LoRaMacClassB::open_beacon_window(void) { Lock(*this); if (!_opstatus.beacon_on) { return; } else if (_opstatus.paused) { if (_opstatus.beacon_found) { _beacon.beacon_time += LORA_BEACON_INTERVAL; send_beacon_miss_indication(); } // Naively schedule next beacon window. Accurate scheduling will occur // on resume _lora_time->start(_beacon_timer, LORA_BEACON_INTERVAL_MILLIS); } else { // Set beacon rx regardless of open window status. Only expected reason for failure is // conflict with class A reception slots. In this case, handle rx(timeout) callback checks for failure _opstatus.beacon_rx = 1; if (_open_rx_window(&_beacon.rx_config)) { tr_debug("Beacon slot open, Freq = %lu", _beacon.rx_config.frequency); } } } void LoRaMacClassB::handle_rx(rx_slot_t rx_slot, const uint8_t *const payload, uint16_t size, uint32_t rx_timestamp) { switch (rx_slot) { case RX_SLOT_WIN_BEACON: handle_beacon_rx(payload, size, rx_timestamp); break; case RX_SLOT_WIN_UNICAST_PING_SLOT: case RX_SLOT_WIN_MULTICAST_PING_SLOT: _opstatus.ping_rx = 0; schedule_ping_slot(); break; default: // beacon rx state is set if beacon slot was not opened or aborted by class A operation if (_opstatus.beacon_rx) { _opstatus.beacon_rx = 0; handle_beacon_rx_timeout(); } break; } } void LoRaMacClassB::handle_rx_timeout(rx_slot_t rx_slot) { switch (rx_slot) { case RX_SLOT_WIN_BEACON: expand_window(); handle_beacon_rx_timeout(); break; case RX_SLOT_WIN_UNICAST_PING_SLOT: case RX_SLOT_WIN_MULTICAST_PING_SLOT: _opstatus.ping_rx = 0; schedule_ping_slot(); break; default: // beacon rx state is set if beacon slot was not opened or aborted by class A operation if (_opstatus.beacon_rx) { _opstatus.beacon_rx = 0; handle_beacon_rx_timeout(); } break; } } void LoRaMacClassB::handle_beacon_rx(const uint8_t *const frame, uint16_t size, uint32_t rx_timestamp) { lorawan_time_t beacon_time; loramac_beacon_status_t beacon_status; lorawan_gps_time_t gps_time; _opstatus.beacon_rx = 0; beacon_time = process_beacon_frame(frame, size); // Beacon is ok if beacon time read if (beacon_time) { _beacon.beacon_time = beacon_time; // Update gps time gps_time = (lorawan_gps_time_t)beacon_time * 1000 + _beacon.time_on_air + (_lora_time->get_current_time() - rx_timestamp); _lora_time->set_gps_time(gps_time); tr_debug("Synchronized GPS Time = %llu from Received Beacon", gps_time); // Reset window expansion to defaults reset_window_expansion(); // Stop acquistion timeout if this the first found beacon if (!_opstatus.beacon_found) { _opstatus.beacon_found = 1; _lora_time->stop(_beacon_acq_timer); beacon_status = BEACON_STATUS_ACQUISITION_SUCCESS; } else { beacon_status = BEACON_STATUS_LOCK; } // Send event to the stack _beacon_event_cb(beacon_status, &_beacon.received_beacon); // Schedule next beacon window reception schedule_beacon_window(); // Schedule beacon window ping slots start_ping_slots(beacon_time); } else { handle_beacon_rx_timeout(); } } void LoRaMacClassB::send_beacon_miss_indication(void) { loramac_beacon_status_t status = _opstatus.beacon_found ? BEACON_STATUS_MISS : BEACON_STATUS_ACQUISITION_FAILED; _beacon_event_cb(status, NULL); } void LoRaMacClassB::handle_beacon_rx_timeout(void) { _opstatus.beacon_rx = 0; // Missed beacon, adjust time if beacon previously found if (_opstatus.beacon_found) { _beacon.beacon_time += LORA_BEACON_INTERVAL; send_beacon_miss_indication(); } schedule_beacon_window(); start_ping_slots(_beacon.beacon_time); } uint32_t LoRaMacClassB::process_beacon_frame(const uint8_t *const frame, uint16_t size) { MbedCRC ct(0, 0, false, false); uint16_t beacon_crc1; uint16_t beacon_crc2; uint32_t crc1; uint32_t crc2; uint32_t beacon_time = 0; // A beacon frame is defined as: // Bytes: | x | 4 | 2 | 7 | y | 2 | // |------|------|------|------------|------|------| // Field: | RFU1 | Time | CRC1 | GwSpecific | RFU2 | CRC2 | // // Field RFU1 and RFU2 have variable sizes. It depends on the region specific implementation if (size == _beacon.frame_size()) { beacon_crc1 = frame[_beacon.rfu1_size + 4] & 0xFF; beacon_crc1 |= ((uint16_t)frame[_beacon.rfu1_size + 5] << 8) & 0xFF00; // Validate the first crc of the beacon frame ct.compute((void *)frame, _beacon.rfu1_size + 4, &crc1); if (crc1 == beacon_crc1) { // Read Time from the frame uint8_t pos = _beacon.rfu1_size; beacon_time = ((uint32_t)frame[pos]) & 0x000000FF; beacon_time |= ((uint32_t)(frame[pos + 1] << 8)) & 0x0000FF00; beacon_time |= ((uint32_t)(frame[pos + 2] << 16)) & 0x00FF0000; beacon_time |= ((uint32_t)(frame[pos + 3] << 24)) & 0xFF000000; _beacon.received_beacon.time = beacon_time; // Validate the second crc of the beacon frame beacon_crc2 = frame[_beacon.rfu1_size + 4 + 2 + 7 + _beacon.rfu2_size] & 0xFF; beacon_crc2 |= ((uint16_t)frame[_beacon.rfu1_size + 4 + 2 + 7 + _beacon.rfu2_size + 1] << 8) & 0xFF00; ct.compute((void *)(frame + _beacon.rfu1_size + 4 + 2), 7 + _beacon.rfu2_size, &crc2); if (crc2 == beacon_crc2) { // Read frame GwSpecific field memcpy(_beacon.received_beacon.gw_specific, frame + _beacon.rfu1_size + 4 + 2, LORAMAC_BEACON_GW_SPECIFIC_LEN); } else { tr_debug("Beacon frame Gateway Specific part integrity check failed"); memset(_beacon.received_beacon.gw_specific, 0, LORAMAC_BEACON_GW_SPECIFIC_LEN); } } else { tr_debug("Beacon frame Network Common part integrity check failed"); } } return beacon_time; } void LoRaMacClassB::reset_window_expansion(void) { // Beacon slot expansion defaults _beacon.expansion.timeout = MBED_CONF_LORA_CLASS_B_EXPANSION_TIMEOUT_DEFAULT; _beacon.expansion.movement = MBED_CONF_LORA_CLASS_B_EXPANSION_OFFSET_DEFAULT; // Ping slot expansion defaults _ping.expansion.timeout = MBED_CONF_LORA_CLASS_B_EXPANSION_TIMEOUT_DEFAULT; _ping.expansion.movement = MBED_CONF_LORA_CLASS_B_EXPANSION_OFFSET_DEFAULT; } void LoRaMacClassB::expand_window(void) { // beacon symbol timeout _beacon.expansion.timeout *= MBED_CONF_LORA_CLASS_B_EXPANSION_TIMEOUT_FACTOR; if (_beacon.expansion.timeout > MBED_CONF_LORA_CLASS_B_EXPANSION_BEACON_TIMEOUT_MAX) { _beacon.expansion.timeout = MBED_CONF_LORA_CLASS_B_EXPANSION_BEACON_TIMEOUT_MAX; } // beacon slot movement _beacon.expansion.movement *= MBED_CONF_LORA_CLASS_B_EXPANSION_OFFSET_FACTOR; if (_beacon.expansion.movement > MBED_CONF_LORA_CLASS_B_EXPANSION_OFFSET_MAX) { _beacon.expansion.movement = MBED_CONF_LORA_CLASS_B_EXPANSION_OFFSET_MAX; } tr_debug("Expand beacon window timeout=%u, movement=%u", _beacon.expansion.timeout, _beacon.expansion.movement); // ping slot symbol timeout _ping.expansion.timeout *= MBED_CONF_LORA_CLASS_B_EXPANSION_TIMEOUT_FACTOR; if (_ping.expansion.timeout > MBED_CONF_LORA_CLASS_B_EXPANSION_PING_TIMEOUT_MAX) { _ping.expansion.timeout = MBED_CONF_LORA_CLASS_B_EXPANSION_PING_TIMEOUT_MAX; } // ping slot movement _ping.expansion.movement *= MBED_CONF_LORA_CLASS_B_EXPANSION_OFFSET_FACTOR; if (_ping.expansion.movement > MBED_CONF_LORA_CLASS_B_EXPANSION_OFFSET_MAX) { _ping.expansion.movement = MBED_CONF_LORA_CLASS_B_EXPANSION_OFFSET_MAX; } tr_debug("Expand ping slot timeout=%u, movement=%u", _ping.expansion.timeout, _ping.expansion.movement); } bool LoRaMacClassB::compute_ping_offset(uint32_t beacon_time, uint32_t address, uint16_t ping_period, uint16_t &ping_offset) { int crypto_status; uint16_t rand_offset; crypto_status = _lora_crypto->compute_ping_slot_random_offset(beacon_time, address, &rand_offset); if (crypto_status == 0) { ping_offset = rand_offset % ping_period; return true; } else { tr_error("Compute Ping slot random offset crypto error (%d)", crypto_status); } return false; } lorawan_gps_time_t LoRaMacClassB::compute_ping_slot(uint32_t beacon_time, lorawan_gps_time_t current_time, uint16_t ping_period, uint8_t ping_nb, uint32_t address, uint16_t ping_slot_offset, uint16_t &next_slot_nb, rx_config_params_t &rx_config) { uint64_t beacon_time_millis; uint8_t slot = 0; uint16_t slot_nb = ping_slot_offset; lorawan_gps_time_t slot_time = 0; bool status; // Convert beacon time to millis beacon_time_millis = (uint64_t)beacon_time * 1000; // Compute the first ping slot = beacon reserved + (ping_offset * slot window length) slot_time = beacon_time_millis + CLASSB_BEACON_RESERVED + (slot_nb * CLASSB_PING_SLOT_WINDOW); // first slot is in the past, compute next slot time if (slot_time <= current_time) { // Get next slot number slot = ((current_time - slot_time) / (ping_period * CLASSB_PING_SLOT_WINDOW)) + 1; // Compute next slot time slot_nb = ping_slot_offset + (slot * ping_period); slot_time = (beacon_time_millis + CLASSB_BEACON_RESERVED) + (slot_nb * CLASSB_PING_SLOT_WINDOW); } // Check computed slot is less than configured number of slots per beacon period if (slot < ping_nb) { status = _lora_phy->compute_ping_win_params(beacon_time, address, MBED_CONF_LORA_DOWNLINK_PREAMBLE_LENGTH, MBED_CONF_LORA_MAX_SYS_RX_ERROR, &rx_config); if (status) { // For window timeout select the larger of the phy computed and class-b window expanded rx_config.window_timeout = MAX(_ping.expansion.timeout, rx_config.window_timeout); // PHY layer computes window offset adjusting for minimum preamble, // timing errors, receiver wakeup time int32_t window_offset = rx_config.window_offset - _ping.expansion.movement; slot_time += window_offset; next_slot_nb = slot_nb; return slot_time; } else { tr_error("Compute Ping slot PHY parameters failed"); } } return 0; } void LoRaMacClassB::start_ping_slots(uint32_t beacon_time) { // Do nothing if ping slots are not enabled if (!_opstatus.ping_on) { return; } loramac_protocol_params *params = protocol_params(); // Compute unicast & multicast ping slot offsets for (uint8_t i = 0; i <= MBED_CONF_LORA_CLASS_B_MULTICAST_ADDRESS_MAX_COUNT; i++) { if (_ping.slot[i].address) { compute_ping_offset(beacon_time, _ping.slot[i].address, params->sys_params.ping_slot.ping_period, _ping.slot[i].offset); } } // Schedule next ping slot schedule_ping_slot(); } void LoRaMacClassB::schedule_ping_slot(void) { lorawan_gps_time_t current_time = get_gps_time(); lorawan_gps_time_t best_slot_time = 0; lorawan_gps_time_t slot_time; int32_t delay; loramac_protocol_params *params = protocol_params(); const uint16_t ping_period = params->sys_params.ping_slot.ping_period; const uint16_t ping_nb = params->sys_params.ping_slot.ping_nb; _lora_time->stop(_ping_slot_timer); _ping.slot_idx = LORAMAC_INVALID_PING_SLOT; // Compute next ping slot times for (uint8_t i = 0; i <= MBED_CONF_LORA_CLASS_B_MULTICAST_ADDRESS_MAX_COUNT; i++) { if (!_ping.slot[i].address) { continue; } slot_time = compute_ping_slot(_beacon.beacon_time, current_time, ping_period, ping_nb, _ping.slot[i].address, _ping.slot[i].offset, _ping.slot[i].slot_nb, _ping.slot[i].rx_config); if (slot_time) { if (!best_slot_time || (slot_time < best_slot_time)) { _ping.slot_idx = i; best_slot_time = slot_time; } } } // Valid slot idx if ping slot to open in the current beacon period if (_ping.slot_idx != LORAMAC_INVALID_PING_SLOT) { delay = best_slot_time - get_gps_time(); if (delay > 0) { _lora_time->start(_ping_slot_timer, delay); } else { open_ping_slot(); } } } void LoRaMacClassB::open_ping_slot(void) { static lorawan_time_t last_ping_slot_time = 0; lorawan_time_t current_time; Lock(*this); // Check ping slot reception is enabled and receiver configuration is set if (!_opstatus.ping_on || _opstatus.paused || (_ping.slot_idx == LORAMAC_INVALID_PING_SLOT)) { return; } if (_open_rx_window(&_ping.slot[_ping.slot_idx].rx_config)) { _opstatus.ping_rx = 1; // Limiting ping slot trace period to prevent excessive debug output for small ping period current_time = _lora_time->get_current_time(); if ((current_time - last_ping_slot_time) > CLASSB_PING_OPEN_DBG_TRACE_PERIOD) { tr_debug("Ping address = %lx slot = %u open, Freq = %lu", _ping.slot[_ping.slot_idx].address, _ping.slot[_ping.slot_idx].slot_nb, _ping.slot[_ping.slot_idx].rx_config.frequency); last_ping_slot_time = current_time; } } } void LoRaMacClassB::pause(void) { Lock(*this); if (_opstatus.paused) { return; } _opstatus.paused = 1; if (_opstatus.beacon_on) { tr_debug("Pause Class B"); // Disable radio if beacon or ping slot is active if (_opstatus.beacon_rx) { _close_rx_window(RX_SLOT_WIN_BEACON); } else if (_opstatus.ping_rx) { if (_ping.slot_idx == 0) { _close_rx_window(RX_SLOT_WIN_UNICAST_PING_SLOT); } else { _close_rx_window(RX_SLOT_WIN_MULTICAST_PING_SLOT); } } } } void LoRaMacClassB::resume(void) { if (!_opstatus.paused) { return; } _opstatus.paused = 0; if (!_opstatus.beacon_on) { return; } tr_debug("Resume Class B"); // If pause occured during beacon slot in the beacon found state, send miss notification if (_opstatus.beacon_rx) { _opstatus.beacon_rx = 0; if (_opstatus.beacon_found) { send_beacon_miss_indication(); } } // Schedule next beacon window _lora_time->stop(_beacon_timer); schedule_beacon_window(); // Resume ping slots if (_opstatus.ping_on) { schedule_ping_slot(); } } lorawan_status_t LoRaMacClassB::get_last_rx_beacon(loramac_beacon_t &beacon) { if (_opstatus.beacon_found) { beacon = _beacon.received_beacon; return LORAWAN_STATUS_OK; } return LORAWAN_STATUS_NO_BEACON_FOUND; } bool LoRaMacClassB::add_multicast_address(uint32_t address) { // Look for available slot for (uint8_t i = 1; i <= MBED_CONF_LORA_CLASS_B_MULTICAST_ADDRESS_MAX_COUNT; i++) { if (_ping.slot[i].address != 0) { continue; } _ping.slot[i].address = address; if (_opstatus.ping_on) { compute_ping_offset(_beacon.beacon_time, address, protocol_params()->sys_params.ping_slot.ping_period, _ping.slot[i].offset); } return true; } return false; } void LoRaMacClassB::remove_multicast_address(uint32_t address) { for (uint8_t i = 1; i < MBED_CONF_LORA_CLASS_B_MULTICAST_ADDRESS_MAX_COUNT; i++) { if (_ping.slot[i].address == address) { _ping.slot[i].address = 0; } } }