IoT/实现和分析 NB-IoT+DTLS+PSK 接入华为云物联网平台IoTDA过程,总结避坑攻略
文章目录
- 概述
- 代码假连接成功?
- PC-AT 的余威
- 破坏dtls配置
- 破坏PSK配置
- 设备标识码和设备ID
- 设备ID默认命名规则
- node_id 即设备标识码
- 只用node_id不够?
- node_id不可重复使用?
- 非 IMEI 不可做node_id?
- 设备ID/app_server.ep_id
- 接入流程分析
- 参数结构字段
- 用户配置操作
- 连接过程注册al层
- boudica150 入口函数
- 指令序列分析
- NB-IoT 用户层接入参数
- 平台设备-PSK格式
- WIFI接入代码-PSK格式
- NB-IoT接入代码-PSK格式
- 实现DTLS+PSK的AT函数
- 真正地MCU-AT接入IoTDA
概述
本文是对#<IoT/HCIP实验-5/基于NB-IoT的智慧农业实验(平台侧开发+端侧编码+基础调试分析)># 中实验过程的一次深刻反思,重新对NB-IoT设备接入参数,如node_id、pskid、psk、设备ID等概念进行了对比理解,更细致分析了oc_lwm2m中基于NB-IoT的连接过程,并在此基础上将源码中NB-IoT非安全链接模式,补充修正为使用 DTLS+PSK / 5684 CoAPs 的安全接入IoTDA模式。
@HISTORY
在 <实验5 基于NB-IoT的智慧农业实验> 中,我们"完美地"接入了IoTDA,还进行了业务操作交互验证。但实际上这里头埋了好几个乌龙,过了很长时间,我才后知后觉的从坑里爬出来。在这次乌龙内外涉及到了几个问题,一并描述如下,本文将逐一解答:
0、PC-AT指令的部分设置是存储在模组内的,可能导致,即使MCU-AT不执行某些操作,也会成功连接!
1、NB-IoT通信模组通过串口以AT指令形式与MCU通信。
1、注册NB-IoT设备时,设备标识只能是IMEI国际移动设备识别码?
2、在当下连接到IoTDA,是否必须启用DTLS?不再支持5683非安全接入?
3、设备密钥格式和位数限制,在代码中以字符串定义还是以十六进制数字字节数组定义?
@NOTE
转载请标明出处,https://blog.csdn.net/quguanxin/category_12929470.html
代码假连接成功?
在实验5-Wifi的后期阶段,我已经开始意识到一个存在于前期"基于NB-IoT的智慧农业"实验中的大错误:我的代码明显是错误的,但是我却成功链接到了IoTDA平台。最大的问题在于,示例代码中采用的是lwm2m非安全接入方式,源代码中并没有DTLS和PSK的操作代码或AT指令。在这些错误之下,理论上不该成功连接至IoTDA,但是我竟然还成功基于此代码完成了全部业务操作。
我想起来一个名词 KV,这可能是原因所在。在物联网模组中,KV(Key-Value,键值对存储) 是一种轻量级的数据存储机制,用于保存AT指令设置的配置参数(如网络参数、设备密钥等),并在设备重启后保持配置的持久性。其本质是基于非易失性存储器的数据结构,通常通过哈希表或专用存储引擎实现快速读写。
PC-AT 的余威
连接设备平台CDP服务器地址信息,
DTLS加密接入方式开启,5684端口,
设置具体的数据加密模式,==1标准DTLS加密模式,
如上三个配置,都会存KV表。因此,即使在代码中没有这些项的设置操作,通信模组也持有着正确的配置信息。
#<IoT/基于NB28-A/BC28-CNV通信模组使用AT指令连接华为云IoTDA平台>#,在整理上述文章时,我就想干的一件‘坏事’,把PC-AT指令模式下配置的CDP服务器信息、秘钥信息、DTLS信息,逐个关掉,然后再进行MCU-AT指令的实验。
破坏dtls配置
在上述文章中我们提到,DTLS设置也会在进入深休眠或 AT+NRB 重启后保存到 KV,且立即生效。我们先毁掉DTLS配置,执行以下操作,
//切换AT指令拨码开关至PC位置
//在PC串口终端中选择对应的串口号
//切换波特率,从115200(MCU-AT)到9600(PC-AT)
//执行以下DTLS关闭指令/并确认返回OK
AT+QSECSWT=0
上述破坏操作后,切换至PC-AT,并通过RESET按键重启开发板,通过串口终端观察日志。还是原先的"验证成功的代码",此时果然无法再成功连接到平台。日志显示,此时CGATT可以最终查询到成功状态,但是不会有 +QLWEVTIND:0 / +QLWEVTIND:3 这样标记连接平台成功的URC消息。程序在等待一会后,会在此NRB重启,以试图连接到平台,如此往复。
示例代码中,原本使用的是非加密接入方式,并不存在AT+QSECSWT配置,我们仿照CGATT实现该过程,
//add //enable the dtls
static bool_t boudica150_set_dtls(int enable) {bool_t ret ;char cmd[64];(void) memset(cmd,0,64);(void) snprintf(cmd,64,"AT+QSECSWT=%d\r",enable);ret = boudica150_atcmd(cmd,"OK");return ret;
}
//并修改boudica150_boot函数,添加上述设置过程。在boudica150_set_cgatt调用后即可
编译上述修改后的代码,重新烧录,下载程序,重启板卡。运行日志显示,NRB两次后,连接并上报数据成功。
破坏PSK配置
在上述代码基础上,我们进行下一个破坏。示例代码中也是不存在QSETPSK密钥配置过程的。破坏方式类似,使用以下指令,毁掉模组KV中存储的PSK配置。如果KV表中已存在,则允许MCU代码中不执行配置。
//切换PC-AT /切换破特率9600 /执行如下指令后/重新执行上述代码
//以下密码格式错误,将会返回ERROR-4 /此时不会篡改KV表/即使执行了重新上电操作
AT+QSETPSK=0,0
//以下指令可以成功设置一个错误密码(正确的为00112233445566778899AA)/返回OK
AT+QSETPSK=0,00112233445566778899BB
上述操作后,“原本成功的代码”,将无法再连接到平台,现象与QSECSWT实验一致。添加如下过程,
//river.qu //set the psk / rrefer cdp
static bool_t boudica150_set_psk(const char *pskid,const char *psk) {char cmd[64];char resp[64];char cmp[64];bool_t ret = true;if(NULL != pskid) {(void) memset(resp,0,64);(void) memset(cmp,0,64);(void) memset(cmd,0,64);//查询PSK是否已正确设置? /只能是判断格式/考AT手册ret = boudica150_atcmd_response("AT+QSETPSK?\r","OK", resp, 64);(void) snprintf(cmp,64,"+QSETPSK:%s,%s\r", pskid, psk); //which means we need to set itif((false == ret)||(NULL == strstr(resp,cmp))) {(void) memset(cmd,0,64);(void) snprintf(cmd,64,"AT+QSETPSK=%s,%s\r", pskid, psk);ret = boudica150_atcmd(cmd,"OK");}else {ret = true; //we need not to set}}return ret;
}
PSK的格式问题,请参考 #<IoT/基于NB-IoT或Wifi通信模组,使用LwM2M/CoAPs协议,以DTLS加密方式接入华为云物联网IoTDA平台>#。一个合理的猜测是:lwm2m_al 工作在NB-IoT模式下时,psk以纯字符串格式,通过串口,从MCU/PC进入到模组内部,由模组内部的固件程序进行二次解析,将纯字符串数据转换为十六进制数据的字节流,并最终打包成更底层的数据流。
设备标识码和设备ID
使用默认规则创建的NB-IoT设备,其设备标识码和设备ID如下。本章我们将重新深入认识它们。
设备ID默认命名规则
如上所述,设备标识码通常使用IMEI (International Mobile Equipment Identity)国际移动设备识别码、MAC地址、或 Serial No序列号。设备ID默认==product_id_node_id,设备注册界面输入设备标识码的过程中,设备ID会自动跟随填充,可以推测所谓node_id就是设备标识码。当然设备ID也可以自定义输入长度为4-128的字符串,保证唯一性就可以。
node_id 即设备标识码
在北向应用接口文档中,我们查看 AddDevice 创建设备接口,数据体主要结构字段如下表,可以进一步认清 [device_id/设备ID] 和 [node_id/设备标识码] 的含义。
只用node_id不够?
设备标识(node_id)是物理标识,设备的硬件级唯一标识,通常由制造商提供,如IMEI、MAC地址或自定义序列号。用于设备初次接入平台时的身份认证(如MQTT连接鉴权),确保设备物理身份的唯一性。而设备ID(device_id)是逻辑标识,其存在的主要目的是解耦物理设备与业务实体。IMEI/MAC等物理标识与设备硬件强耦合,更换模组即失效,不同厂商设备标识也是形态各异。而有了抽象的逻辑的device_id 后,无论底层硬件如何变化,业务系统只需使用固定device_id操作设备。若设备更换硬件(node_id变化),但device_id保持不变,可避免业务中断(如历史数据关联)。
node_id不可重复使用?
通过上述操作验证,可以确定,同一个IMEI,不能作为设备标识码被使用两次,至少同一个资源空间下是不可以的。
非 IMEI 不可做node_id?
写这篇文章的时候,我早已经完成了智慧农业Wifi的实验,在平台注册Wifi设备时,我并没有使用ESP8266通信模组的MAC地址,而是使用了自定义的 csdn_dahe_0528,且我是最终实验成功的。那么NB设备呢,应该也不是只能使用 IMEI 国际移动设备识别码!
为了验证上述想法,我先后想到两种方案,
第一方案是实践出真知,在上文已经测试过的代码下,去新注册设备(设备标识码和设备ID都发生了改变),并修改代码中的相关宏定义,编译烧录运行测试。第二个方案是基于了对连接过程的源码分析之后,我已经意识到,会使用设备标识码,也即node_id 这个信息的操作步骤,就只有AT+QSETPSK指令过程,因此通过PC-AT指令进行验证就可以了。下文按照方案2展开。
新注册设备,使用非IMEI信息 csdn_NB_IOT_T5_Dahe 作为设备标识码,设置PSK密码为112233445566778899AA。使用PC AT指令(波特率9600、AT开关PC侧),尝试连接到华为云平台,参照 #<NB28-A接入IoTDA>#,关注AT+QSETPSK操作,
如上验证结果,指令中的pskid,要么是0,要么是IMEI号,否则AT+QSETPSK设置指令返回50错误码,标识参数不正确。其实,在 <Quectel_BC28-CNV&BC95-CNV_AT命令手册> QSETPSK指令参数说明中,表达也很明确,基于该通信模组型号、该指令的pskid参数只能是IMEI的15位值,不能是别的,而且要求 LwM2M物联网平台也要使用此值,即注册设备的设备标识码也必须是IMEI。这就限制的死死的啦,对于一个NB-IoT模组,其固件决定了其解析AT+QSETPSK指令的方式,固件程序就只认0或IMEI做参数。
故,我的一个结论是,
Wifi设备的设备标识码可以随意输入唯一值,可不必是MAC地址。但NB-IoT设备,其设备标识码必须是其IMEI号。至少在LwM2M通信协议下是这样的,MQTT协议时如何,我们后续再继续测试。
设备ID/app_server.ep_id
通过后续章节的源码分析可知,注册NB设备时生成的设备ID,虽然在用户层代码中被赋值,但实际上,该字段并没有在连接过程中被使用。为此我专门进行了如下验证试验,修改cn_endpoint_id宏为任意错误的字符串,重新编译和烧录运行程序。在前文的合理代码下,依然可连接到IoTDA平台和成功上报数据。这说明,在NB-IoT通信模组下,cn_endpoint_id/或app_server.ep_id,确实没有起作用。
验证过程可以参考 #<IoT/透过oc_lwm2m源码,分析NB-IoT接入华为云物联网平台IoTDA过程,总结避坑攻略># 中的相关内容。
接入流程分析
我们在应用层向lwm2m传递连接参数,这些参数字段是怎么作用于平台连接过程的呢?
参数结构字段
/** @brief this is the agent configuration */
typedef struct
{en_oc_boot_strap_mode_t boot_mode; ///< bootmode,if boot client_initialize, then the bs must be setoc_server_t boot_server; ///< which will be used by the bootstrap, if not, set NULL hereoc_server_t app_server; ///< if factory or smart boot, must be set herefn_oc_lwm2m_msg_deal rcv_func; ///< receive function caller herevoid *usr_data; ///< used for the user
} oc_config_param_t;
我们这里重点关注 oc_server_t app_server 字段。oc_server_t 结构体定义如下,
typedef struct {char *ep_id; ///< endpoint identifier, which could be recognized by the server /设备ID/非设备标识码char *address; ///< server address,maybe domain name /lwm2m 工作或引导服务器IP地址char *port; ///< server portchar *psk_id; ///< server encode by psk, if not set NULL here /非设备标识码char *psk; //秘钥 /字符串或16进制数字流?int psk_len; //密钥长度 /字符数或sizeof计算?
}oc_server_t;
用户配置操作
在应用层的用户代码中,如,static int app_report_task_entry()函数内,如下调用,
static int app_report_task_entry() {
...ret = oc_lwm2m_config( &oc_param);
...
}
oc_lwm2m_config 函数源码实现中的主要部分是,s_oc_lwm2m_ops.opt->config 回调函数的执行,
int oc_lwm2m_config( oc_config_param_t *param) {...ret = s_oc_lwm2m_ops.opt->config(param);...
}
后续章节,将从 上述 s_oc_lwm2m_ops.opt->config(param) 回调函数调用过程,逐层展开分析。
连接过程注册al层
上文中提到的 ->config(…) 是一个形式为 fn_oc_lwm2m_config 的函数指针,在 oc_lwm2m 下,其有两个注册源。可以参考下图,弱函数oc_lwm2m_imp_init的相关定义与实现,及其宏开关配置如下图所示,
本文重点关注,NB-IoT相关的注册函数,即boudica150_oc_config,
//..\iot_link\oc\oc_lwm2m\boudica150_oc\boudica150_oc.c
static int boudica150_oc_config(oc_config_param_t *param) {...if(boudica150_boot(s_boudica150_oc_cb.plmn,s_boudica150_oc_cb.apn,s_boudica150_oc_cb.bands,\s_boudica150_oc_cb.oc_param.app_server.address,s_boudica150_oc_cb.oc_param.app_server.port))...
}
上述函数以fn_oc_lwm2m_config回调函数格式被赋值到以下全部变量,
const oc_lwm2m_opt_t g_boudica150_oc_opt = \
{.config = boudica150_oc_config,.deconfig = boudica150_oc_deconfig,.report = (fn_oc_lwm2m_report)boudica150_oc_report,
};
上述全局变被注册到lwm2m适配层,oc_lwm2m_imp_init是适配层的弱函数,
int oc_lwm2m_imp_init(void) {...ret = oc_lwm2m_register("boudica150",&g_boudica150_oc_opt);
}
在适配层 oc_lwm2m_al.c 中,弱函数 oc_lwm2m_init 在 boudica150 / NB-IoT 模式下的主要实现,
int oc_lwm2m_init() {//依靠宏开关决定弱函数oc_lwm2m_imp_init的实现版本int ret = oc_lwm2m_imp_init();LINK_LOG_DEBUG("IOT_LINK:DO OC LWM2M LOAD-IMPLEMENT RET:%d\n\r",ret);#ifdef CONFIG_OCLWM2M_DEMO_ENABLE(void) oc_lwm2m_demo_main();
#endifreturn 0;
}
//上述函数会在 int link_main(void *args) -> mian(..) 函数内调用
boudica150 入口函数
函数中的config函数指针,就是oc_lwm2m_init -> oc_lwm2m_register -> g_boudica150_oc_opt 注册过程关联起来的 boudica150_oc_config 函数,而上述函数的关键子函数是 boudica150_boot,只有它在使用 oc_config_param_t oc_param 参数内容。接下来我们就围绕boudica150_boot 函数展开讨论,示例代码中并没有dtls的相关处理,下文是我添加相关处理后的代码,
static bool_t boudica150_boot(const char *plmn, const char *apn, const char *bands,const char *server,const char *port)
{//(void) memset(&s_boudica150_oc_cb,0,sizeof(s_boudica150_oc_cb));at_oobregister("qlwevind",cn_urc_qlwevtind,strlen(cn_urc_qlwevtind),urc_qlwevtind,NULL);at_oobregister("boudica150rcv",cn_boudica150_rcvindex,strlen(cn_boudica150_rcvindex),boudica150_rcvdeal,NULL);//阻塞式尝试连接while(1) {s_boudica150_oc_cb.lwm2m_observe = false;//###参考指令序列分析章节###break;}//reach here means everything is ok, we can go nows_boudica150_oc_cb.sndenable = true;LINK_LOG_DEBUG("NB MODULE RXTX READY NOW\n\r");return true;
}
在boudica150_boot函数所在的 iot_link\oc\oc_lwm2m\boudica150_oc\boudica150_oc.c源文件中,我们可以发现,oc_param.app_server参数下的诸多字段,只有address和port是被使用了的。ep_id、psk、psk_len、cb_name 它们是没有被任何语句调用。我在增加dtls安全接入的相关代码时,会使用到psk、psk_len字段。因此到最后,只有ep_id和cb_name字段是彻底没有使用到的。
指令序列分析
在 boudica150_boot 函数中被循环执行的指令序列如下。包含了前文增加的dtls 和 psk 配置过程。部分指令调用过程,请直接参考 #<IoT/基于NB28-A/BC28-CNV通信模组使用AT指令连接华为云IoTDA平台># 中的表述。
//如下过程的外层是while(1)//Do:执行命令 AT+NRB 重启UE 即重启我们的NB-IoT通信模组 并延时等待10sboudica150_reboot();
//Do:执行ATE0,等待返回OK /禁用模组对接收到的AT指令的回显(Echo)/什么是回显下文另谈boudica150_set_echo (0);
//Do: 设置UE是自动还是手动触发以注册IoT平台 /这就是一个本地设置操作,些模组内存或KV? 应该会立即返回Okboudica150_set_regmode(1);
//Do:使能 +CME ERROR: <err> 结果码 https://blog.csdn.net/quguanxin/article/details/146547709boudica150_set_cmee(1);
//Do:关闭AUTOCONNECT自动连接,本质上是想关闭CFUN射频
//cgatt and cfun must be called if autoconnect is falseboudica150_set_autoconnect(0);
//Do:设置通信模组支持的频带(依据SIM卡运营商选择,或默认选择全部支持的频段)/Band20并不被支持,需要修改掉boudica150_set_bands(bands);
//Do:AT+CFUN=1 使能射频boudica150_set_fun(1);
//Do:AT+COPS=0 设置自动注册网络/源码中实参plmn==NULLboudica150_set_plmn(plmn);
//Do:由于apn==NULL,呼应plmn==NULL;本质上该函数内部什么也没干,直接返回trueboudica150_set_apn(apn);
//Do:参见PC-AT指令,设置CDP服务器boudica150_set_cdp(server,port);
//Do:开启dtls /river.qu 202506boudica150_set_dtls(1);
//Do:设置 PSK ID 和 PSK /river.qu 202506boudica150_set_psk(s_boudica150_oc_cb.oc_param.app_server.psk_id, s_boudica150_oc_cb.oc_param.app_server.psk);
//Do:AT+CGATT=1boudica150_set_cgatt(1);
//Do:AT+NNMI=1 /使能新消息指示和数据,会返回当前所有缓存的消息boudica150_set_nnmi(1);
//Do:检测网络附着状态/重复执行AT+CGATT?查询指令if(false == boudica150_check_netattach(16)) {continue;}
//Do:if(false == boudica150_check_observe(16)) {continue; //we should do the reboot for the nB}
上述代码中的有些指令发送过程,在 #<PC-AT实验># 和 #<IoT/透过oc_lwm2m/boudica150 源码中的AT指令序列,分析NB-IoT接入华为云物联网平台IoTDA的工作机制># 中有详细表述。
NB-IoT 用户层接入参数
作为一只菜鸟,我向着蓝天飞翔了10天,入海遨游了10天,折腾了太多的时间,终于又绕回来了,回到菜鸟的本职工作。
平台设备-PSK格式
在讲述<PC-AT指令>是文章中,我多次提到过PSK格式问题,哈哈,每次都有新发现。时间来到了20250707,受启发于串口数据传输信息A5,可以使用(0xA5)或(0x41+0x35)两种方式,我几乎是瞬间理解了上述矛盾。AT指令手册中说的是16位16进制数据,IoT平台则使用的是 32 “个” 字符。两种表述中 “位”、"个"的含义是不一样的。如A5,它可以是0xA5这 1个 16进制数字,也可以描述为A和5两位16进制数值,如果是在字符串里,则是两个字符。结合BC28的AT手册和实践操作,这里的PSK为最少8个十六进制数字,即最少16位。
WIFI接入代码-PSK格式
本文不对此做详细介绍,重点关注到,在使用lwm2m_tiny时,PSK秘钥使用的是十六进制数字的数组,
#define cn_app_pskid "csdn_dahe_0528"
//#define cn_app_psk "00112233445566778899AA" //注意此时不能使用字符串哦
const unsigned char cn_app_psk[]={0x00,0x11,0x22,0x33,0x44,0x55,0x66,0x77,0x88,0x99,0xAA};
#define cn_app_psklen sizeof(cn_app_psk)
NB-IoT接入代码-PSK格式
#define cn_app_pskid "860059077691603"
#define cn_app_psk "00112233445566778899" //注意还是使用字符串哦
#define cn_app_psklen 20 //实际上AT源码中没有使用此字段
由于NB-IoT模组通过串口,以AT指令的形式与主机MCU进行通信,PSK数据并不会直接用作lwm2m的协议交互过程。AT指令和数据,在串口通信中本身就是纯字符串交互的,MCU写入串口的数据,由模组固件程序进行解析后,再传递给lwm2m的相关接口。
实现DTLS+PSK的AT函数
参见前文 boudica150_set_dtls 和 boudica150_set_psk 函数实现。
真正地MCU-AT接入IoTDA
再来一次破坏操作,清空NB-IoT模组KV表中的正确配置信息,破坏CDP配置、dtls配置、PSK配置,也破坏代码中的设备ID定义、频段定义、PLMN定义、APN定义等。我们重新编译、烧录和执行测试上述完全修正后的NB-IoT智慧农业代码。
//连接平台基本变量
//#define cn_endpoint_id "6850b867d582f620018321e88_860059077691603" //平台中真实的设备ID
#define cn_endpoint_id "csdn_t5_abc_dahe" //随意设定的设备ID
#define cn_app_server "124.70.30.197"
#define cn_app_port "5684" //安全接入方式
#define cn_app_pskid "860059077691603"
#define cn_app_psk "00112233445566778899"
#define cn_app_psklen 20 //实际上没有被使用
//AT模式下/不能直接传递16进制数字的数组哈
//const unsigned char cn_app_psk[]={0x00,0x11,0x22,0x33,0x44,0x55,0x66,0x77,0x88,0x99};
参数初始化相关代码,
static int app_data_report_task(void *usr_data) {oc_config_param_t oc_config;(void) memset(&oc_config,0,sizeof(oc_config));//设置引导模式oc_config.boot_mode = en_oc_boot_strap_mode_factory;oc_config.rcv_func = app_msg_deal;oc_config.usr_data = NULL;oc_config.app_server.address = cn_app_server;oc_config.app_server.port = cn_app_port;oc_config.app_server.ep_id = cn_endpoint_id; #if 1 //CoAPS/DTLS加密oc_config.app_server.psk = (char *)cn_app_psk;oc_config.app_server.psk_len = cn_app_psklen;oc_config.app_server.psk_id = cn_app_pskid;#endifret = oc_lwm2m_config(&oc_config);...
}
接入日志的末尾部分截取,
平台设备在线状态,