物联网实战--入门篇之(七)嵌入式-MQTT
目录
一、MQTT简介
二、MQTT使用方法
三、MQTT驱动设计
四、代码解析
五、使用过程
六、总结
一、MQTT简介
MQTT因为其轻量、高效和稳定的特点,特别适合作为物联网系统的数据传输协议,已经成为物联网事实上的通信标准了。关于协议的具体内容看看这篇文章和官方文档MQTT协议详解(完整版)-CSDN博客,在这里我们主要讲解使用方法。
作为嵌入式设备,设备资源比较紧张,我们这里选用开源库paho mqtt,开源地址在这儿GitHub - eclipse/paho.mqtt.embedded-c: Paho MQTT C client library for embedded systems. Paho is an Eclipse IoT project (https://iot.eclipse.org/)
我们项目里已经都整理好了,直接用就行了,具体如下图所示,从映射文件可以看出,mqtt开源库大概占用2KB的 ROM,已经很轻量化了。这个开源库的核心作用就是可以帮我们根据协议要求组合要发送的数据,或者拆解接收到的数据,而应用层不用去太关心协议本身的内容。
二、MQTT使用方法
MQTT是以服务器为中心,客户端对为对象,话题为关系纽带的一种通讯协议,在这个体系里,净化器设备是客户端,用户手机也是客户端,手机订阅净化器发布的话题,服务器就会把净化器发布的消息推送给手机;同样的道理,手机根据设备订阅的话题来发布消息,就可以对净化器设备进行控制了。
下图是净化器项目的话题,其中11223344是设备的序列号,对于所有净化器的数据手机都能收的到,手机针对某个净化器的数据也只有某个净化器能接收,其它序列号的设备收不到。这里面的核心逻辑都是服务器根据话题来区分运行的。
三、MQTT驱动设计
MQTT的驱动应该算是比较难的,首先要确定它的地位和作用,如下图所示,drv_mqtt是作为设备端mqtt的核心,整合了底层的开源库、物理层的收发接口和应用层的参数配置功能,以及自身的连接、收发、订阅/取消订阅等功能。
下面进入代码进行解析,从头文件开始,MQTTPacket.h主要包含了mqtt开源库的功能文件,这个应该没什么问题,下面的ringbuffer.h需要强调下,它是RT-Thread的功能,叫环形缓冲区,就是数据按顺序环形保存,取出的时候按照先进先出的原则,MQTT开源库需要按顺序取出数据解析,有这个ringbuffer作为缓存媒介在操作上非常便捷,这也是使用RT-Thread的另一个重要原因了。
接下来是宏定义的内容,没什么特殊情况默认即可,有需要改变的在user_opt.h中重定义即可,具体的内容都有注释,就不赘述了。
订阅话题是个重要组成部分,在这里定义了话题的三个状态,空闲、订阅和取消订阅,取消订阅一般用不到,特殊情况下会有一些临时话题,为了缓解资源,可以取消订阅。结构体里的base_msg_id主要是为了标记 订阅/取消订阅 时返回的话题,这样程序才能区分。
最后是最重要的客户端连接信息了,具体都有注释,其中用户名、密码和客户端ID都是指针,在应用层定义这些信息需要用全局变量或者静态变量,才能保证信息的完整性;同样的,收发函数也是采用回调的方式,在应用程根据不同的物理接口进行注册,这里我们采用的自然是esp8266的收发函数了。
四、代码解析
先从初始化开始,主要就是对用户名、密码和客户端ID进行赋值。
/*
================================================================================
描述 : 初始化指定MQTT连接
输入 :
输出 :
================================================================================
*/
void drv_mqtt_init(u8 index, char *usr_name, char *passwd, char *client_id)
{if(index<MQTT_CONN_NUM){MqttClientStruct *pClient=&g_sMqttWork.client_list[index];MQTTPacket_connectData connect_init = MQTTPacket_connectData_initializer;if((pClient->rb=rt_ringbuffer_create(MQTT_RING_BUFF_SIZE))!=NULL ) {memcpy(&pClient->condata, &connect_init, sizeof(connect_init));//复制连接初始化信息pClient->condata.keepAliveInterval=MQTT_KEEP_TIME; pClient->condata.username.cstring=usr_name;//用户名pClient->condata.password.cstring=passwd;//密码pClient->condata.clientID.cstring=client_id;//客户ID pClient->is_enable=true;} }
}
接下来就是连接和订阅了,在这里就可以很清晰的看到mqtt开源库的作用了,就是组合连接、订阅和取消订阅的报文。MQTT里也有保活功能,这是协议层的,如果指定时间内没有没有收到数据,那么会自己发个ping请求包来保持连接。
/*
================================================================================
描述 : 连接和订阅
输入 :
输出 :
================================================================================
*/
void drv_mqtt_connect(void)
{static u32 last_sec_time=0;static u8 make_buff[80]={0};const int make_size=sizeof(make_buff);int make_len; u32 now_sec_time=drv_get_sec_counter();if(now_sec_time-last_sec_time>=2){static u8 conn_ptr=0;if(conn_ptr>=MQTT_CONN_NUM)conn_ptr=0;MqttClientStruct *pClient=&g_sMqttWork.client_list[conn_ptr];if(pClient->is_enable){if(pClient->is_connected==false){memset(make_buff, 0, make_size);make_len=MQTTSerialize_connect(make_buff, make_size, &pClient->condata);//组合连接请求包 if(pClient->mqtt_send!=NULL){
// printf("client=%d, mqtt send connect! make_len=%d\n",conn_ptr, make_len); pClient->mqtt_send(make_buff, make_len);//发送} }else{//订阅话题for(u8 i=0; i<MQTT_SUB_NUM; i++){SubPackStruct *pSub=&pClient->sub_list[i];if(strlen(pSub->sub_topic)>0 && pSub->curr_state!=pSub->dst_state){if(pSub->dst_state==TopicStateSub)//需要订阅{MQTTString topicString = MQTTString_initializer;int req_qos=1; topicString.cstring=pSub->sub_topic;memset(make_buff, 0, make_size);make_len = MQTTSerialize_subscribe(make_buff, make_size, 0, pSub->base_msg_id, 1, &topicString, &req_qos);//组合订阅报文if(pClient->mqtt_send!=NULL){printf("sub topic=%s\n", pSub->sub_topic);pClient->mqtt_send(make_buff, make_len);//发送} }else if(pSub->dst_state==TopicStateUnSub)//需要取消订阅{MQTTString topicString = MQTTString_initializer; topicString.cstring=pSub->sub_topic;memset(make_buff, 0, make_size);make_len = MQTTSerialize_unsubscribe(make_buff, make_size, 0, pSub->base_msg_id, 1, &topicString);//组合取消订阅报文if(pClient->mqtt_send!=NULL){printf("unsub topic=%s\n", pSub->sub_topic);pClient->mqtt_send(make_buff, make_len);//发送} }break;//每次只订阅一个,避免堵塞}}//超时检测u32 det_time=now_sec_time-pClient->keep_time;if(det_time>=MQTT_KEEP_TIME){printf("mqtt sock_id=%d timeout, close!\n", conn_ptr);drv_mqtt_close(pClient);//超时关闭 }else if(det_time>=MQTT_KEEP_TIME-10){//发送ping请求,保活memset(make_buff, 0, make_size);make_len=MQTTSerialize_pingreq(make_buff, make_size);//组合ping包 if(pClient->mqtt_send!=NULL){
// printf("sock=%d, mqtt send ping req! make_len=%d\n",conn_ptr,make_len); pClient->mqtt_send(make_buff, make_len);//发送} } }}conn_ptr++;last_sec_time=drv_get_sec_counter();}
}
接收部分的逻辑是MQTTPacket_read函数调用回调函数pClient->mqtt_recv获取环形缓冲区内的数据并按照协议解析,最后根据解析结果执行相应动作,消息类型如下图所示,常用的是连接回复、收到发布数据、订阅回复、取消订阅回复、ping回复和断开连接。
/*
================================================================================
描述 : 接收检查
输入 :
输出 :
================================================================================
*/
void drv_mqtt_recv_check(void)
{static u8 make_buff[MQTT_SUB_BUFF_SIZE];const int make_size=sizeof(make_buff);int rc;u8 dup;int qos;u8 retained;u16 msgid;int payloadlen_in;u8 *payload_in; MQTTString receivedTopic; for(u8 i=0; i<MQTT_CONN_NUM; i++){MqttClientStruct *pClient=&g_sMqttWork.client_list[i];if(pClient->is_enable==true)//启用{rc=MQTTPacket_read(make_buff, make_size, pClient->mqtt_recv);switch(rc){case CONNACK://连接回复{printf("mqtt_id=%d CONNACK!\n", i);u8 sessionPresent, connack_rc;if (MQTTDeserialize_connack(&sessionPresent, &connack_rc, make_buff, make_size) != 1 || connack_rc != 0)//解析收到的回复报文{drv_mqtt_close(pClient);printf("mqtt sock_id=%d Unable to connect, return code %d\n",i, connack_rc); }else{pClient->is_connected=true;pClient->keep_time=drv_get_sec_counter();//更新时间printf("mqtt sock_id=%d connect ok!\n", i);} break;} case PUBREC:case PUBACK: //发布回复{
// debug("sock_id=%d PUBACK!\n", i);break;} case PUBLISH://收到发布的消息{pClient->keep_time=drv_get_sec_counter();//更新时间printf("sock_id=%d PUBLISH!\n", i);rc = MQTTDeserialize_publish(&dup, &qos, &retained, &msgid, &receivedTopic, &payload_in, &payloadlen_in, make_buff, make_size); char *pTopic=receivedTopic.lenstring.data;if(g_sMqttWork.mqtt_recv_parse!=NULL){char topic[30]={0};int len=(char*)payload_in-pTopic;//topic 长度if(len>sizeof(topic)){len=sizeof(topic)-1;}memcpy(topic, pTopic, len);g_sMqttWork.mqtt_recv_parse(i, topic, payload_in, payloadlen_in);//应用层数据解析}break;} case SUBACK://订阅回复{
// debug("sock_id=%d SUBACK!\n", i);
// printf_hex("sub buff=", make_buff, 30);int count, requestedQoSs[1];MQTTDeserialize_suback(&msgid, 1, &count, requestedQoSs, make_buff, make_size);
// debug("$$$ msgid=0x%04X\n", msgid);for(u8 k=0; k<MQTT_SUB_NUM; k++){SubPackStruct *pSub=&pClient->sub_list[k];if(pSub->base_msg_id==msgid){printf("topic=%s sub ok!\n", pSub->sub_topic);pSub->curr_state=TopicStateSub;
// pSub->subed_time=drv_get_sec_counter();}}break;} case UNSUBACK://取消订阅回复{
// debug("sock_id=%d UNSUBACK!\n", i);MQTTDeserialize_unsuback(&msgid, make_buff, make_size);
// debug("$$$ msgid=0x%04X\n", msgid); for(u8 k=0; k<MQTT_SUB_NUM; k++){SubPackStruct *pSub=&pClient->sub_list[k];if(pSub->base_msg_id==msgid){printf("topic=%s unsub ok!\n", pSub->sub_topic);pSub->curr_state=TopicStateUnSub;
// pSub->subed_time=drv_get_sec_counter();}} break;}case PINGRESP://ping回复{pClient->keep_time=drv_get_sec_counter();//更新时间
// debug("sock_id=%d PINGRESP!\n", i);break;} case DISCONNECT://断开连接{printf("mqtt_id=%d DISCONNECT!\n", i);drv_mqtt_close(pClient); break;} }} }
}
剩下的就是一些简单的功能了,比如设置话题、发布消息,关闭连接等等,较为简单。
/*
================================================================================
描述 : 设置话题信息
输入 :
输出 :
================================================================================
*/
void drv_mqtt_set_topic_info(u8 client_id, u8 sub_id, char *topic, u32 base_msg_id, u8 dst_state)
{if(client_id<MQTT_CONN_NUM) { MqttClientStruct *pClient=&g_sMqttWork.client_list[client_id];if(sub_id<MQTT_SUB_NUM){SubPackStruct *pSub=&pClient->sub_list[sub_id];if(strlen(topic)<sizeof(pSub->sub_topic)){pSub->curr_state=TopicStateIdel; pSub->dst_state=dst_state;pSub->base_msg_id=base_msg_id;strcpy(pSub->sub_topic, topic); }}}
}/*
================================================================================
描述 : 设置话题订阅状态
输入 :
输出 :
================================================================================
*/
void drv_mqtt_set_topic_state(u8 client_id, u8 sub_id, u8 dst_state)
{if(client_id<MQTT_CONN_NUM) { MqttClientStruct *pClient=&g_sMqttWork.client_list[client_id];if(sub_id<MQTT_SUB_NUM){SubPackStruct *pSub=&pClient->sub_list[sub_id];pSub->dst_state=dst_state;}}
}
/*
================================================================================
描述 : MQTT发布数据
输入 :
输出 :
================================================================================
*/
void drv_mqtt_publish(u8 index, u8 *msg_buff, u16 msg_len, char *topic)
{static u8 make_buff[MQTT_PUB_BUFF_SIZE]={0};static const int make_size=sizeof(make_buff); u16 make_len=0; if(index<MQTT_CONN_NUM){MqttClientStruct *pClient=&g_sMqttWork.client_list[index];if(pClient->is_connected==true)//已经连接{ pClient->msg_id++;MQTTString topicString = MQTTString_initializer;topicString.cstring=topic; make_len = MQTTSerialize_publish(make_buff, make_size, 0, 1,0, pClient->msg_id, topicString, msg_buff, msg_len);//组合发布报文if(pClient->mqtt_send!=NULL && make_len>0){int ret=pClient->mqtt_send(make_buff, make_len);//发送} } }
}
/*
================================================================================
描述 : 关闭连接
输入 :
输出 :
================================================================================
*/
void drv_mqtt_close(MqttClientStruct *pClient)
{pClient->is_connected=false;for(u8 i=0; i<MQTT_SUB_NUM; i++){SubPackStruct *pSub=&pClient->sub_list[i];pSub->curr_state=TopicStateIdel;
// pSub->subed_time=0;}pClient->msg_id=0;pClient->keep_time=0;
}
五、使用过程
应用层的使用主要就是根据要求配置信息,首先物理通讯接口先设置,这里使用esp8266的连接3作为网络链路,同时注册接收函数把数据缓存进ringbuffer;然后就是MQTT用户名、密码、客户端ID的设置了;接下来有三个回调函数注册,两个是物理层的MQTT收发,还有一个是应用层的数据解析,这里已经来到了最后的净化器项目本身了,由此可以看出,要想代码好维护,写代码之前就要分层设计,这样出问题了才好分级排查,再后期自己阅读时逻辑也更走得通;最后一步就是话题订阅了,这样才能收到用户的控制数据,每个设备订阅话题都不一样,最后都带上了自己序列号,这样用户端才能针对性控制设备。
下面代码是净化器应用层的数据解析。
/*
================================================================================
描述 : 设备解析服务器下发的数据
输入 :
输出 :
================================================================================
*/
void app_air_recv_parse(u8 *buff, u16 len)
{u8 head[2]={0xAA, 0x55};u8 *pData=memstr(buff, len, head, 2);if(pData!=NULL){u16 total_len=pData[2]<<8 | pData[3];u16 crcValue=pData[total_len]<<8 | pData[total_len+1];if(crcValue==drv_crc16(pData, total_len)){pData+=4;u32 device_sn=pData[0]<<24|pData[1]<<16|pData[2]<<8|pData[3];pData+=4;if(device_sn!=g_sAirWork.device_sn)//识别码确认return;u8 cmd_type=pData[0];pData++;switch(cmd_type){case AIR_CMD_HEART://心跳包{break;}case AIR_CMD_DATA://数据包{break;}case AIR_CMD_SET_SPEED://设置风速{u8 speed=pData[0];pData+=1;app_motor_set_speed(speed);break;} case AIR_CMD_SET_SWITCH://设置开关{u8 state=pData[0];pData+=1;g_sAirWork.switch_state=state;if(state>0){app_motor_set_speed(100);//启动风扇}else{app_motor_set_speed(0);//停止风扇}app_air_send_status();break;}}}}
}
六、总结
MQTT协议本身较为繁琐,现在应用阶段暂时不用太深入,先学会使用就行,用熟了再去查阅文档,这样理解起来更透彻。mqtt的驱动设计相较于其他驱动文件更为复杂,因为它所牵涉的内容更广,有开源库、网络链路、应用层参数配置等等,完整的工程在第二篇文章里有的下载,自行查阅。
本项目的交流QQ群:701889554
写于2024-4-1
相关文章:

物联网实战--入门篇之(七)嵌入式-MQTT
目录 一、MQTT简介 二、MQTT使用方法 三、MQTT驱动设计 四、代码解析 五、使用过程 六、总结 一、MQTT简介 MQTT因为其轻量、高效和稳定的特点,特别适合作为物联网系统的数据传输协议,已经成为物联网事实上的通信标准了。关于协议的具体内容看看这…...
跑模型——labelme的json文件转成yolo使用的txt文件(语义分割)
前言 将labelme多边形标注的json文件转换成yolo使用的txt文件 import os import json import numpy as np from tqdm import tqdm#实现函数 def json2txt(path_json, path_txt): # 可修改生成格式with open(path_json, r) as path_json:jsonx json.load(path_json)with open…...

一个项目仿京东商场代码
git clone http://git.itcast.cn/heimaqianduan/erabbit-uni-app-vue3-ts.git...

计算机网络——WEB服务器编程实验
实验目的 1. 处理一个 http 请求 2. 接收并解析 http 请求 3. 从服务器文件系统中获得被请求的文件 4. 创建一个包括被请求的文件的 http 响应信息 5. 直接发送该信息到客户端 具体内容 一、C 程序来实现 web 服务器功能。 二、用 HTML 语言编写两个 HTML文件,并…...
蓝桥杯算法题:最大比例
题目描述: X星球的某个大奖赛设了 M 级奖励。 每个级别的奖金是一个正整数。 并且,相邻的两个级别间的比例是个固定值。 也就是说:所有级别的奖金数构成了一个等比数列。 比如:16,24,36,54,其等比值为:3/2。…...

【堡垒机】堡垒机的介绍
目前,常用的堡垒机有收费和开源两类。 收费的有行云管家、纽盾堡垒机; 开源的有jumpserver; 这几种各有各的优缺点,如何选择,大家可以根据实际场景来判断 什么是堡垒机 堡垒机,即在一个特定的网络环境下&…...
通过 ffmpeg命令行 调节视频播放速度
1. 仅调整视频速率 视频调速原理:修改视频的pts,dts # 可能会丢帧 ffmpeg -i input.mkv -an -filter:v "setpts0.5*PTS" output.mkv # 可用-r参数指定输出视频FPS以防止丢帧 ffmpeg -i input.mkv -an -r 60 -filter:v "setpts2.0*PTS&q…...

SQLite数据库在Linux系统上的使用
SQLite是一个轻量级的数据库解决方案,它是一个嵌入式的数据库管理系统。SQLite的特点是无需独立的服务器进程,可以直接嵌入到使用它的应用程序中。由于其配置简单、支持跨平台、服务器零管理,以及不需要复杂的设置和操作,SQLite非…...
Spring中依赖注入的方法有几种,分别是什么?
依赖注入的目的: 都是为了减少对象之间的紧密耦合 1. 构造函数注入:通过在类的构造函数中接受依赖对象作为参数,Spring在创建对象时将依赖注入。 2. Setter方法注入:在类中提供setter方法,Spring通过调用这些setter方法…...

【面试精讲】MyBatis设计模式及源码分析,MyBatis设计模式实现原理
【面试精讲】MyBatis设计模式及源码分析,MyBatis设计模式实现原理 目录 本文导读 一、MyBatis中运用的设计模式详解 1. 工厂模式(Factory Pattern) 2. 单例模式(Singleton Pattern) 3. 建造者模式(Bu…...

Acrel-1000DP光伏监控系统在尚雷仕(湖北)健康科技有限公司5.98MW分布式光伏10KV并网系统的应用
摘 要:分布式光伏发电特指在用户场地附近建设,运行方式多为自发自用,余电上网,部分项目采用全额上网模式。分布式光伏全额上网的优点是可以充分利用分布式光伏发电系统的发电量,提高分布式光伏发电系统的利用率。发展分…...

电脑远程控制esp32上的LED
1、思路整理 首先esp32需要连接上wifi 然后创建udp socket 接受udp数据 最后解析数据,控制LED 2、micropython代码实现 import network from socket import * from machine import Pin p2Pin(2,Pin.OUT)def do_connect(): #连接wifi wlan network.WLAN(network.…...
ARXML处理 - C#的解析代码(一)
目的 本文介绍通过AUTOSAR组织提供的xsd文件,自动生成对应的C#解析代码的框架。 自动生成方法:Microsoft SDKs\Windows\v7.0A\bin\xsd.exe 命令:xsd.exe AUTOSAR_4-0-3.xsd /c /l:CS /n:AUTOSAR4 AUTOSAR_4-0-3.xsd 是需要生成代码的xsd文…...

OJ 栓奶牛【C】【Python】【二分算法】
题目 算法思路 要求的距离在最近木桩与最远木桩相隔距离到零之间,所以是二分法 先取一个中间值,看按照这个中间值可以栓多少奶牛,再与输入奶牛数比较,如果大于等于,则增大距离,注意这里等于也是增大距离…...

Spring6-单元测试:JUnit
1. 概念 在进行单元测试时,特别是针对使用了Spring框架的应用程序,我们通常需要与Spring容器交互以获取被测试对象及其依赖。传统做法是在每个测试方法中手动创建Spring容器并从中获取所需的Bean。以下面的两行常见代码为例: ApplicationCo…...

ubuntu系统安装k8s1.28精简步骤
目录 一、规划二、环境准备2.1 配置apt仓库配置系统基本软件仓库配置k8s软件仓库安装常用软件包 2.2 修改静态ip、ntp时间同步、主机名、hosts文件、主机免密2.3 内核配置2.4 关闭防火墙、selinux、swap2.5 安装软件安装docker安装containerd安装k8s软件包 三、安装配置k8s3.1 …...
探讨Java和Go语言的缺点
文章目录 Java的缺点Go语言的缺点 通常我们都会讨论Java和GO的优点,如果讨论缺点往往能让人们更清楚优点的重要性,Java和Go的缺点或许往往就是对方优点所在 Java的缺点 冗长的代码:相较于一些现代编程语言,Java 的语法相对冗长&am…...

短剧在线搜索PHP网站源码
源码简介 短剧在线搜索PHP网站源码,自带本地数据库500数据,共有6000短剧视频,与短剧猫一样。 搭建环境 PHP 7.3 Mysql 5.6 安装教程 1.上传源码到网站目录中 2.修改【admin.php】中, $username ‘后台登录账号’; $passwor…...
Python map遍历
在Python中,map 函数是一个内置函数,它将指定的函数应用于给定序列(如列表、元组等)的每个项,并返回一个迭代器,该迭代器包含所有项经过指定函数处理后的结果。 ### map 函数的基本用法 map 函数的语法如…...

数据结构—红黑树
红黑树介绍 红黑树(Red Black Tree)是一种自平衡二叉查找树。由于其自平衡的特性,保证了最坏情形下在 O(logn) 时间复杂度内完成查找、增加、删除等操作,性能表现稳定。 在 JDK 中,TreeMap、TreeSet 以及 JDK1.8 的 …...

地震勘探——干扰波识别、井中地震时距曲线特点
目录 干扰波识别反射波地震勘探的干扰波 井中地震时距曲线特点 干扰波识别 有效波:可以用来解决所提出的地质任务的波;干扰波:所有妨碍辨认、追踪有效波的其他波。 地震勘探中,有效波和干扰波是相对的。例如,在反射波…...

Redis相关知识总结(缓存雪崩,缓存穿透,缓存击穿,Redis实现分布式锁,如何保持数据库和缓存一致)
文章目录 1.什么是Redis?2.为什么要使用redis作为mysql的缓存?3.什么是缓存雪崩、缓存穿透、缓存击穿?3.1缓存雪崩3.1.1 大量缓存同时过期3.1.2 Redis宕机 3.2 缓存击穿3.3 缓存穿透3.4 总结 4. 数据库和缓存如何保持一致性5. Redis实现分布式…...
Spring AI与Spring Modulith核心技术解析
Spring AI核心架构解析 Spring AI(https://spring.io/projects/spring-ai)作为Spring生态中的AI集成框架,其核心设计理念是通过模块化架构降低AI应用的开发复杂度。与Python生态中的LangChain/LlamaIndex等工具类似,但特别为多语…...
C++.OpenGL (14/64)多光源(Multiple Lights)
多光源(Multiple Lights) 多光源渲染技术概览 #mermaid-svg-3L5e5gGn76TNh7Lq {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-3L5e5gGn76TNh7Lq .error-icon{fill:#552222;}#mermaid-svg-3L5e5gGn76TNh7Lq .erro…...
NPOI Excel用OLE对象的形式插入文件附件以及插入图片
static void Main(string[] args) {XlsWithObjData();Console.WriteLine("输出完成"); }static void XlsWithObjData() {// 创建工作簿和单元格,只有HSSFWorkbook,XSSFWorkbook不可以HSSFWorkbook workbook new HSSFWorkbook();HSSFSheet sheet (HSSFSheet)workboo…...
Kubernetes 网络模型深度解析:Pod IP 与 Service 的负载均衡机制,Service到底是什么?
Pod IP 的本质与特性 Pod IP 的定位 纯端点地址:Pod IP 是分配给 Pod 网络命名空间的真实 IP 地址(如 10.244.1.2)无特殊名称:在 Kubernetes 中,它通常被称为 “Pod IP” 或 “容器 IP”生命周期:与 Pod …...

stm32wle5 lpuart DMA数据不接收
配置波特率9600时,需要使用外部低速晶振...

阿里云Ubuntu 22.04 64位搭建Flask流程(亲测)
cd /home 进入home盘 安装虚拟环境: 1、安装virtualenv pip install virtualenv 2.创建新的虚拟环境: virtualenv myenv 3、激活虚拟环境(激活环境可以在当前环境下安装包) source myenv/bin/activate 此时,终端…...

Neko虚拟浏览器远程协作方案:Docker+内网穿透技术部署实践
前言:本文将向开发者介绍一款创新性协作工具——Neko虚拟浏览器。在数字化协作场景中,跨地域的团队常需面对实时共享屏幕、协同编辑文档等需求。通过本指南,你将掌握在Ubuntu系统中使用容器化技术部署该工具的具体方案,并结合内网…...

倒装芯片凸点成型工艺
UBM(Under Bump Metallization)与Bump(焊球)形成工艺流程。我们可以将整张流程图分为三大阶段来理解: 🔧 一、UBM(Under Bump Metallization)工艺流程(黄色区域ÿ…...