本文主要結合LoRaNode SDK v4.4.2和LoRaWAN規範1.0.3來展開。
1、入網(激活)方式
可以看出,兩種入網(激活)方式:
OTAA(Over-The-Air Activation):空中激活
ABP(Activation By Personalization):手動激活
2、空中激活
空中激活的過程,其實就是和服務器數據交換的過程,且當上行或下行消息丟失時,需要重新交換一次數據。
入網的過程,設備需要以下3個參數:
DevEUI:設備ID;
AppEUI:應用ID;
AppKey:128位的跟密鑰,用於產生網絡會話密鑰NwkSKey 和應用會話密鑰AppSKey。
2.1、入網過程
從節點的角度看,入網過程是與服務器的兩次數據交換,分別是入網請求和入網回覆。
2.2、入網請求信息
入網信息包含AppEUI、DevEUI、DevNone,DevNone是一個隨機值 、入網請求是不加密的。
2.3、入網回覆信息
如果設備被允許入網,網絡服務器將會回覆一個“入網回覆”信息給到“入網請求”信息。
“入網回覆”信息包括AppNonce、NetID、DevAddr、DLSettings、RxDelay、CFList字段。
AppNonce是服務器產生的隨機值或者是以某種形式產生的唯一ID,用於終端設備計算NwkSKey和AppSKey。
3、手動激活
手動激活方式,沒有“入網請求”和“入網回覆”的過程。DevAddr、NwkSKey、AppSKey直接存儲在終端設備中。手動激活的時候
必須確保NwkSKey和AppSkey是唯一的。
4、代碼分析
我們很容易看出,SDK工程用狀態機在調度。
定義了6種狀態,如下:
static enum eDeviceState
{
DEVICE_STATE_RESTORE,
DEVICE_STATE_START,
DEVICE_STATE_JOIN,
DEVICE_STATE_SEND,
DEVICE_STATE_CYCLE,
DEVICE_STATE_SLEEP
}DeviceState;
由於代碼量很大,我們只截取main中while(1)中的代碼:
while( 1 )
{
// Process Radio IRQ
if( Radio.IrqProcess != NULL )
{
Radio.IrqProcess( );
}
// Processes the LoRaMac events
LoRaMacProcess( );
switch( DeviceState )
{
case DEVICE_STATE_RESTORE:
{
// Try to restore from NVM and query the mac if possible.
if( NvmCtxMgmtRestore( ) == NVMCTXMGMT_STATUS_SUCCESS ) //1.1.x以後才支持存儲管理
{
printf( "\r\n###### ===== CTXS RESTORED ==== ######\r\n\r\n" );
}
else
{
#if( OVER_THE_AIR_ACTIVATION == 0 ) //不使用otaa
// Tell the MAC layer which network server version are we connecting too.
mibReq.Type = MIB_ABP_LORAWAN_VERSION;
mibReq.Param.AbpLrWanVersion.Value = ABP_ACTIVATION_LRWAN_VERSION;
LoRaMacMibSetRequestConfirm( &mibReq );
#endif
#if( ABP_ACTIVATION_LRWAN_VERSION == ABP_ACTIVATION_LRWAN_VERSION_V10x )
mibReq.Type = MIB_GEN_APP_KEY;
mibReq.Param.GenAppKey = GenAppKey;
LoRaMacMibSetRequestConfirm( &mibReq );
#else
mibReq.Type = MIB_APP_KEY;
mibReq.Param.AppKey = AppKey;
LoRaMacMibSetRequestConfirm( &mibReq );
#endif
mibReq.Type = MIB_NWK_KEY;
mibReq.Param.NwkKey = NwkKey;
LoRaMacMibSetRequestConfirm( &mibReq );
// Initialize LoRaMac device unique ID if not already defined in Commissioning.h
if( ( devEui[0] == 0 ) && ( devEui[1] == 0 ) &&
( devEui[2] == 0 ) && ( devEui[3] == 0 ) &&
( devEui[4] == 0 ) && ( devEui[5] == 0 ) &&
( devEui[6] == 0 ) && ( devEui[7] == 0 ) )
{
BoardGetUniqueId( devEui );
}
mibReq.Type = MIB_DEV_EUI;
mibReq.Param.DevEui = devEui;
LoRaMacMibSetRequestConfirm( &mibReq );
mibReq.Type = MIB_JOIN_EUI;
mibReq.Param.JoinEui = joinEui;
LoRaMacMibSetRequestConfirm( &mibReq );
#if( OVER_THE_AIR_ACTIVATION == 0 ) //ABP方式,使用終端和服務器約定好的參數
// Choose a random device address if not already defined in Commissioning.h
if( DevAddr == 0 )
{
// Random seed initialization
srand1( BoardGetRandomSeed( ) );
// Choose a random device address
DevAddr = randr( 0, 0x01FFFFFF );
}
mibReq.Type = MIB_NET_ID;
mibReq.Param.NetID = LORAWAN_NETWORK_ID;
LoRaMacMibSetRequestConfirm( &mibReq );
mibReq.Type = MIB_DEV_ADDR;
mibReq.Param.DevAddr = DevAddr;
LoRaMacMibSetRequestConfirm( &mibReq );
mibReq.Type = MIB_F_NWK_S_INT_KEY;
mibReq.Param.FNwkSIntKey = FNwkSIntKey;
LoRaMacMibSetRequestConfirm( &mibReq );
mibReq.Type = MIB_S_NWK_S_INT_KEY;
mibReq.Param.SNwkSIntKey = SNwkSIntKey;
LoRaMacMibSetRequestConfirm( &mibReq );
mibReq.Type = MIB_NWK_S_ENC_KEY;
mibReq.Param.NwkSEncKey = NwkSEncKey;
LoRaMacMibSetRequestConfirm( &mibReq );
mibReq.Type = MIB_APP_S_KEY;
mibReq.Param.AppSKey = AppSKey;
LoRaMacMibSetRequestConfirm( &mibReq );
#endif
}
DeviceState = DEVICE_STATE_START;
break;
}
case DEVICE_STATE_START:
{
TimerInit( &TxNextPacketTimer, OnTxNextPacketTimerEvent );
TimerInit( &Led1Timer, OnLed1TimerEvent );
TimerSetValue( &Led1Timer, 25 );
TimerInit( &Led2Timer, OnLed2TimerEvent );
TimerSetValue( &Led2Timer, 25 );
mibReq.Type = MIB_PUBLIC_NETWORK;
mibReq.Param.EnablePublicNetwork = LORAWAN_PUBLIC_NETWORK;
LoRaMacMibSetRequestConfirm( &mibReq );
mibReq.Type = MIB_ADR;
mibReq.Param.AdrEnable = LORAWAN_ADR_ON;
LoRaMacMibSetRequestConfirm( &mibReq );
#if defined( REGION_EU868 ) || defined( REGION_RU864 ) || defined( REGION_CN779 ) || defined( REGION_EU433 )
LoRaMacTestSetDutyCycleOn( LORAWAN_DUTYCYCLE_ON );
#endif
mibReq.Type = MIB_SYSTEM_MAX_RX_ERROR;
mibReq.Param.SystemMaxRxError = 20;
LoRaMacMibSetRequestConfirm( &mibReq );
LoRaMacStart( );
mibReq.Type = MIB_NETWORK_ACTIVATION;
status = LoRaMacMibGetRequestConfirm( &mibReq );
if( status == LORAMAC_STATUS_OK )
{
if( mibReq.Param.NetworkActivation == ACTIVATION_TYPE_NONE ) //沒有激活
{
DeviceState = DEVICE_STATE_JOIN;
}
else
{
DeviceState = DEVICE_STATE_SEND;
NextTx = true;
}
}
break;
}
case DEVICE_STATE_JOIN:
{
mibReq.Type = MIB_DEV_EUI;
LoRaMacMibGetRequestConfirm( &mibReq );
printf( "DevEui : %02X", mibReq.Param.DevEui[0] );
for( int i = 1; i < 8; i++ )
{
printf( "-%02X", mibReq.Param.DevEui[i] );
}
printf( "\r\n" );
mibReq.Type = MIB_JOIN_EUI; //其實就是appeui
LoRaMacMibGetRequestConfirm( &mibReq );
printf( "AppEui : %02X", mibReq.Param.JoinEui[0] );
for( int i = 1; i < 8; i++ )
{
printf( "-%02X", mibReq.Param.JoinEui[i] );
}
printf( "\r\n" );
printf( "AppKey : %02X", NwkKey[0] );
for( int i = 1; i < 16; i++ )
{
printf( " %02X", NwkKey[i] );
}
printf( "\n\r\n" );
#if( OVER_THE_AIR_ACTIVATION == 0 )
printf( "###### ===== JOINED ==== ######\r\n" );
printf( "\r\nABP\r\n\r\n" );
printf( "DevAddr : %08lX\r\n", DevAddr );
printf( "NwkSKey : %02X", FNwkSIntKey[0] );
for( int i = 1; i < 16; i++ )
{
printf( " %02X", FNwkSIntKey[i] );
}
printf( "\r\n" );
printf( "AppSKey : %02X", AppSKey[0] );
for( int i = 1; i < 16; i++ )
{
printf( " %02X", AppSKey[i] );
}
printf( "\n\r\n" );
mibReq.Type = MIB_NETWORK_ACTIVATION;
mibReq.Param.NetworkActivation = ACTIVATION_TYPE_ABP;
LoRaMacMibSetRequestConfirm( &mibReq );
DeviceState = DEVICE_STATE_SEND;
#else
JoinNetwork( ); //入網操作
#endif
break;
}
case DEVICE_STATE_SEND:
{
if( NextTx == true )
{
PrepareTxFrame( AppPort );
NextTx = SendFrame( );
}
DeviceState = DEVICE_STATE_CYCLE;
break;
}
case DEVICE_STATE_CYCLE:
{
DeviceState = DEVICE_STATE_SLEEP;
if( ComplianceTest.Running == true )
{
// Schedule next packet transmission
TxDutyCycleTime = 5000; // 5000 ms
}
else
{
// Schedule next packet transmission
TxDutyCycleTime = APP_TX_DUTYCYCLE + randr( -APP_TX_DUTYCYCLE_RND, APP_TX_DUTYCYCLE_RND );
}
// Schedule next packet transmission
TimerSetValue( &TxNextPacketTimer, TxDutyCycleTime );
TimerStart( &TxNextPacketTimer );
break;
}
case DEVICE_STATE_SLEEP:
{
if( NvmCtxMgmtStore( ) == NVMCTXMGMT_STATUS_SUCCESS )
{
printf( "\r\n###### ===== CTXS STORED ==== ######\r\n" );
}
CRITICAL_SECTION_BEGIN( );
if( IsMacProcessPending == 1 )
{
// Clear flag and prevent MCU to go into low power modes.
IsMacProcessPending = 0;
}
else
{
// The MCU wakes up through events
BoardLowPowerHandler( );
}
CRITICAL_SECTION_END( );
break;
}
default:
{
DeviceState = DEVICE_STATE_START;
break;
}
}
依照上面的主循環裏面的代碼,我們畫了一個流程圖,如下:
可以看出:OTAA入網需要執行DEVICE_STATE_JOIN這個過程,入網之後上報數據;ABP是沒有入網過程的,直接就上報數據了。最終在3個狀態之間切換:
我們跟蹤一下DEVICE_STATE_JOIN這個狀態,看一下這個入網的過程:
函數體太長,我們僅列出函數名:
JoinNetwork——>LoRaMacMlmeRequest——>SendReJoinReq——>ScheduleTx——>SecureFrame——>LoRaMacCryptoPrepareJoinRequest——>LoRaMacSerializerJoinRequest——>SendFrameOnChannel——>Radio.Send
至此,入網請求消息,就通過射頻發送出去了。
上述做了一系列的操作,其實就是封裝數據包、加密數據,由此也可以看出LoRaWAN就是純軟件層面的東西。
我們重點看一下,LoRaMacSerializerJoinRequest這個函數:我們列出函數的原型如下:
LoRaMacSerializerStatus_t LoRaMacSerializerJoinRequest( LoRaMacMessageJoinRequest_t* macMsg )
{
if( ( macMsg == 0 ) || ( macMsg->Buffer == 0 ) )
{
return LORAMAC_SERIALIZER_ERROR_NPE;
}
uint16_t bufItr = 0;
// Check macMsg->BufSize
if( macMsg->BufSize < LORAMAC_JOIN_REQ_MSG_SIZE )
{
return LORAMAC_SERIALIZER_ERROR_BUF_SIZE;
}
macMsg->Buffer[bufItr++] = macMsg->MHDR.Value;
memcpyr( &macMsg->Buffer[bufItr], macMsg->JoinEUI, LORAMAC_JOIN_EUI_FIELD_SIZE );
bufItr += LORAMAC_JOIN_EUI_FIELD_SIZE;
memcpyr( &macMsg->Buffer[bufItr], macMsg->DevEUI, LORAMAC_DEV_EUI_FIELD_SIZE );
bufItr += LORAMAC_DEV_EUI_FIELD_SIZE;
macMsg->Buffer[bufItr++] = macMsg->DevNonce & 0xFF;
macMsg->Buffer[bufItr++] = ( macMsg->DevNonce >> 8 ) & 0xFF;
macMsg->Buffer[bufItr++] = macMsg->MIC & 0xFF;
macMsg->Buffer[bufItr++] = ( macMsg->MIC >> 8 ) & 0xFF;
macMsg->Buffer[bufItr++] = ( macMsg->MIC >> 16 ) & 0xFF;
macMsg->Buffer[bufItr++] = ( macMsg->MIC >> 24 ) & 0xFF;
macMsg->BufSize = bufItr;
return LORAMAC_SERIALIZER_SUCCESS;
}
在這個函數裏面實現了數據的封裝,我們看到了MHDR、JoinEUI(特別說明一下JoinEUI和APPEUI是同一個東西)、DevEUI、
DevNonce、MIC字段,MHDR是Mac數據頭、MIC是數據一致性校驗,剩下的3個字段,與我們上面從LoRaWAN規範join
request一節中看到的一樣。
我們再來看看,LoRaWAN規範裏面講的MAC消息格式:
這個圖中小藍框框起來的地方,正是我們這函數中數據封裝的各個字段。
至此我們可以總結一下,入網請求數據包的格式:
MHDR |
JoinEUI |
DevEUI |
DevNone |
MIC |
1byte |
8byte |
8byte |
2byte |
4byte |
可以看出,入網請求包長度是1+8+8+2+4=23byte。
關於“入網回覆”,我們先不管機制是怎麼樣的,我們暫時只查看相應的數據解包過程,我們類比發包的過程:先封包再發送,收
包剛好和這個相反,收到數據包,再解包,查看每個字段。
static void ProcessRadioRxDone( void )這個函數就是對射頻接收到的數據的處理了,可以看到使用switch case語句,通過
macHdr.Bits.MType字段對接收到的數據包進行了區分,FRAME_TYPE_JOIN_ACCEPT這個類型的包,正是我們的“入網回覆”
數據,順着往下跟蹤LoRaMacCryptoHandleJoinAccept——>LoRaMacParserJoinAccept,正是在LoRaMacParserJoinAccept這個
函數裏面解析“入網回覆”數據的,我們列出這個函數的原型如下:
LoRaMacParserStatus_t LoRaMacParserJoinAccept( LoRaMacMessageJoinAccept_t* macMsg )
{
if( ( macMsg == 0 ) || ( macMsg->Buffer == 0 ) )
{
return LORAMAC_PARSER_ERROR_NPE;
}
uint16_t bufItr = 0;
macMsg->MHDR.Value = macMsg->Buffer[bufItr++];
memcpy1( macMsg->JoinNonce, &macMsg->Buffer[bufItr], 3 );
bufItr = bufItr + 3;
memcpy1( macMsg->NetID, &macMsg->Buffer[bufItr], 3 );
bufItr = bufItr + 3;
macMsg->DevAddr = ( uint32_t ) macMsg->Buffer[bufItr++];
macMsg->DevAddr |= ( ( uint32_t ) macMsg->Buffer[bufItr++] << 8 );
macMsg->DevAddr |= ( ( uint32_t ) macMsg->Buffer[bufItr++] << 16 );
macMsg->DevAddr |= ( ( uint32_t ) macMsg->Buffer[bufItr++] << 24 );
macMsg->DLSettings.Value = macMsg->Buffer[bufItr++];
macMsg->RxDelay = macMsg->Buffer[bufItr++];
if( ( macMsg->BufSize - LORAMAC_MIC_FIELD_SIZE - bufItr ) == LORAMAC_C_FLIST_FIELD_SIZE )
{
memcpy1( macMsg->CFList, &macMsg->Buffer[bufItr], LORAMAC_C_FLIST_FIELD_SIZE );
bufItr = bufItr + LORAMAC_C_FLIST_FIELD_SIZE;
}
else if( ( macMsg->BufSize - LORAMAC_MIC_FIELD_SIZE - bufItr ) > 0 )
{
return LORAMAC_PARSER_FAIL;
}
macMsg->MIC = ( uint32_t ) macMsg->Buffer[bufItr++];
macMsg->MIC |= ( ( uint32_t ) macMsg->Buffer[bufItr++] << 8 );
macMsg->MIC |= ( ( uint32_t ) macMsg->Buffer[bufItr++] << 16 );
macMsg->MIC |= ( ( uint32_t ) macMsg->Buffer[bufItr++] << 24 );
return LORAMAC_PARSER_SUCCESS;
}
我們依次可以看到MHDR、JoinNonce(和規範中的AppNonce是同一個東西)、NetID、DevAddr、DLSettings、RxDelay、
CFList、MIC字段,這也與我們上面在LoRaWAN規範裏面看到的數據包吻合。
我們現在來總結一下數據入網回覆數據包的格式:
MHDR |
JoinNonce |
NetID |
DevAddr |
DLSettings |
RxDelay |
CFList |
MIC |
1byte |
3byte |
3byte |
4byte |
1byte |
1byte |
nbyte |
4byte |
其中CFList這個字段是可選的,如果沒有的話,就是0。
我們測試一下,看看“入網請求”和“入網回覆”,如下:
入網請求的數據幀爲:00 01 00 00 00 00 00 00 00 DF 46 00 00 10 FF FF FF 0F A6 C4 38 42 A7
剛好是23個字節,可以看出,入網請求是沒有加密的,每個代表字段含義如下:
00:MHDR
01 00 00 00 00 00 00 00:JoinEUI
DE 46 00 00 10 FF FF FF:DevEUI
0F A6:DevNone
C4 38 42 A7:MIC
入網回覆的數據幀爲:20 69 e1 e7 2b 3e 0b 52 c9 c5 de 36 4f e2 69 41 25
是17+(0或16)個字節,20是MHDR,之後的數據是經過加密的,
經過解密之後的數據如下:7d c4 83 03 02 01 92 c5 f1 07 00 00 e4 5b 50 1b
7d c4 83 :JoinNonce
03 02 01 :NetID
92 c5 f1 07:DevAddr
00 :DLSettings
00 :RxDelay
e4 5b 50 1b:MIC
5、總結
OTAA入網:有入網過程,入網之後服務器分配DevAddr,節點計算出NwkSKey、AppSKey兩個加密密鑰。
ABP入網:無入網過程,DevAddr、NwkSKey、AppSKey直接存儲在終端設備中(說直白一點就是,節點和服務器提前約定好了參數)。
==========================end========================================
推薦閱讀:
歡迎關注公衆號:“物聯網思考”,獲取更多開發資料、經驗。