物联网实战--入门篇之(七)嵌入式-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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/508589.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

苹果应用上架成功之道

苹果上架要求是苹果公司对于提交应用程序到苹果商店上架的要求和规定。这些要求主要是为了保证用户体验、应用程序的质量和安全性。以下是苹果上架要求的详细介绍&#xff1a;1. 应用程序的内容和功能必须符合苹果公司的规 苹果上架要求是苹果公司对于提交应用程序到苹果商店上…

CListCtrl中高亮插入行,条目上移下移

1. 样式修改 2. 关键代码 BOOL CMFCApplication3Dlg::OnInitDialog() {CDialogEx::OnInitDialog();// // 代码省略......//// 设置此对话框的图标。 当应用程序主窗口不是对话框时&#xff0c;框架将自动// 执行此操作SetIcon(m_hIcon, TRUE); // 设置大图标SetIcon(m_hIc…

ubuntu软件推荐(2):终端美化 tabby+oh-my-zsh

文章目录 一、tabby下载二、tabby安装三、安装Oh-my-zsh1. 安装zsh2. 设置默认终端为zsh3. 下载安装oh-my-zsh3. 根据自己喜好安装字体&#xff08;略&#xff09;4. 在tabby终端修改字体5. 安装oh-my-zsh插件6. 修改主题插件生效 一、tabby下载 https://github.com/Eugeny/ta…

预处理指令——一些比较少见的概念

前言&#xff1a;预处理是我们的c语言源代码成为可执行程序的第一个步骤。而宏和预处理指令都是在这个阶段完成。本节内容就是关于宏和预处理指令相关知识点的解析。 目录 宏 预定义符号 #define定义常量 #define定义符号 #define定义宏 带副作用的宏参数 宏的替换规则…

2024最新GPT4.0使用教程:GPTs,AI绘画,AI换脸,AI绘画,文档分析一站式解决

一、前言 ChatGPT3.5、GPT4.0、相信对大家应该不感到陌生吧&#xff1f;简单来说&#xff0c;GPT-4技术比之前的GPT-3.5相对来说更加智能&#xff0c;会根据用户的要求生成多种内容甚至也可以和用户进行创作交流。 然而&#xff0c;GPT-4对普通用户来说都是需要额外付费才可以…

分享10个免费高可用的GPT3.5和4.0网站并做功能测试【第一个】

1.介绍 网址&#xff1a;直接点&#xff1a;aicnn 或者 www.aicnn.cn 基于ChatGPT可以实现智能聊天、绘画生成、高清文本转语音、论文润色等多种功能&#xff0c;基于sd和mj实现的绘画功能&#xff0c;下面是功能测试&#xff1a; 博主从 1.GPT3.5是否完全免费/是否限制频率、…

Mysql的基本命令

1 服务相关命令 命令描述systemctl status mysql查看MySQL服务的状态systemctl stop mysql停止MySQL服务systemctl start mysql启动MySQL服务systemctl restart mysql重启MySQL服务ps -ef | grep mysql查看mysql的进程mysql -uroot -hlocalhost -p123456登录MySQLhelp显示MySQ…

【PRO3.0 】电子面单模版请求失败问题处理

注意&#xff1a;&#xff1a;改完重启守护进程 1、文件地址&#xff1a;crmeb/services/express/storage/Express.php 行数 202 行左右&#xff0c; 方法名&#xff1a;temp() 如下如图把 POST 改成 GET 2、crmeb/services/HttpService.php 行数&#xff1a;81 行左右 方…

多线程带来的的风险-线程安全、锁的问题

线程安全的概念 想给出一个线程安全的确切定义是复杂的&#xff0c;但我们可以这样认为&#xff1a; 如果多线程环境下代码运行的结果是符合我们预期的&#xff0c;即在单线程环境应该的结果&#xff0c;则说这个程序是线程安全的。 观察线程不安全 class Counter {public…

C++初阶 | [十一] priority_queue 优先级队列

摘要&#xff1a;priority_queue 优先级队列的使用和模拟实现&#xff0c;仿函数 前言——优先级队列介绍&#xff1a; 优先队列是一种容器适配器&#xff0c;根据严格的弱排序标准&#xff0c;它的第一个元素总是它所包含的元素中最大的。此上下文类似于堆&#xff0c;在堆中…

OpenHarmony实战:用IPOP调试 OpenHarmony 内核

前言 我使用的是 IPOP V4.1&#xff0c;基于 OpenHarmony 开源系统和 RK3568 开发板&#xff0c;在 PC 上运行此软件&#xff0c;查看运行、错误日志来调试内核。作为网络、嵌入式式内核调试的必备工具&#xff0c;建议同学珍藏。IPOP 运行在 PC 上&#xff0c;操作系统是 Win…

真快乐APP抢购源码实现

支持多个平台的自动 滑动验证码、选字验证码。缺点就是需要自己找一个验证码识别服务器,可以自己用python写一个,或者使用超级鹰(本篇教程就是使用它) 下面是实现源码 "ui"; Date.prototype.Format = function (fmt) {var o = {"M+": this.getMonth() …

学习transformer模型-broadcast广播的简明介绍

broadcast的定义和目的&#xff1a; 广播发生在将较小的张量“拉伸”以具有与较大张量兼容的形状&#xff0c;以便执行操作时。 广播是一种有效执行张量操作而不创建重复数据的方式。 广播的处理过程&#xff1a; 1&#xff0c; 确定最右边的维度是否兼容 每…

前端性能优化-Table渲染速度优化

教务系统-排课页面性能优化总结 一、前言 在公司教务系统中,排课页面慢的令人发指,在某些情况由于数据量大导致页面主进程卡死,遂组织进行一次排查优化,现记录一下 二、效果对比 以下数据均为UAT环境 Performence对比 更改前: 主进程渲染时间为 8s 教务系统-排课页面性…

Intel FPGA (6):dac tlv5618a

Intel FPGA (6)&#xff1a;dac tlv5618a 前提摘要 个人说明&#xff1a; 限于时间紧迫以及作者水平有限&#xff0c;本文错误、疏漏之处恐不在少数&#xff0c;恳请读者批评指正。意见请留言或者发送邮件至&#xff1a;“Email:noahpanzzzgmail.com”。本博客的工程文件均存放…

探索DeFi元宇宙:NFT、Web3和DAPP的数藏Swap合约应用开发

随着区块链技术的发展和普及&#xff0c;DeFi&#xff08;去中心化金融&#xff09;和NFT&#xff08;非同质化代币&#xff09;等概念在数字经济中扮演着越来越重要的角色。而元宇宙、Web3、DAPP等概念也逐渐成为人们关注的焦点。在这个背景下&#xff0c;将这些概念融合在一起…

Flutter仿Boss-2.启动页、引导页

简述 在移动应用开发中&#xff0c;启动页和引导页是用户初次接触应用时的重要组成部分&#xff0c;能够提升用户体验和导航用户了解应用功能。本文将介绍如何使用Flutter实现启动页和引导页&#xff0c;并展示相关代码实现。 启动页 启动页是应用的第一个页面&#xff0c;首…

Excel 粘贴回筛选后的单元格不能完全粘老是少数据 ,有些单元格还是空的

环境&#xff1a; excel2021 Win10专业版 问题描述&#xff1a; excel 粘贴回筛选后的单元格不能完全粘老是少数据 有些单元格还是空的 复制选择筛选后A1-A10单元格 &#xff0c;定位条件&#xff09;&#xff08;仅可见单元格&#xff09;来访问&#xff0c;或者你可以使用…

Python接口自动化测试-篇1(postman+requests+pytest+allure)

Python接口自动化测试是一种使用Python编程语言来编写脚本以自动执行针对应用程序接口&#xff08;APIs&#xff09;的测试过程。这种测试方法专注于检查系统的不同组件或服务之间的交互&#xff0c;确保它们按照预期规范进行通信&#xff0c;而不涉及用户界面&#xff08;UI&a…

项目部署到线上byte[]转换中文乱码,本地是好的

项目部署到线上byte[]转换中文乱码&#xff0c;本地是好的 byte[]转换成中文乱码&#xff0c;在idea上面调试没有乱码&#xff0c;部署到线上就乱码&#xff0c;原因是idea启动项目是utf-8, 然后线上是windows环境不知道啥 vo.setJsonObject(JSONUtil.parseObj(Convert.toStr(…