/* * Copyright (c) 2020 Manivannan Sadhasivam * * SPDX-License-Identifier: Apache-2.0 */ #include #include #include #include #include "lw_priv.h" #include #include #include "nvm/lorawan_nvm.h" #ifdef CONFIG_LORAMAC_REGION_AS923 #define DEFAULT_LORAWAN_REGION LORAMAC_REGION_AS923 #define DEFAULT_LORAWAN_CHANNELS_MASK_SIZE LORAWAN_CHANNELS_MASK_SIZE_AS923 #elif CONFIG_LORAMAC_REGION_AU915 #define DEFAULT_LORAWAN_REGION LORAMAC_REGION_AU915 #define DEFAULT_LORAWAN_CHANNELS_MASK_SIZE LORAWAN_CHANNELS_MASK_SIZE_AU915 #elif CONFIG_LORAMAC_REGION_CN470 #define DEFAULT_LORAWAN_REGION LORAMAC_REGION_CN470 #define DEFAULT_LORAWAN_CHANNELS_MASK_SIZE LORAWAN_CHANNELS_MASK_SIZE_CN470 #elif CONFIG_LORAMAC_REGION_CN779 #define DEFAULT_LORAWAN_REGION LORAMAC_REGION_CN779 #define DEFAULT_LORAWAN_CHANNELS_MASK_SIZE LORAWAN_CHANNELS_MASK_SIZE_CN779 #elif CONFIG_LORAMAC_REGION_EU433 #define DEFAULT_LORAWAN_REGION LORAMAC_REGION_EU433 #define DEFAULT_LORAWAN_CHANNELS_MASK_SIZE LORAWAN_CHANNELS_MASK_SIZE_EU433 #elif CONFIG_LORAMAC_REGION_EU868 #define DEFAULT_LORAWAN_REGION LORAMAC_REGION_EU868 #define DEFAULT_LORAWAN_CHANNELS_MASK_SIZE LORAWAN_CHANNELS_MASK_SIZE_EU868 #elif CONFIG_LORAMAC_REGION_KR920 #define DEFAULT_LORAWAN_REGION LORAMAC_REGION_KR920 #define DEFAULT_LORAWAN_CHANNELS_MASK_SIZE LORAWAN_CHANNELS_MASK_SIZE_KR920 #elif CONFIG_LORAMAC_REGION_IN865 #define DEFAULT_LORAWAN_REGION LORAMAC_REGION_IN865 #define DEFAULT_LORAWAN_CHANNELS_MASK_SIZE LORAWAN_CHANNELS_MASK_SIZE_IN865 #elif CONFIG_LORAMAC_REGION_US915 #define DEFAULT_LORAWAN_REGION LORAMAC_REGION_US915 #define DEFAULT_LORAWAN_CHANNELS_MASK_SIZE LORAWAN_CHANNELS_MASK_SIZE_US915 #elif CONFIG_LORAMAC_REGION_RU864 #define DEFAULT_LORAWAN_REGION LORAMAC_REGION_RU864 #define DEFAULT_LORAWAN_CHANNELS_MASK_SIZE LORAWAN_CHANNELS_MASK_SIZE_RU864 #else #error "At least one LoRaWAN region should be selected" #endif /* Use version 1.0.3.0 for ABP */ #define LORAWAN_ABP_VERSION 0x01000300 #include LOG_MODULE_REGISTER(lorawan, CONFIG_LORAWAN_LOG_LEVEL); K_SEM_DEFINE(mlme_confirm_sem, 0, 1); K_SEM_DEFINE(mcps_confirm_sem, 0, 1); K_MUTEX_DEFINE(lorawan_join_mutex); K_MUTEX_DEFINE(lorawan_send_mutex); /* We store both the default datarate requested through lorawan_set_datarate * and the current datarate so that we can use the default datarate for all * join requests, even as the current datarate changes due to ADR. */ static enum lorawan_datarate default_datarate; static enum lorawan_datarate current_datarate; static bool lorawan_adr_enable; static sys_slist_t dl_callbacks; static LoRaMacPrimitives_t mac_primitives; static LoRaMacCallback_t mac_callbacks; static LoRaMacEventInfoStatus_t last_mcps_confirm_status; static LoRaMacEventInfoStatus_t last_mlme_confirm_status; static LoRaMacEventInfoStatus_t last_mcps_indication_status; static LoRaMacEventInfoStatus_t last_mlme_indication_status; static LoRaMacRegion_t selected_region = DEFAULT_LORAWAN_REGION; static enum lorawan_channels_mask_size region_channels_mask_size = DEFAULT_LORAWAN_CHANNELS_MASK_SIZE; static lorawan_battery_level_cb_t battery_level_cb; static lorawan_dr_changed_cb_t dr_changed_cb; /* implementation required by the soft-se (software secure element) */ void BoardGetUniqueId(uint8_t *id) { /* Do not change the default value */ } static uint8_t get_battery_level(void) { if (battery_level_cb != NULL) { return battery_level_cb(); } else { return 255; } } static void mac_process_notify(void) { LoRaMacProcess(); } static void datarate_observe(bool force_notification) { MibRequestConfirm_t mib_req; mib_req.Type = MIB_CHANNELS_DATARATE; LoRaMacMibGetRequestConfirm(&mib_req); if ((mib_req.Param.ChannelsDatarate != current_datarate) || (force_notification)) { current_datarate = mib_req.Param.ChannelsDatarate; if (dr_changed_cb != NULL) { dr_changed_cb(current_datarate); } LOG_INF("Datarate changed: DR_%d", current_datarate); } } static void mcps_confirm_handler(McpsConfirm_t *mcps_confirm) { LOG_DBG("Received McpsConfirm (for McpsRequest %d)", mcps_confirm->McpsRequest); if (mcps_confirm->Status != LORAMAC_EVENT_INFO_STATUS_OK) { LOG_ERR("McpsRequest failed : %s", lorawan_eventinfo2str(mcps_confirm->Status)); } else { LOG_DBG("McpsRequest success!"); } /* Datarate may have changed due to a missed ADRACK */ if (lorawan_adr_enable) { datarate_observe(false); } last_mcps_confirm_status = mcps_confirm->Status; k_sem_give(&mcps_confirm_sem); } static void mcps_indication_handler(McpsIndication_t *mcps_indication) { struct lorawan_downlink_cb *cb; LOG_DBG("Received McpsIndication %d", mcps_indication->McpsIndication); if (mcps_indication->Status != LORAMAC_EVENT_INFO_STATUS_OK) { LOG_ERR("McpsIndication failed : %s", lorawan_eventinfo2str(mcps_indication->Status)); return; } /* Datarate can change as result of ADR command from server */ if (lorawan_adr_enable) { datarate_observe(false); } /* Iterate over all registered downlink callbacks */ SYS_SLIST_FOR_EACH_CONTAINER(&dl_callbacks, cb, node) { if ((cb->port == LW_RECV_PORT_ANY) || (cb->port == mcps_indication->Port)) { cb->cb(mcps_indication->Port, /* IsUplinkTxPending also indicates pending downlinks */ mcps_indication->IsUplinkTxPending == 1, mcps_indication->Rssi, mcps_indication->Snr, mcps_indication->BufferSize, mcps_indication->Buffer); } } last_mcps_indication_status = mcps_indication->Status; } static void mlme_confirm_handler(MlmeConfirm_t *mlme_confirm) { MibRequestConfirm_t mib_req; LOG_DBG("Received MlmeConfirm (for MlmeRequest %d)", mlme_confirm->MlmeRequest); if (mlme_confirm->Status != LORAMAC_EVENT_INFO_STATUS_OK) { LOG_ERR("MlmeConfirm failed : %s", lorawan_eventinfo2str(mlme_confirm->Status)); goto out_sem; } switch (mlme_confirm->MlmeRequest) { case MLME_JOIN: mib_req.Type = MIB_DEV_ADDR; LoRaMacMibGetRequestConfirm(&mib_req); LOG_INF("Joined network! DevAddr: %08x", mib_req.Param.DevAddr); break; case MLME_LINK_CHECK: /* Not implemented */ LOG_INF("Link check not implemented yet!"); break; default: break; } out_sem: last_mlme_confirm_status = mlme_confirm->Status; k_sem_give(&mlme_confirm_sem); } static void mlme_indication_handler(MlmeIndication_t *mlme_indication) { LOG_DBG("Received MlmeIndication %d", mlme_indication->MlmeIndication); last_mlme_indication_status = mlme_indication->Status; } static LoRaMacStatus_t lorawan_join_otaa( const struct lorawan_join_config *join_cfg) { MlmeReq_t mlme_req; MibRequestConfirm_t mib_req; mlme_req.Type = MLME_JOIN; mlme_req.Req.Join.Datarate = default_datarate; mlme_req.Req.Join.NetworkActivation = ACTIVATION_TYPE_OTAA; if (IS_ENABLED(CONFIG_LORAWAN_NVM_NONE)) { /* Retrieve the NVM context to store device nonce */ mib_req.Type = MIB_NVM_CTXS; if (LoRaMacMibGetRequestConfirm(&mib_req) != LORAMAC_STATUS_OK) { LOG_ERR("Could not get NVM context"); return -EINVAL; } mib_req.Param.Contexts->Crypto.DevNonce = join_cfg->otaa.dev_nonce; } mib_req.Type = MIB_DEV_EUI; mib_req.Param.DevEui = join_cfg->dev_eui; LoRaMacMibSetRequestConfirm(&mib_req); mib_req.Type = MIB_JOIN_EUI; mib_req.Param.JoinEui = join_cfg->otaa.join_eui; LoRaMacMibSetRequestConfirm(&mib_req); mib_req.Type = MIB_NWK_KEY; mib_req.Param.NwkKey = join_cfg->otaa.nwk_key; LoRaMacMibSetRequestConfirm(&mib_req); mib_req.Type = MIB_APP_KEY; mib_req.Param.AppKey = join_cfg->otaa.app_key; LoRaMacMibSetRequestConfirm(&mib_req); return LoRaMacMlmeRequest(&mlme_req); } static LoRaMacStatus_t lorawan_join_abp( const struct lorawan_join_config *join_cfg) { MibRequestConfirm_t mib_req; mib_req.Type = MIB_ABP_LORAWAN_VERSION; mib_req.Param.AbpLrWanVersion.Value = LORAWAN_ABP_VERSION; LoRaMacMibSetRequestConfirm(&mib_req); mib_req.Type = MIB_NET_ID; mib_req.Param.NetID = 0; LoRaMacMibSetRequestConfirm(&mib_req); mib_req.Type = MIB_DEV_ADDR; mib_req.Param.DevAddr = join_cfg->abp.dev_addr; LoRaMacMibSetRequestConfirm(&mib_req); mib_req.Type = MIB_F_NWK_S_INT_KEY; mib_req.Param.FNwkSIntKey = join_cfg->abp.nwk_skey; LoRaMacMibSetRequestConfirm(&mib_req); mib_req.Type = MIB_S_NWK_S_INT_KEY; mib_req.Param.SNwkSIntKey = join_cfg->abp.nwk_skey; LoRaMacMibSetRequestConfirm(&mib_req); mib_req.Type = MIB_NWK_S_ENC_KEY; mib_req.Param.NwkSEncKey = join_cfg->abp.nwk_skey; LoRaMacMibSetRequestConfirm(&mib_req); mib_req.Type = MIB_APP_S_KEY; mib_req.Param.AppSKey = join_cfg->abp.app_skey; LoRaMacMibSetRequestConfirm(&mib_req); mib_req.Type = MIB_NETWORK_ACTIVATION; mib_req.Param.NetworkActivation = ACTIVATION_TYPE_ABP; LoRaMacMibSetRequestConfirm(&mib_req); return LORAMAC_STATUS_OK; } int lorawan_set_region(enum lorawan_region region) { switch (region) { #if defined(CONFIG_LORAMAC_REGION_AS923) case LORAWAN_REGION_AS923: selected_region = LORAMAC_REGION_AS923; region_channels_mask_size = LORAWAN_CHANNELS_MASK_SIZE_AS923; break; #endif #if defined(CONFIG_LORAMAC_REGION_AU915) case LORAWAN_REGION_AU915: selected_region = LORAMAC_REGION_AU915; region_channels_mask_size = LORAWAN_CHANNELS_MASK_SIZE_AU915; break; #endif #if defined(CONFIG_LORAMAC_REGION_CN470) case LORAWAN_REGION_CN470: selected_region = LORAMAC_REGION_CN470; region_channels_mask_size = LORAWAN_CHANNELS_MASK_SIZE_CN470; break; #endif #if defined(CONFIG_LORAMAC_REGION_CN779) case LORAWAN_REGION_CN779: selected_region = LORAMAC_REGION_CN779; region_channels_mask_size = LORAWAN_CHANNELS_MASK_SIZE_CN779; break; #endif #if defined(CONFIG_LORAMAC_REGION_EU433) case LORAWAN_REGION_EU433: selected_region = LORAMAC_REGION_EU433; region_channels_mask_size = LORAWAN_CHANNELS_MASK_SIZE_EU433; break; #endif #if defined(CONFIG_LORAMAC_REGION_EU868) case LORAWAN_REGION_EU868: selected_region = LORAMAC_REGION_EU868; region_channels_mask_size = LORAWAN_CHANNELS_MASK_SIZE_EU868; break; #endif #if defined(CONFIG_LORAMAC_REGION_KR920) case LORAWAN_REGION_KR920: selected_region = LORAMAC_REGION_KR920; region_channels_mask_size = LORAWAN_CHANNELS_MASK_SIZE_KR920; break; #endif #if defined(CONFIG_LORAMAC_REGION_IN865) case LORAWAN_REGION_IN865: selected_region = LORAMAC_REGION_IN865; region_channels_mask_size = LORAWAN_CHANNELS_MASK_SIZE_IN865; break; #endif #if defined(CONFIG_LORAMAC_REGION_US915) case LORAWAN_REGION_US915: selected_region = LORAMAC_REGION_US915; region_channels_mask_size = LORAWAN_CHANNELS_MASK_SIZE_US915; break; #endif #if defined(CONFIG_LORAMAC_REGION_RU864) case LORAWAN_REGION_RU864: selected_region = LORAMAC_REGION_RU864; region_channels_mask_size = LORAWAN_CHANNELS_MASK_SIZE_RU864; break; #endif default: LOG_ERR("No support for region %d!", region); return -ENOTSUP; } LOG_DBG("Selected region %d", region); return 0; } int lorawan_join(const struct lorawan_join_config *join_cfg) { MibRequestConfirm_t mib_req; LoRaMacStatus_t status; int ret = 0; k_mutex_lock(&lorawan_join_mutex, K_FOREVER); /* MIB_PUBLIC_NETWORK powers on the radio and does not turn it off */ mib_req.Type = MIB_PUBLIC_NETWORK; mib_req.Param.EnablePublicNetwork = IS_ENABLED(CONFIG_LORAWAN_PUBLIC_NETWORK); LoRaMacMibSetRequestConfirm(&mib_req); if (join_cfg->mode == LORAWAN_ACT_OTAA) { status = lorawan_join_otaa(join_cfg); if (status != LORAMAC_STATUS_OK) { LOG_ERR("OTAA join failed: %s", lorawan_status2str(status)); ret = lorawan_status2errno(status); goto out; } LOG_DBG("Network join request sent!"); /* * We can be sure that the semaphore will be released for * both success and failure cases after a specific time period. * So we can use K_FOREVER and no need to check the return val. */ k_sem_take(&mlme_confirm_sem, K_FOREVER); if (last_mlme_confirm_status != LORAMAC_EVENT_INFO_STATUS_OK) { ret = lorawan_eventinfo2errno(last_mlme_confirm_status); goto out; } } else if (join_cfg->mode == LORAWAN_ACT_ABP) { status = lorawan_join_abp(join_cfg); if (status != LORAMAC_STATUS_OK) { LOG_ERR("ABP join failed: %s", lorawan_status2str(status)); ret = lorawan_status2errno(status); goto out; } } else { ret = -EINVAL; } out: /* If the join succeeded */ if (ret == 0) { /* * Several regions (AS923, AU915, US915) overwrite the * datarate as part of the join process. Reset the datarate * to the value requested (and validated) in * lorawan_set_datarate so that the MAC layer is aware of the * set datarate for LoRaMacQueryTxPossible. This is only * performed when ADR is disabled as it the network servers * responsibility to increase datarates when ADR is enabled. */ if (!lorawan_adr_enable) { MibRequestConfirm_t mib_req2; mib_req2.Type = MIB_CHANNELS_DATARATE; mib_req2.Param.ChannelsDatarate = default_datarate; LoRaMacMibSetRequestConfirm(&mib_req2); } /* * Force a notification of the datarate on network join as the * user may not have explicitly set a datarate to use. */ datarate_observe(true); } k_mutex_unlock(&lorawan_join_mutex); return ret; } int lorawan_set_class(enum lorawan_class dev_class) { MibRequestConfirm_t mib_req; DeviceClass_t current_class; LoRaMacStatus_t status; mib_req.Type = MIB_DEVICE_CLASS; LoRaMacMibGetRequestConfirm(&mib_req); current_class = mib_req.Param.Class; switch (dev_class) { case LORAWAN_CLASS_A: mib_req.Param.Class = CLASS_A; break; case LORAWAN_CLASS_B: LOG_ERR("Class B not supported yet!"); return -ENOTSUP; case LORAWAN_CLASS_C: mib_req.Param.Class = CLASS_C; break; default: return -EINVAL; } if (mib_req.Param.Class != current_class) { status = LoRaMacMibSetRequestConfirm(&mib_req); if (status != LORAMAC_STATUS_OK) { LOG_ERR("Failed to set device class: %s", lorawan_status2str(status)); return lorawan_status2errno(status); } } return 0; } int lorawan_set_channels_mask(uint16_t *channels_mask, size_t channels_mask_size) { MibRequestConfirm_t mib_req; if ((channels_mask == NULL) || (channels_mask_size != region_channels_mask_size)) { return -EINVAL; } /* Notify MAC layer of the requested channel mask. */ mib_req.Type = MIB_CHANNELS_MASK; mib_req.Param.ChannelsMask = channels_mask; if (LoRaMacMibSetRequestConfirm(&mib_req) != LORAMAC_STATUS_OK) { /* Channels mask is invalid for this region. */ return -EINVAL; } return 0; } int lorawan_set_datarate(enum lorawan_datarate dr) { MibRequestConfirm_t mib_req; /* Bail out if using ADR */ if (lorawan_adr_enable) { return -EINVAL; } /* Notify MAC layer of the requested datarate */ mib_req.Type = MIB_CHANNELS_DATARATE; mib_req.Param.ChannelsDatarate = dr; if (LoRaMacMibSetRequestConfirm(&mib_req) != LORAMAC_STATUS_OK) { /* Datarate is invalid for this region */ return -EINVAL; } default_datarate = dr; current_datarate = dr; return 0; } void lorawan_get_payload_sizes(uint8_t *max_next_payload_size, uint8_t *max_payload_size) { LoRaMacTxInfo_t tx_info; /* QueryTxPossible cannot fail */ (void) LoRaMacQueryTxPossible(0, &tx_info); *max_next_payload_size = tx_info.MaxPossibleApplicationDataSize; *max_payload_size = tx_info.CurrentPossiblePayloadSize; } enum lorawan_datarate lorawan_get_min_datarate(void) { MibRequestConfirm_t mib_req; mib_req.Type = MIB_CHANNELS_MIN_TX_DATARATE; LoRaMacMibGetRequestConfirm(&mib_req); return mib_req.Param.ChannelsMinTxDatarate; } void lorawan_enable_adr(bool enable) { MibRequestConfirm_t mib_req; if (enable != lorawan_adr_enable) { lorawan_adr_enable = enable; mib_req.Type = MIB_ADR; mib_req.Param.AdrEnable = lorawan_adr_enable; LoRaMacMibSetRequestConfirm(&mib_req); } } int lorawan_set_conf_msg_tries(uint8_t tries) { MibRequestConfirm_t mib_req; mib_req.Type = MIB_CHANNELS_NB_TRANS; mib_req.Param.ChannelsNbTrans = tries; if (LoRaMacMibSetRequestConfirm(&mib_req) != LORAMAC_STATUS_OK) { return -EINVAL; } return 0; } int lorawan_send(uint8_t port, uint8_t *data, uint8_t len, enum lorawan_message_type type) { LoRaMacStatus_t status; McpsReq_t mcps_req; LoRaMacTxInfo_t tx_info; int ret = 0; bool empty_frame = false; if (data == NULL && len > 0) { return -EINVAL; } k_mutex_lock(&lorawan_send_mutex, K_FOREVER); status = LoRaMacQueryTxPossible(len, &tx_info); if (status != LORAMAC_STATUS_OK) { /* * If status indicates an error, then most likely the payload * has exceeded the maximum possible length for the current * region and datarate. We can't do much other than sending * empty frame in order to flush MAC commands in stack and * hoping the application to lower the payload size for * next try. */ LOG_ERR("LoRaWAN Query Tx Possible Failed: %s", lorawan_status2str(status)); empty_frame = true; mcps_req.Type = MCPS_UNCONFIRMED; mcps_req.Req.Unconfirmed.fBuffer = NULL; mcps_req.Req.Unconfirmed.fBufferSize = 0; mcps_req.Req.Unconfirmed.Datarate = DR_0; } else { switch (type) { case LORAWAN_MSG_UNCONFIRMED: mcps_req.Type = MCPS_UNCONFIRMED; break; case LORAWAN_MSG_CONFIRMED: mcps_req.Type = MCPS_CONFIRMED; break; } mcps_req.Req.Unconfirmed.fPort = port; mcps_req.Req.Unconfirmed.fBuffer = data; mcps_req.Req.Unconfirmed.fBufferSize = len; mcps_req.Req.Unconfirmed.Datarate = current_datarate; } status = LoRaMacMcpsRequest(&mcps_req); if (status != LORAMAC_STATUS_OK) { LOG_ERR("LoRaWAN Send failed: %s", lorawan_status2str(status)); ret = lorawan_status2errno(status); goto out; } /* * Always wait for MAC operations to complete. * We can be sure that the semaphore will be released for * both success and failure cases after a specific time period. * So we can use K_FOREVER and no need to check the return val. */ k_sem_take(&mcps_confirm_sem, K_FOREVER); if (last_mcps_confirm_status != LORAMAC_EVENT_INFO_STATUS_OK) { ret = lorawan_eventinfo2errno(last_mcps_confirm_status); } /* * Indicate to the application that the provided data was not sent and * it has to resend the packet. */ if (empty_frame) { ret = -EAGAIN; } out: k_mutex_unlock(&lorawan_send_mutex); return ret; } void lorawan_register_battery_level_callback(lorawan_battery_level_cb_t cb) { battery_level_cb = cb; } void lorawan_register_downlink_callback(struct lorawan_downlink_cb *cb) { sys_slist_append(&dl_callbacks, &cb->node); } void lorawan_register_dr_changed_callback(lorawan_dr_changed_cb_t cb) { dr_changed_cb = cb; } int lorawan_start(void) { LoRaMacStatus_t status; MibRequestConfirm_t mib_req; GetPhyParams_t phy_params; PhyParam_t phy_param; status = LoRaMacInitialization(&mac_primitives, &mac_callbacks, selected_region); if (status != LORAMAC_STATUS_OK) { LOG_ERR("LoRaMacInitialization failed: %s", lorawan_status2str(status)); return -EINVAL; } LOG_DBG("LoRaMAC Initialized"); if (!IS_ENABLED(CONFIG_LORAWAN_NVM_NONE)) { lorawan_nvm_init(); lorawan_nvm_data_restore(); } status = LoRaMacStart(); if (status != LORAMAC_STATUS_OK) { LOG_ERR("Failed to start the LoRaMAC stack: %s", lorawan_status2str(status)); return -EINVAL; } /* Retrieve the default TX datarate for selected region */ phy_params.Attribute = PHY_DEF_TX_DR; phy_param = RegionGetPhyParam(selected_region, &phy_params); default_datarate = phy_param.Value; current_datarate = default_datarate; /* TODO: Move these to a proper location */ mib_req.Type = MIB_SYSTEM_MAX_RX_ERROR; mib_req.Param.SystemMaxRxError = CONFIG_LORAWAN_SYSTEM_MAX_RX_ERROR; LoRaMacMibSetRequestConfirm(&mib_req); return 0; } static int lorawan_init(void) { sys_slist_init(&dl_callbacks); mac_primitives.MacMcpsConfirm = mcps_confirm_handler; mac_primitives.MacMcpsIndication = mcps_indication_handler; mac_primitives.MacMlmeConfirm = mlme_confirm_handler; mac_primitives.MacMlmeIndication = mlme_indication_handler; mac_callbacks.GetBatteryLevel = get_battery_level; mac_callbacks.GetTemperatureLevel = NULL; if (IS_ENABLED(CONFIG_LORAWAN_NVM_NONE)) { mac_callbacks.NvmDataChange = NULL; } else { mac_callbacks.NvmDataChange = lorawan_nvm_data_mgmt_event; } mac_callbacks.MacProcessNotify = mac_process_notify; return 0; } SYS_INIT(lorawan_init, POST_KERNEL, 0);