当前位置: 首页 > news >正文

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);...
}

接入日志的末尾部分截取,
在这里插入图片描述
平台设备在线状态,
在这里插入图片描述

http://www.dtcms.com/a/325713.html

相关文章:

  • DeepCompare文件深度对比软件:权限管理与安全功能全面解析
  • Day12 Maven高级
  • openpnp - 顶部相机环形灯光DIY
  • 基于AI量化模型的比特币周期重构:传统四年规律是否被算法因子打破?
  • Apple Intelligence
  • 代币化股票的崛起:比特币安全吗?
  • Linux操作系统从入门到实战(十九)进程状态
  • SpringBoot 实现 Excel 导入导出功能的三种实现方式
  • SpringBoot 自动配置核心机制(面试高频考点)
  • 随身WiFi技术军备赛白热化:WiFi6架构下放中端市场,格行中兴华为三足鼎立;从芯片到场景的 10 款标杆产品深度解析
  • 使用Windbg分析多线程死锁项目实战问题分享
  • FPGA学习笔记——DS18B20(数字温度传感器)
  • 智慧工地:以三大监测技术筑牢安全屏障
  • 衡石科技HENGSHI SENSE 6.0 亮点功能一览-新增仪表盘入口和可视化
  • 【软件安装那些事 6】SOLIDWORKS 2021 详细安装教程(中文简体版)步骤完整不跳步 { 附软件提取下载链接,永久有效---------百度网盘 }
  • Python进阶(6):模块Modules
  • 游戏美术总监级工作流:Firefly AI赋能概念设计,从2D到3D重塑开发管线!
  • CVPR 2025 | 视觉感知新突破丨PF3Det、SemiDAViL与3D物体功能定位的创新点合集
  • MacroDroid 安卓版:功能强大的安卓自动化应用
  • Blender 数据集格式介绍
  • Mybatis学习之逆向工程(十)
  • 华为虚拟防火墙配置案例详解
  • 【软考中级网络工程师】知识点之 UDP 协议:网络通信中的高效轻骑兵
  • Open-Source Agentic Hybrid RAG Framework for Scientific Literature Review
  • Spark 优化全攻略:从 “卡成 PPT“ 到 “飞一般体验“
  • Hadoop和Spark的区别
  • vscode新建esp32工程,没有sample_project怎么办?
  • Mysql——Sql的执行过程
  • Windows Git Bash 常用配置
  • 设计模式笔记_结构型_门面模式