第三章 Freertos物联网实战esp8266模块

在本次物联网温湿度检测中,采用esp8266-01s模块作为wifi模块。实现ONENET云平台与STM32单片机的数据通信。看本文对部分内容感到迷惑的地方可以查看整个教程:【Freertos实战】零基础制作基于stm32的物联网温湿度检测(教程非常简易),如有错误之处,望大家批评指正。

一、ESP8266-01S固件烧入

1.引脚定义

当我们拿到模块的时候首先要看下引脚定义和电压,为我们烧入固件做准备,对于刚到手的wifi模块内部是没有固件的,无法实现联网通信的功能。

这里到手后,可以按照下面的引脚定义进行接线操作。在烧入固件下,IO0也必须要接地,IO0也必须要接地,IO0也必须要接地。着重强调。建议大家买一个专门的烧录器,这样更方便些

|------|-----------|
| 引脚名称 | 描述 |
| GND | GND |
| IO2 | 通用IO内部已上拉 |
| IO0 | 工作模式选择 |
| RXD | 串口接收 |
| 3V3 | 电源正极3.3V |
| RST | 复位 |
| EN | 使能 |
| TX | 串口发送 |

GPIO0 为高电平正常**Flash** 启动
GPIO0为低电平代表进入刷固件状态,此时可以经过串口升级内部固件 RST(GPIO16)可做外部硬件复位使用

2.固件烧入

在接好引脚后,大家可以去这个地址下载固件资料等。资料下载

这个是我们待会要烧入的固件

先进入烧写工具目录下,双击我们的烧入工具

选择esp8266,选择完成后点击OK

这几个参数大家也跟我一样

下载完成后会显示"完成" ,我们在按照下面的方式重新进行接线。

二、数据通信

在完成固件烧入以后,我们就可以开始测试通信,看看能否与云平台进行数据交互。采用的方式是ONENET云平台的MQTT协议。基于新版Onenet搭建云服务(stm32物联网)参考该文章完成云服务搭建。

1.ESP8266的工作模式

ESP8266WIFI 模式有两种,一种叫 AP 模式,一种叫 Station 模式,AP 就是我们平时所说的热点,如 WIFI 路由器,开了热点的手机,或者是公共热点等,这些 AP 设备可以允许其他设备(如手机,笔记本电脑等)输入热点名和密码(也可不设置密码)后接入,Station 则是前面说的连接 AP 的设备,如:手机,笔记本电脑等,ESP8266 还有第三种模式:AP+Station,即:将 AP 和 Station 的功能合二为一,但是应用的场景不多,这里不做展示。

2.测试数据流

大家可以用下面这段数据流去按顺序逐条发送,同时观察自己的云平台数据变化情况,我这边是没有问题的。

这里有疑惑的小伙伴一定要去看下这篇文章:基于新版Onenet搭建云服务(stm32物联网)

cpp 复制代码
1.AT //测试esp8266是否正常工作
2.AT+RST //将设备进行复位,类似重启
3.AT+CWMODE=1 //将esp8266的工作模式选择为station
4.AT+CWDHCP=1,1 //开启 Station 模式下的 DHCP 功能(自动获取 IP 地址)
5.AT+CWJAP="CMCC-yr24","4fy@brba" //WIFI名称CMCC-yr24  密码4fy@brba  大家根据自己的WIFI名称和密码进行修改

6.AT+MQTTUSERCFG=0,1,"DB01","PtAFXPCG49","version=2018-10-31&res=products%2FPtAFXPCG49%2Fdevices%2FDB01&et=1756260513&method=sha1&sign=TcuMb4Vdl%2FQiGc6AkEceJHmt2pI%3D",0,0,""//设备名称或设备ID:DB01   产品ID:PtAFXPCG49 version为token 这部分也是大家根据自己的情况进行修改整合

AT+MQTTCONN=0,"mqtts.heclouds.com",1883,1//连接到ONENT云平台上
AT+MQTTSUB=0,"$sys/PtAFXPCG49/DB01/thing/property/post/reply",0 //MQTT 主题订阅
AT+MQTTSUB=0,"$sys/PtAFXPCG49/DB01/thing/property/set",0 //订阅 "属性设置" 主题

//以下就是stm32往云平台上同步数据流,这里用的是温湿度传感器的信息
AT+MQTTPUB=0,"$sys/PtAFXPCG49/DB01/thing/property/post","{\"id\":\"123\"\,\"params\":{\"Temp\":{\"value\":16\}\,\"Humi\":{\"value\":66\}}}",0,0
或
AT+MQTTPUBRAW=0,"$sys/PtAFXPCG49/DB01/thing/property/post",65,0,0
{"id":"123","params":{"Temp":{"value":33},"Humi":{"value":58}}}

在发送每一条信息后,设备都会返回一段OK的消息。

三、STM32F103C8T6主程序设计

我们之前都是用串口调试助手的方式去给esp8266发送报文信息,这里开始直接用单片机的串口通信去控制,不了解 串口通信的建议大家去看看这篇文章:串口通信(基于stm32)

贴上单片机uart1的代码文件

cpp 复制代码
#ifndef __USART_H
#define __USART_H
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include "sys.h" 
//////////////////////////////////////////////////////////////////////////////////	 

#define USART_REC_LEN  			512  	//接收消息长度
#define EN_USART1_RX 			1		//

extern uint8_t USART_RxFlag;	 
extern uint8_t Recv_LED_Flag;
extern char  USART_RX_BUF[USART_REC_LEN]; 
extern int ok_received;
extern int count;
//extern u16 USART_RX_STA;         		

void uart_init(u32 bound);
void Serial_SendByte(uint8_t Byte);
int wait_for_ok(char *str,long int wait,char *ack);
int parse_json_command(char *json_str, int *parsed_id, bool *led_status, bool *is_led_command);

#endif
cpp 复制代码
#include "sys.h"
#include "usart.h"	 
 /* FreeRTOS头文件 */
#include "FreeRTOS.h"
#include "Task.h"

////////////////////////////////////////////////////////////////////////////////// 	 
//如果使用UCOS,则包括下面的头文件即可
#if SYSTEM_SUPPORT_OS
#include "includes.h"					//ucos 使用
#endif

//////////////////////////////////////////////////////////////////
//加入以下代码,支持printf函数  
#if 1
#pragma import(__use_no_semihosting)             
//标准库需要的支持函数                
struct __FILE 
{ 
	int handle; 

}; 

FILE __stdout;       
//定义  _sys_exit以避免使用半主机模式
void _sys_exit(int x) 
{ 
	x = x; 
} 
//重定义fputc函数
int fputc(int ch, FILE *f)
{      
	while((USART1->SR&0X40)==0);//循环发送,直到发送完毕  
    USART1->DR = (u8) ch;      
	return ch;
}
#endif 


#if EN_USART1_RX   //如果使能了接收
//串口1中断服务程序
//注意,读取USARTx->SR能避免莫名奇妙的错误
char USART_RX_BUF[USART_REC_LEN];     //接收缓冲
uint8_t USART_RxFlag;					//接收完成标志位
u8 Temp_Recv[3];
uint8_t Recv_LED_Flag;					//是否接收到需要的LED信息

int count = 0;//计数各内容
int ok_received = 0;//判断是否接收到了ok

void uart_init(u32 bound){
  	//GPIO端口设置
  	GPIO_InitTypeDef GPIO_InitStructure;
	USART_InitTypeDef USART_InitStructure;
	NVIC_InitTypeDef NVIC_InitStructure;
	 
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE);	//使能USART1,GPIOA时钟
  
	//USART1_TX   GPIOA.9
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;	//复用推挽输出
	GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.9
   
	//USART1_RX	  GPIOA.10初始化
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;//PA10
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
	GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.10  

  	//Usart1 NVIC 配置
  	NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0;//抢占优先级0
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;		//子优先级1
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			//IRQ通道使能
	NVIC_Init(&NVIC_InitStructure);	//初始化NVIC寄存器
  
   //USART 初始化设置

	USART_InitStructure.USART_BaudRate = bound;//波特率
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;//数据位
	USART_InitStructure.USART_StopBits = USART_StopBits_1;//停止位
	USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件流控制
	USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;	//收发模式

	USART_Init(USART1, &USART_InitStructure); //初始化串口1
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启串口接收中断
	USART_Cmd(USART1, ENABLE);                    //使能串口1
	USART_RxFlag = 0;
	Recv_LED_Flag = 0;
	memset(Temp_Recv, 0, 3);
}

void Serial_SendByte(uint8_t Byte)
{
	USART_SendData(USART1, Byte);
	while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}

/**
 * 发送AT指令并检查响应
 * @param command AT指令字符串
 * @param expected_response 期望的响应字符串
 * @param timeout_ms 超时时间(毫秒)
 * @return 成功返回1,失败返回0
 */
int wait_for_ok(char *command,long int timeout_ms,char *expected_response)
{
	int timeout = 0;
	USART_RxFlag=1;
	
		printf("%s",command);
		while (!ok_received && timeout < timeout_ms)
    {
		// 检查是否接收到 "OK"
			if (count >= 2 && strstr(USART_RX_BUF, expected_response) != NULL)
			{
				ok_received = 1;
			}
			vTaskDelay(1);
      timeout++;
    }
		USART_RxFlag = 0;
    if (ok_received)
    {
        // 清空缓冲区
        memset(USART_RX_BUF, 0, USART_REC_LEN);
        count = 0;
        ok_received = 0;
        return 1; // 成功收到 "OK"
    }
    else
    {
       return 0; // 超时未收到 "OK"
    }
}

/**
 * 等待并解析JSON报文,带超时判断
 * @param timeout_ms 超时时间(毫秒)
 * @param parsed_id 解析出的ID(引用传递)
 * @param led_status 解析出的LED状态(引用传递)
 * @param is_led_command 是否为LED控制指令(引用传递)
 * @return 解析成功返回1,失败或超时返回0
 */
int wait_and_parse_json(long int timeout_ms, int *parsed_id, bool *led_status, bool *is_led_command)
{
    int timeout = 0;

    bool json_complete = false;
    
    // 重置接收缓冲区
    memset(USART_RX_BUF, 0, USART_REC_LEN);
    count = 0;
    
    // 等待JSON结束标记 "}}" 或超时
    while (!json_complete && timeout < timeout_ms)
    {
        // 检查是否接收到 "}}"
        if (count >= 2 && strstr(USART_RX_BUF, "}}") != NULL)
        {
            json_complete = true;
        }
        
        vTaskDelay(1);
        timeout++;
    }
    
    if (json_complete)
    {
        // 解析JSON报文
        if (parse_json_command(USART_RX_BUF, parsed_id, led_status, is_led_command))
        {
            // 清空缓冲区
            memset(USART_RX_BUF, 0, USART_REC_LEN);
            count = 0;
            return 1; // 解析成功
        }
        else
        {
            return 0; // 解析失败
        }
    }
    else
    {
        // 清空缓冲区
        memset(USART_RX_BUF, 0, USART_REC_LEN);
        count = 0;
        return 0; // 超时
    }
}

/**
 * 解析JSON报文中的ID和LED状态(C语言指针版本)
 * @param json_str 接收到的JSON字符串
 * @param parsed_id 解析出的ID(指针传递)
 * @param led_status 解析出的LED状态(指针传递)
 * @param is_led_command 是否为LED控制指令(指针传递)
 * @return 解析成功返回1,失败返回0
 */
int parse_json_command(char *json_str, int *parsed_id, bool *led_status, bool *is_led_command) {
    char id_str[10] = {0};
    char led_str[5] = {0};
    
    // 1. 提取ID(格式:"id":"3")
    char *id_pos = strstr(json_str, "\"id\":\"");
    if (id_pos == NULL) return 0;
    id_pos += 6; // 跳过"id":"
    char *id_end = strchr(id_pos, '"');
    if (id_end == NULL) return 0;
    strncpy(id_str, id_pos, id_end - id_pos);
    *parsed_id = atoi(id_str); // 转为整数
    // 2. 检查是否包含LED字段(格式:"LED":true/false)
    char *led_pos = strstr(json_str, "\"LED\":");
    if (led_pos == NULL) {
        *is_led_command = false;
        return 1; // 存在ID但无LED字段
    }
    *is_led_command = true;
    
    // 3. 提取LED状态(true/false)
    led_pos += 6; // 跳过"LED":
    char *led_end = strchr(led_pos, ',');
    if (led_end == NULL) led_end = strchr(led_pos, '}'); // 处理末尾情况
    if (led_end == NULL) return 0;
    strncpy(led_str, led_pos, led_end - led_pos);
    *led_status = (strcmp(led_str, "true") == 0); // 转为布尔值
    
    return 1;
}

// 当串口1收到数据, 系统自动调用此中断函数
void USART1_IRQHandler(void)                	//串口1接收中断
{
	u8 Serial_RxData = 0;
	static int j=0;
	if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
	{
		Serial_RxData = USART_ReceiveData(USART1);
		//判断是否是微信小程序回传信息
		if(Temp_Recv[0] == 's' && Temp_Recv[1] == 'e' && Temp_Recv[2] == 't')
		{
			USART_RxFlag = 1;
			Recv_LED_Flag = 1;
			memset(Temp_Recv, 0, 3);
			memset(USART_RX_BUF, 0, USART_REC_LEN);
			count = 0;
		}
		else if(Temp_Recv[0] == 's' && Temp_Recv[1] == 'e')
		{
			j=2;
			Temp_Recv[j] = Serial_RxData;
		}
		else if(Temp_Recv[0] == 's')
		{
			j=1;
			Temp_Recv[j] = Serial_RxData;
		}
		else
		{
			j=0;
			Temp_Recv[j] = Serial_RxData;
		}

		//抓取内部信息
		if(USART_RxFlag == 1)
		{
			USART_RX_BUF[count++] = Serial_RxData;
			if(count == USART_REC_LEN-1)
			{
				count = 0;
				memset(USART_RX_BUF, 0, USART_REC_LEN);
			}
		}
		USART_ClearITPendingBit(USART1, USART_IT_RXNE);
	}
} 

#endif	

最后贴上esp8266的代码文件

cpp 复制代码
#ifndef __ESP8266_H_
#define __ESP8266_H_	 
#include "sys.h" 
#include "usart.h"
#include <stdbool.h> 

void esp8266_Init();
int esp8266_mqtt_reply(int id);
int esp8266_mqtt_send_int(const char *property_name, int value);
int esp8266_mqtt_send_bool(const char *property_name, bool value);

#endif
cpp 复制代码
#include "esp8266.h"
#include "delay.h"
/* FreeRTOS头文件 */
#include "FreeRTOS.h"
#include "Task.h"

// 设备和网络配置
#define product_id "PtAFXPCG49"
#define device_id "DB01"
#define device_key "version=2018-10-31&res=products%2FPtAFXPCG49%2Fdevices%2FDB01&et=1756260513&method=sha1&sign=TcuMb4Vdl%2FQiGc6AkEceJHmt2pI%3D"

#define wifi_name "CMCC-yr24"
#define wifi_password "4fy@brba"

/**
 * ESP8266初始化函数
 * @param bound 串口波特率
 * @return 初始化成功返回1,失败返回0
 */
void esp8266_Init()
{
    // 定义各命令的超时时间(毫秒)
    const uint32_t AT_TIMEOUT = 1000;     // 普通AT命令超时
    const uint32_t WIFI_TIMEOUT = 30000; // WiFi连接超时
    const uint32_t MQTT_TIMEOUT = 10000; // MQTT连接超时
    
    // 用于构建AT命令的缓冲区
    char cmd_buffer[256];

    // 步骤1:测试AT通信
    while(!wait_for_ok("AT\r\n", AT_TIMEOUT, "OK"));
    vTaskDelay(100);

    // 步骤2:软重启ESP8266
    while(!wait_for_ok("AT+RST\r\n", AT_TIMEOUT, "ready"));
    vTaskDelay(1000); // 等待模块完全重启

    // 步骤3:设置WiFi模式为STA
    while(!wait_for_ok("AT+CWMODE=1\r\n", AT_TIMEOUT, "OK"));
    vTaskDelay(100);

    // 步骤4:启用DHCP
    while(!wait_for_ok("AT+CWDHCP=1,1\r\n", AT_TIMEOUT, "OK"));
    vTaskDelay(100);
 
    // 步骤5:连接WiFi
    snprintf(cmd_buffer, sizeof(cmd_buffer), "AT+CWJAP=\"%s\",\"%s\"\r\n", wifi_name, wifi_password);
    while(!wait_for_ok(cmd_buffer, WIFI_TIMEOUT, "OK"));
    vTaskDelay(100);
 
    // 步骤6:配置MQTT用户信息
    snprintf(cmd_buffer, sizeof(cmd_buffer), "AT+MQTTUSERCFG=0,1,\"%s\",\"%s\",\"%s\",0,0,\"\"\r\n", 
             device_id, product_id, device_key);
    while(!wait_for_ok(cmd_buffer, AT_TIMEOUT, "OK"));
    vTaskDelay(100);

    // 步骤7:连接MQTT服务器
    while(!wait_for_ok("AT+MQTTCONN=0,\"mqtts.heclouds.com\",1883,1\r\n", MQTT_TIMEOUT, "OK"));
    vTaskDelay(100);

    // 步骤8:订阅属性回复主题
    snprintf(cmd_buffer, sizeof(cmd_buffer), "AT+MQTTSUB=0,\"$sys/%s/%s/thing/property/post/reply\",0\r\n", 
             product_id, device_id);
    while(!wait_for_ok(cmd_buffer, AT_TIMEOUT, "OK"));
    vTaskDelay(100);

    // 步骤9:订阅属性设置主题
    snprintf(cmd_buffer, sizeof(cmd_buffer), "AT+MQTTSUB=0,\"$sys/%s/%s/thing/property/set\",0\r\n", 
             product_id, device_id);
    while(!wait_for_ok(cmd_buffer, AT_TIMEOUT, "OK"));
    vTaskDelay(100);

		while(!esp8266_mqtt_send_bool("LED", false));
		vTaskDelay(100);

}

/**
 * 发送MQTT回复消息到OneNET平台
 * @param id 平台下发指令的ID值
 * @return 发送成功返回1,失败返回0
 */
// 当接收到平台下发的指令ID为36时,回复成功
//esp8266_mqtt_reply(36);
int esp8266_mqtt_reply(int id)
{
    // 定义命令缓冲区
    char cmd_buffer[256];
    // 定义消息内容缓冲区
    char msg_buffer[128];

    // 构建JSON格式的消息内容(修正转义)
    snprintf(msg_buffer, sizeof(msg_buffer), 
             "{\\\"id\\\":\\\"%d\\\"\\,\\\"code\\\": 200\\,\\\"msg\\\":\\\"success\\\"}", id);
    
    // 构建完整的AT+MQTTPUB命令
    snprintf(cmd_buffer, sizeof(cmd_buffer), 
             "AT+MQTTPUB=0,\"$sys/%s/%s/thing/property/set_reply\",\"%s\",0,0\r\n", 
             product_id, device_id, msg_buffer);
    
    // 发送命令并等待响应
    return wait_for_ok(cmd_buffer, 5000, "OK");
}

/**
 * 发送单个传感器值到OneNET平台
 * @param property_name 要上报的属性名称,如"Temp"或"Humi"
 * @param value 属性值
 * @return 发送成功返回1,失败返回0
 */
// 发送温度值16
//esp8266_mqtt_send_int("Temp", 16);

// 发送湿度值66
//esp8266_mqtt_send_int("Humi", 66);
int esp8266_mqtt_send_int(const char *property_name, int value)
{
    // 定义命令缓冲区
    char cmd_buffer[256];
    // 定义消息内容缓冲区
    char msg_buffer[128];
    
    // 构建JSON格式的消息内容(修正转义)
    snprintf(msg_buffer, sizeof(msg_buffer), 
             "{\\\"id\\\":\\\"123\\\"\\,\\\"params\\\":{\\\"%s\\\":{\\\"value\\\":%d}}}", 
             property_name, value);
    
    // 构建完整的AT+MQTTPUB命令
    snprintf(cmd_buffer, sizeof(cmd_buffer), 
             "AT+MQTTPUB=0,\"$sys/%s/%s/thing/property/post\",\"%s\",0,0\r\n", 
             product_id, device_id, msg_buffer);
    
    // 发送命令并等待响应(注意:MQTT响应通常是"OK"而非"success")
    return wait_for_ok(cmd_buffer, 5000, "OK");
}

/**
 * 发送布尔类型的传感器值到OneNET平台
 * @param property_name 要上报的属性名称,如"LED"
 * @param value 属性值(true或false)
 * @return 发送成功返回1,失败返回0
 */
// 发送LED开启状态
//esp8266_mqtt_send_bool("LED", true);

// 发送LED关闭状态
//esp8266_mqtt_send_bool("LED", false);
int esp8266_mqtt_send_bool(const char *property_name, bool value)
{
    // 定义命令缓冲区
    char cmd_buffer[256];
    // 定义消息内容缓冲区
    char msg_buffer[128];
    // 将布尔值转换为JSON格式的字符串
    const char *bool_str = value ? "true" : "false";
    
    // 构建JSON格式的消息内容(修正转义)
    snprintf(msg_buffer, sizeof(msg_buffer), 
             "{\\\"id\\\":\\\"123\\\"\\,\\\"params\\\":{\\\"%s\\\":{\\\"value\\\":%s}}}", 
             property_name, bool_str);
    
    // 构建完整的AT+MQTTPUB命令
    snprintf(cmd_buffer, sizeof(cmd_buffer), 
             "AT+MQTTPUB=0,\"$sys/%s/%s/thing/property/post\",\"%s\",0,0\r\n", 
             product_id, device_id, msg_buffer);
    
    // 发送命令并等待响应(注意:MQTT响应通常是"OK"而非"success")
    return wait_for_ok(cmd_buffer, 5000, "OK");
}

创建不易。希望大家能够点赞、收藏、关注。谢谢大家!!!!!!!!

相关推荐
时序数据说4 小时前
时序数据库IoTDB的核心功能特性
大数据·数据库·物联网·时序数据库·iotdb
ws2019078 小时前
AUTO TECH 2025广州先进汽车材料展:华南聚能,能否点燃产业升级新引擎?
大数据·人工智能·物联网
TDengine (老段)15 小时前
TDengine 计算百分位函数使用手册
大数据·数据库·物联网·时序数据库·iot·tdengine·涛思数据
小赖同学啊20 小时前
物联网-规则引擎的定义
物联网
御控工业物联网21 小时前
智能泵房监控系统:物联网应用与智能管理解决方案
物联网·智慧水利·智能泵房监控系统·泵房远程运维·工业数字化转型
liupengfei-iot1 天前
物联网后端系统架构:从基础到AI驱动的未来 - 第八章:AI赋能的部署与运维
人工智能·物联网·系统架构
华普微HOPERF1 天前
华普微Matter模块HM-MT7201,打破智能家居生态孤岛
科技·物联网·智能家居
IT项目分享1 天前
实时视频传输遥控车:DIY智能家居监控与探索机器人
人工智能·物联网·机器人·智能家居·it项目网
猫猫的小茶馆1 天前
【STM32】CRC 校验函数
stm32·单片机·嵌入式硬件·物联网·51单片机·智能硬件·pcb工艺
liupengfei-iot1 天前
物联网后端系统架构:从基础到AI驱动的未来 - 第九章:实际案例分析
人工智能·物联网·系统架构