物联网智慧教室项目(完整版)

物联网智慧教室项目(一):智慧教室项目解决方案

一、智慧教室项目设计

(一)环境信息采集控制功能

1、硬件设计
  • 使用STM32开发板模拟灯光控制,报警控制,光照信息采集:
    • 灯光控制通过GPIO控制板载LED
    • 报警控制通过GPIO控制蜂鸣器
    • 光照采集通过ADC采集板载光敏电阻
2、 软件设计
  • 控制接口
    • LED打开,关闭
    • Buzer打开,关闭
  • 采集任务
    • 创建采集任务
    • 等待Zigbee串口数据
    • 解析Zigbee串口数据

(二)人机交互功能

1、硬件设计
  • LCD接口
    • 数据/指令接口,采用STM32 FSMC接口
    • 触摸感应接口,采用STM32 SPI接口
  • 外部扩展内存接口
    • 需要跑嵌入式GUI库,对内存要求比较高
    • 采用STM32 FSMC接口驱动外部SRAM
  • 外部扩展FLASH接口
    • GUI显示中文,需要有中文字库,需要把中文字库放在外部FLASH
    • 采用STM32 SPI接口驱动外部FLASH
2、 软件设计
  • LCD外设驱动
    • Touch驱动
    • LCD驱动
  • 嵌入式GUI库
    • STemWin移植
    • GUI显示任务
    • Touch检测任务

(三)WebServer功能

1、硬件设计

WebServer是基于网络通信,需要硬件支持

  • 以太网-WebServer
    • STM32 通过RMII接口驱动以太网外设
  • SD卡- 存储网页文件
    • STM32 通过RMII接口驱动以太网外设
2、软件设计

Web其实就是浏览器与服务器通过HTTP协议进行网络通信

  • STM32作为Web服务器
    • 通过LwIP驱动网卡,实现HttpServer
    • 通过FatFS驱动SD卡,实现存储网页文件
  • Web网页开发
    • 移植开源网页框架

二、开发环境搭建

(一)软件环境

STM32

STM32CubeMX
c 复制代码
/*
	1.STM32CubeMX 要求版本 5.0.0以上
*/
c 复制代码
/*
	2.STM32Cube MCU Package for STM32F4   要求版本1.23.0
*/	
MDK-ARM
c 复制代码
/*
	MDK-ARM 要求版本5.23.0.0 以上
*/
c 复制代码
/*
	Keil.STM32F4xx_DFP.2.12.0.pack
*/

zigbee

IAR for 8051
c 复制代码
/*
	EW8051-EV-8103-Web
*/
Zstack
c 复制代码
/*
	EW8051-EV-8103-Web
*/
串口调试工具

(二)硬件环境

STM32

STM32F407开发板
c 复制代码
/*需要安装驱动程序*/
USB转RS232
c 复制代码
/*需安装USB串口驱动*/
读卡器
网线

zigbee

CC2530开发板
c 复制代码
/*需安装SmartRF04EB驱动*/

物联网智慧教室项目(二):智慧教室项目驱动开发

一、环境监控驱动开发

外设驱动开发流程

Created with Raphaël 2.3.0 原理图分析 数据手册分析 外设配置

(一)时钟&SWD配置

原理图分析

时钟原理图

如下图所示,STM32F407外部高速晶振为25MHz,分别连接到PH0和PH1引脚!

SWD原理图

如下图所示,STM32F407仿真接口SWD分别连接到PA13和PA14引脚!

外设配置

时钟外设配置
使能时钟源
  1. 选择RCC外设
  2. 选择高速时钟为外部时钟源
  3. PH0和PH1引脚自动高亮
配置时钟树
  1. 锁相环时钟源为25MHz外部高速时钟
  2. 高速时钟分频系数配置为25,输出为1MHz
  3. 倍频系数配置为336
  4. 分频系数配置为2,输出为168MHz
  5. 系统时钟源选择PLL
  6. APB1配置为4分频,为42MHz
  7. APB2配置为2分频,为84MHz
SWD外设配置
  1. 选择SYS外设
  2. 配置debug接口为串行接口(SWD)
  3. 引脚自动高亮

(二)串口配置

原理图分析

Zigbee通信接口
  1. J28为Zigbee模块底座,其中Z_W_R和Z_W_T分别Zigbee串行通信接口
  2. Z_W_R和Z_W_T网络连接到J13,通过J13选择连接STM32还是USB转串口,我们选择连接到STM32上的USART1(PA9和PA10)
串行调试接口

如下图所示:

  1. 板载两个串行通信接口,串口1连接到USART1,串口2连接到USART3,我们选择USART3
  2. 由于USART3可以用于串口和485通信,我们选择485必须要把CON4和CON5拨到串口通信

外设配置

USART1配置
  1. 配置PA9和PA10为USART1模式
  2. 打开USART1,配置为异步通信模式

3.使能NVIC,优先级先默认为0

USART3配置
  1. 配置PB10和PB11为USART3模式
  2. 打开USART3外设,并配置为异步通信模式

(三)GPIO配置

原理图分析

  1. PF7 PF8 PF9 PF10 控制板载的D6 D7 D8 D9
  2. PF6控制板载蜂鸣器

GPIO外设配置

  1. 配置PF6-PF10为输出模式
  2. PF6默认输出低
  3. PF7-PF10默认输出高

二、人机交互驱动开发

(一)FSMC

1、SRAM

原理图分析

通过下图所示:

  1. 采用IS61LV51216 SRM 为1MB,其实为了节约成本焊接的为IS61LV25616 为512KB
  2. 占用地址总线为18bit,数据总线为16bit
  3. 内存访问起始地址为0x6800 0000
数据手册分析
read周期

地址建立时间 <7ns

数据建立+保持时间 = 7 + 2.5ns = 10ns

write周期

数据建立周期 = 5 + 3 = 8 + 5ns = 13ns

FSMC读写周期
read
write
外设配置
  1. 打开FSMC外设
  2. 配置FSMC
    1. 选择存储块为NE3
    2. 内存类型为SRAM
    3. 寻址长度为18bit
    4. 数据宽度为16bit
  3. 配置FSMC时序
    1. 地址建立时间为1分频 1/168 = 0.005 = 5ns
    2. 数据建立时间为3分频
  1. 字节访问使能

2、LCD

原理图分析
  1. 如下图所示,LCD采用8080接口,CS片选,D/C命令/数据切换,RD读操作,WR写操作,D[23:0]数据总线
  1. 如下图所示,数据总线D[0:15]连接FSMC总线接口处,RS起始就D/C接口,连接到FSMC地址总线A0,CS片选总线连接到FSMC_NE4上,WR写操作连接FSMC_NWE总线上,RD读操作总线连接到FSMC_NOE上,背光控制连接到PC7上
  1. 写命令操作0x6C00 0000
  2. 写数据操作0x6C00 0002
数据手册分析
  1. 分析LCD驱动芯片时序图,计算得出地址和数据总线建立时间

    地址保持周期 = 2ns

    数据建立周期 = 12 + 1ns = 15ns

外设配置
  1. 打开FSMC外设
  2. 配置FSMC参数
    1. 内存块为NE4
    2. 内存类型为LCD
    3. LCD数据/命令切换映射到A0
    4. 数据宽度为16bit
  1. 配置PC7为输出模式
    1. 上电默认输出高电平

SPI

FLASH

原理图分析
  1. 如下图所示,SPI接口,CS连接到PH2,MISO连接到PB4,MOSI连接到PB5,CLK连接到PA5
数据手册分析

1.时钟极性及相位

通过时序图分析,SPI CLK 高有效 时钟极性为高

时钟边沿为奇数边沿

2.SPI速率

满足数据传输速率,最大不能超过33MHz

外设配置
  1. 配置SPI时钟和数据引脚
  2. 配置SPI为全双工主机模式
  3. 配置SPI参数
    1. 通信速率为系统时钟4分频
    2. 时钟极性为低电平
    3. 相位为奇数边沿
  1. 配置片选引脚PH2默认输出高

Touch

原理图分析
  1. 如下图所示,SPI接口,CS连接到PG15,MISO连接到PI2,MOSI连接到PI3,SCK连接到PI1
  2. 触摸中断连接到PG7
数据手册分析
  1. 通过计算TCH+TCL得出SPI通信速率
  2. 通过时序图分析,SPI不工作时为低电平
  1. 时钟边沿为奇数边沿
  2. 通信速率最小为400ns,大概2Mbit/S左右
外设配置
  1. 配置SPI时钟和数据引脚
  2. 配置SPI为全双工主机模式
  3. 配置SPI参数
    1. 通信速率为系统时钟32分频
    2. 时钟极性为低电平
    3. 相位为奇数边沿
  1. 配置SPI片选引脚
    1. 配置PG15为输出模式
    2. 配置PG15上电默认输出高,SPI低电平有效

三、WebServer驱动开发

(一)SDIO

原理图分析

根据原理图分析,我们采用SD总线,4bit

配置CLK、DAT0-3

外设配置

  1. 打开SDIO外设
  2. 配置SD总线为4bit位宽
  3. 配置DMA接收和发送
  1. 使能sdio全局中断 重点配置DMA优先级小于SDIO优先级

(二)ETH

原理图分析

  1. 如下图所示,以太网PHY采用DP83848芯片,通信模式采用RMII接口

  2. 配置MDC、CLK、MDIO、DV、RXD0、RXD1;EN、TXD0、TXD1

数据手册分析

Created with Raphaël 2.3.0 配置PHY地址为0x01 配置PHYSTS寄存器地址为0x10 配置Speed Status 掩码为0x0002 配置Duplex Status 掩码为0x0004

PHY地址

PHY寄存器

BMCR
BMSR
PHYSTS

外设配置

四、FreeRTOS配置及任务创建

(一)freeRTOS配置

配置内核定时器

由于我们采用STM32HAL库进行开发,HAL库内部使用systick定时器用于系统延时功能,而FreeRTOS也需要一个定时器用于操作系统内核调度使用, 顾需要修改HAL定时器时钟源

  1. 打开SYS选项
  2. 配置时钟源为TIM1

配置FreeRTOS内核功能

  • 多数功能在后续程序设计中,需要根据具体功能,进行配置
  • 前期只需要配置动态内存空间和创建开始任务就可以
配置动态内存分配空间
  1. 采用FreeRTOS动态内存分配,开发效率高!顾我们程序内存使用,多数使用动态内存分配方式,分配动态内存总空间为23k=23552byte
    2. 使能FreeRTOS功能
    3. 分配内存空间为40960

(二)任务创建

任务及优先级划分

Created with Raphaël 2.3.0 WebServer 高 Touch Zigbee GUI 低

任务堆栈划分

Created with Raphaël 2.3.0 WebServer 16K Touch 4K Zigbee 1K GUI 8K

1、中断优先级分配

2、printf重定向

  1. 在main.c文件内添加fputc函数,采用USART3作为调试接口
c 复制代码
int fputc(int ch, FILE *p)
{
	while(!(USART1->SR & (1<<7)));
	
	USART1->DR = ch;
	
	return ch;
}
  1. 在touch任务内打印启动信息

五、lwIP配置及网卡驱动

(一)lwIP配置

1、IP组网配置

  1. 使能lwIP
  2. 关闭DHCP服务
  3. 配置IP地址信息

2、lwIP参数配置

  1. 分配10k内存空间
  2. 使能链路检测回调功能

(二)ping测试

配置笔记本的网段和STM32开发板的网段一致

以太网断线检测

c 复制代码
ethernetif.c
/* USER CODE BEGIN 8 */
/**
  * @brief  This function notify user about link status changement.
  * @param  netif: the network interface
  * @retval None
  */
__weak void ethernetif_notify_conn_changed(struct netif *netif)
{
  /* NOTE : This is function could be implemented in user file 
            when the callback is needed,
  */
	
	if(netif_is_link_up(netif)){
		
		printf("netif link is up\r\n");
		if(!netif_is_up(netif)){
			netif_set_up(netif);
			printf("netif is up\r\n");
		}
	}else{
		printf("netif link is down\r\n");
		
	}

}

两种情况,一种是设备通电时断开网线,一种是网线没插,设备通电

  • 第一种, 断开网线之后再插入,通过ping命令进行检测
  • 第二种,断开网线,设备通电,当设备正常工作后,再插入网线,通过ping命令进行检测。

物联网智慧教室项目(三):嵌入式文件系统FatFS

一、FatFS介绍及STM32集成

(一)FatFS介绍

官方网站(可边翻译成中文对照学习)

URL 复制代码
http://elm-chan.org/fsw/ff/00index_e.html

应用接口

介质访问接口

相关资源

(二)STM32Cube集成FatFS

FatFs 中间件模块架构

FatFS配置

  1. 打开FatFS
  2. 使能磁盘为SD卡
  3. 配置中文编码
  4. 配置命名空间为HEAP
  1. 增大C库堆空间

FatFS示例代码

c 复制代码
uint8_t u8chr[] = "hello";
uint32_t u32Wbytes;

/* USER CODE END Variables */    

void MX_FATFS_Init(void) 
{
  /*## FatFS: Link the SD driver ###########################*/
  retSD = FATFS_LinkDriver(&SD_Driver, SDPath);
  
  

  /* USER CODE BEGIN Init */
  
  if(f_mount(&SDFatFS,SDPath,1) == FR_OK)
  {
    if(f_open(&SDFile,(const char*)"fatfs.txt",FA_CREATE_ALWAYS|FA_WRITE) == FR_OK)
    {
      if(f_write(&SDFile,u8chr,sizeof(u8chr),&u32Wbytes) == FR_OK)
      {
        f_close(&SDFile);
      
      }
    
    }
  
  }
  
  
  /* additional user code for init */     
  /* USER CODE END Init */
}

二、FatFS应用编程上

(一)FatFS提供的应用接口

(二)API学习方法

学习步骤

Created with Raphaël 2.3.0 函数原型分析 典型代码案例阅读 应用编程

项目当中用到的API

f_mkfs //物理磁盘的格式化
f_mount
f_open
f_read
f_write
f_size
f_close

(三)API分析

f_mkfs

c 复制代码
FRESULT f_mkfs (
  const TCHAR* path,  /* [IN] Logical drive number */
  BYTE  opt,          /* [IN] Format options */
  DWORD au,           /* [IN] Size of the allocation unit */
  void* work,         /* [-]  Working buffer */
  UINT len            /* [IN] Size of working buffer */
);

f_mount

c 复制代码
FRESULT f_mount (
  FATFS*       fs,    /* [IN] File system object */
  const TCHAR* path,  /* [IN] Logical drive number */
  BYTE         opt    /* [IN] Initialization option */
);

f_open

c 复制代码
FRESULT f_open (
  FIL* fp,           /* [OUT] Pointer to the file object structure */
  const TCHAR* path, /* [IN] File name */
  BYTE mode          /* [IN] Mode flags */
);

f_close

c 复制代码
FRESULT f_close (
  FIL* fp     /* [IN] Pointer to the file object */
);

f_read

c 复制代码
FRESULT f_read (
  FIL* fp,     /* [IN] File object */
  void* buff,  /* [OUT] Buffer to store read data */
  UINT btr,    /* [IN] Number of bytes to read */
  UINT* br     /* [OUT] Number of bytes read */
);

f_write

c 复制代码
FRESULT f_write (
  FIL* fp,          /* [IN] Pointer to the file object structure */
  const void* buff, /* [IN] Pointer to the data to be written */
  UINT btw,         /* [IN] Number of bytes to write */
  UINT* bw          /* [OUT] Pointer to the variable to return number of bytes written */
);

f_size

c 复制代码
FSIZE_t f_size (
  FIL* fp   /* [IN] File object */
);

三、FatFS应用编程下

(一)实际文件系统应用案例分析

C 复制代码
//远程终端单元( Remote Terminal Unit,RTU)

(二)怎么存储历史数据呢

历史数据的目的是什么

CSV格式文件

(三)历史数据存储功能实现

Created with Raphaël 2.3.0 创建文件 格式化文件流 文件写入

(四)代码

c 复制代码
	if(f_open(&SDFile,(const char*)"Sensor.csv",FA_CREATE_ALWAYS|FA_WRITE) == FR_OK)
	{
		//格式化文件流
		//创建表头
		sprintf(SensorBuff,"序号,温度,适度,光照\r\n");
		f_write(&SDFile,SensorBuff,strlen(SensorBuff),&u32Wbytes);
		
		//循环写入表项
		for(int i; i < 10; i++)
		{
			sprintf(SensorBuff,"%d,%d,%d,%d\r\n",i + 1, i + 20, i + 30, i + 40);
			f_write(&SDFile,SensorBuff,strlen(SensorBuff),&u32Wbytes);
			//刷新到文件中
			//f_sync(&SDFile);
		}
		//关闭文件,缓存写入文件内
		f_close(&SDFile);
	}
	

四、FatFS底层实现

通用底层驱动API

驱动包含哪些文件

ff_gen_drv.c

c 复制代码
//FatFS 提供的通用驱动文件的实现
c 复制代码
/**
  * @brief  Disk IO Driver structure definition
  */
typedef struct
{
  DSTATUS (*disk_initialize) (BYTE);                     /*!< Initialize Disk Drive                     */
  DSTATUS (*disk_status)     (BYTE);                     /*!< Get Disk Status                           */
  DRESULT (*disk_read)       (BYTE, BYTE*, DWORD, UINT);       /*!< Read Sector(s)                            */
#if _USE_WRITE == 1
  DRESULT (*disk_write)      (BYTE, const BYTE*, DWORD, UINT); /*!< Write Sector(s) when _USE_WRITE = 0       */
#endif /* _USE_WRITE == 1 */
#if _USE_IOCTL == 1
  DRESULT (*disk_ioctl)      (BYTE, BYTE, void*);              /*!< I/O control operation when _USE_IOCTL = 1 */
#endif /* _USE_IOCTL == 1 */

}Diskio_drvTypeDef;
c 复制代码
/**
  * @brief  Global Disk IO Drivers structure definition
  */
typedef struct
{
  uint8_t                 is_initialized[_VOLUMES];//磁盘是否初始化
  const Diskio_drvTypeDef *drv[_VOLUMES];//磁盘的驱动
  uint8_t                 lun[_VOLUMES];//磁盘的编号
  volatile uint8_t        nbr;

}Disk_drvTypeDef;

sd_diskio.c

c 复制代码
//针对SD底层驱动实现,封装成为通用的底层驱动API
//如果使能freeRTOS,在read和Write里面,会用到操作系统的消息队列

bsp_driver_sd.c

C 复制代码
//HAL库的二次封装,把所有基于SD卡的操作都在bsp_driver_sd实现

驱动装载

Created with Raphaël 2.3.0 MX_FATFS_Init FATFS_LinkDriver FATFS_LinkDriverEx

c 复制代码
/**
  * @brief  Links a compatible diskio driver/lun id and increments the number of active
  *         linked drivers.
  * @note   The number of linked drivers (volumes) is up to 10 due to FatFs limits.
  * @param  drv: pointer to the disk IO Driver structure
  * @param  path: pointer to the logical drive path
  * @param  lun : only used for USB Key Disk to add multi-lun management
            else the parameter must be equal to 0
  * @retval Returns 0 in case of success, otherwise 1.
  */
uint8_t FATFS_LinkDriverEx(const Diskio_drvTypeDef *drv, char *path, uint8_t lun)
{
  uint8_t ret = 1;
  uint8_t DiskNum = 0;
  //判断是否超出了fatfs最大的卷数量
  if(disk.nbr < _VOLUMES)
  {
    //未初始化
    disk.is_initialized[disk.nbr] = 0;
    //把驱动进行链接
    disk.drv[disk.nbr] = drv;
    disk.lun[disk.nbr] = lun;
    DiskNum = disk.nbr++;
    path[0] = DiskNum + '0';  //"1:/"
    path[1] = ':';
    path[2] = '/';
    path[3] = 0;
    ret = 0;
  }

  return ret;
}

物联网智慧教室项目(四):emWin图形界面库

一、emWin移植上

LCD驱动程序

c 复制代码
#ifndef __LCD_H__
#define __LCD_H__   
#include "stm32f4xx_hal.h"
void lcd_clear(uint16_t Color);
void lcd_init(void);
void write_data_Prepare(void);
unsigned short lcd_read_gram(unsigned int x,unsigned int y);
void LCD_DrawPoint(uint16_t xsta, uint16_t ysta, uint16_t color);
void LCD_ShowString(uint16_t x0, uint16_t y0, uint8_t *pcStr, uint16_t PenColor, uint16_t BackColor);
void LCD_Fill(uint16_t xsta, uint16_t ysta, uint16_t xend, uint16_t yend, uint16_t colour);
#endif

STemWin结构框架

(一)CRC开启

(二)SRAM 写操作要使能

(三)获取STemWin源码文件

STemWin默认在STM32CUBEMX文档下

例如C:\Users\Think\STM32Cube\Repository\STM32Cube_FW_F4_V1.24.1\Middlewares\ST\STemWin

(四)emWin移植到项目工程

  1. 复制STemWin源码到项目工程中

    工程目录:SmartClassRoom\Middlewares\Third_Party\STemWin

  2. 在keil工程中添加相关文件

    1. 新建工作组:Middlewares/STemWin

    2. 添加需要编译的C和库文件

文件名称 文件描述
GUI_X_OS.c OS支持文件,不需要修改
GUIConf.c GUI配置文件,主要用于GUI内存块初始化
GUIDRV_Template.c GUI驱动模块,主要针对LCD操作接口
LCDConf_FlexColor_Template.c GUI显示配置文件,主要用于LCD参数配置,初始化
GUI_X_Touch_Analog.c 需要自己单独定义,用于触摸笔驱动
STemWin_CM4_OS_wc16_ot.a 基于Cortex-M4驱动库,STemWin源码不开放
  1. 修改库文件格式(keil默认不识别.a文件格式,需要我们手动配置)

(五)移植lcd和touch驱动文件

  1. 添加lcd.c和Touch.c到Src目录下
  1. 添加lcd.h和Touch.h到Inc目录下

二、emWin移植下

(一)emWin LCD驱动适配

修改GUIConf.c

c 复制代码
#include "GUI.h"

/*********************************************************************
*
*       Defines
*
**********************************************************************
*/
//
// Define the available number of bytes available for the GUI
//
#define GUI_NUMBYTES    (512*1024)		 //定义外部存储器大小
#define GUI_BLOCKSUZE   (0X80)			//定义最小内存库操作大小
#define SRAM_BANK_ADDR  ((U32)0x68000000)	//定义外部存储器首地址

/*********************************************************************
*
*       Public code
*
**********************************************************************
*/
/*********************************************************************
*
*       GUI_X_Config
*
* Purpose:
*   Called during the initialization process in order to set up the
*   available memory for the GUI.
*/
void GUI_X_Config(void) {
  //
  // 32 bit aligned memory area
  //
	volatile U32* aMemory = (volatile U32*)(SRAM_BANK_ADDR);
  //
  // Assign memory to emWin
  //分配GUI存储器首地址及最小操作内存块大小
  GUI_ALLOC_AssignMemory((void *)aMemory, GUI_NUMBYTES);
  GUI_ALLOC_SetAvBlockSize(GUI_BLOCKSUZE);
  //
  // Set default font
  //
  GUI_SetDefaultFont(GUI_FONT_32_1);
}

修改GUIDRV_Template.c

只需要完成画点和读取点操作即可

c 复制代码
/*********************************************************************
*
*       _SetPixelIndex
*
* Purpose:
*   Sets the index of the given pixel. The upper layers
*   calling this routine make sure that the coordinates are in range, so
*   that no check on the parameters needs to be performed.
*/
static void _SetPixelIndex(GUI_DEVICE * pDevice, int x, int y, int PixelIndex) {
    //
    // Convert logical into physical coordinates (Dep. on LCDConf.h)
    //
    #if (LCD_MIRROR_X == 1) || (LCD_MIRROR_Y == 1) || (LCD_SWAP_XY == 1)
      int xPhys, yPhys;

      xPhys = LOG2PHYS_X(x, y);
      yPhys = LOG2PHYS_Y(x, y);
    #else
      #define xPhys x
      #define yPhys y
    #endif
    GUI_USE_PARA(pDevice);
    GUI_USE_PARA(x);
    GUI_USE_PARA(y);
    GUI_USE_PARA(PixelIndex);
    {
      //
      // Write into hardware ... Adapt to your system
      //添加lcd画点接口
      LCD_DrawPoint(x,y,PixelIndex);
      // TBD by customer...
      //
    }
    #if (LCD_MIRROR_X == 0) && (LCD_MIRROR_Y == 0) && (LCD_SWAP_XY == 0)
      #undef xPhys
      #undef yPhys
    #endif
}
/*********************************************************************
*
*       _GetPixelIndex
*
* Purpose:
*   Returns the index of the given pixel. The upper layers
*   calling this routine make sure that the coordinates are in range, so
*   that no check on the parameters needs to be performed.
*/
static unsigned int _GetPixelIndex(GUI_DEVICE * pDevice, int x, int y) {
  unsigned int PixelIndex;
    //
    // Convert logical into physical coordinates (Dep. on LCDConf.h)
    //
    #if (LCD_MIRROR_X == 1) || (LCD_MIRROR_Y == 1) || (LCD_SWAP_XY == 1)
      int xPhys, yPhys;

      xPhys = LOG2PHYS_X(x, y);
      yPhys = LOG2PHYS_Y(x, y);
    #else
      #define xPhys x
      #define yPhys y
    #endif
    GUI_USE_PARA(pDevice);
    GUI_USE_PARA(x);
    GUI_USE_PARA(y);
    {
      //
      // Write into hardware ... Adapt to your system
      //添加lcd读取点接口
      PixelIndex = lcd_read_gram(x,y);
      
      // TBD by customer...
      //
      PixelIndex = 0;
    }
    #if (LCD_MIRROR_X == 0) && (LCD_MIRROR_Y == 0) && (LCD_SWAP_XY == 0)
      #undef xPhys
      #undef yPhys
    #endif
  return PixelIndex;
}

修改LCDConf_FlexColor_Template.c

  1. 定义显示尺寸 480*272
  2. 定义触摸笔X,Y AD测量值(需要自己测量获得)
  3. 添加触摸笔校准函数
  4. 添加lcd初始化函数
c 复制代码
#include "GUI.h"
#include "GUIDRV_FlexColor.h"
#include "lcd.h"

/*********************************************************************
*
*       Layer configuration (to be modified)
*
**********************************************************************
*/

//
// Physical display size
//
#define XSIZE_PHYS  480 //  屏幕X坐标长度
#define YSIZE_PHYS  272 //  屏幕Y坐标长度


#define GUI_TOUCH_AD_Y_TOP 170		// 屏幕X0点坐标AD值
#define GUI_TOUCH_AD_Y_BOTTOM 1900	// 屏幕X480点坐标AD值
#define GUI_TOUCH_AD_X_LEFT 100		// 屏幕Y0点坐标AD值
#define GUI_TOUCH_AD_X_RIGHT 1930	// 屏幕Y272点坐标AD值

/*********************************************************************
*
*       Configuration checking
*
**********************************************************************
*/
#ifndef   VXSIZE_PHYS
  #define VXSIZE_PHYS XSIZE_PHYS
#endif
#ifndef   VYSIZE_PHYS
  #define VYSIZE_PHYS YSIZE_PHYS
#endif
#ifndef   XSIZE_PHYS
  #error Physical X size of display is not defined!
#endif
#ifndef   YSIZE_PHYS
  #error Physical Y size of display is not defined!
#endif
#ifndef   GUICC_565
  #error Color conversion not defined!
#endif
#ifndef   GUIDRV_FLEXCOLOR
  #error No display driver defined!
#endif

/*********************************************************************
*
*       Public functions
*
**********************************************************************
*/
/*********************************************************************
*
*       LCD_X_Config
*
* Function description:
*   Called during the initialization process in order to set up the
*   display driver configuration.
*
*/
void LCD_X_Config(void) {
  //
  // 配置GUI LCD驱动以及颜色显示方式
  //
  GUI_DEVICE_CreateAndLink(&GUIDRV_Template_API, GUICC_M565, 0, 0);
  //
  // 显示尺寸配置
  //
  LCD_SetSizeEx (0, XSIZE_PHYS , YSIZE_PHYS);
  LCD_SetVSizeEx(0, VXSIZE_PHYS, VYSIZE_PHYS);
	
  //触摸笔校准
  GUI_TOUCH_Calibrate(GUI_COORD_X, 0, 480, GUI_TOUCH_AD_X_LEFT , GUI_TOUCH_AD_X_RIGHT);
  GUI_TOUCH_Calibrate(GUI_COORD_Y, 0, 272, GUI_TOUCH_AD_Y_TOP, GUI_TOUCH_AD_Y_BOTTOM);
  //
  // Orientation
  //
  //
  // Set controller and operation mode
  //
}

/*********************************************************************
*
*       LCD_X_DisplayDriver
*
* Function description:
*   This function is called by the display driver for several purposes.
*   To support the according task the routine needs to be adapted to
*   the display controller. Please note that the commands marked with
*   'optional' are not cogently required and should only be adapted if
*   the display controller supports these features.
*
* Parameter:
*   LayerIndex - Index of layer to be configured
*   Cmd        - Please refer to the details in the switch statement below
*   pData      - Pointer to a LCD_X_DATA structure
*
* Return Value:
*   < -1 - Error
*     -1 - Command not handled
*      0 - Ok
*/
int LCD_X_DisplayDriver(unsigned LayerIndex, unsigned Cmd, void * pData) {
  int r;
  (void) LayerIndex;
  (void) pData;
  
  switch (Cmd) {
  case LCD_X_INITCONTROLLER: {
    //
    // Called during the initialization process in order to set up the
    // display controller and put it into operation. If the display
    // controller is not initialized by any external routine this needs
    // to be adapted by the customer...
    //
    // ...
		//添加lcd初始化
		lcd_init();
    return 0;
  }
  default:
    r = -1;
  }
  return r;
}

添加GUI_X_Touch_Analog.c

c 复制代码
#include "GUI.h"
#include "Touch.h"


void GUI_TOUCH_X_ActivateX(void) 
{

}

void GUI_TOUCH_X_ActivateY(void)
{

}

//获取X坐标AD值
int  GUI_TOUCH_X_MeasureX(void) 
{
	return XPT_Read_XY(CMD_RDX);
}

//获取Y坐标AD值
int  GUI_TOUCH_X_MeasureY(void) 
{	
	return XPT_Read_XY(CMD_RDY);
}

(二)emWin测试程序编写

测试程序编写

c 复制代码
/* USER CODE BEGIN Header_Touch_Task */
/**
  * @brief  Function implementing the TouchTask thread.
  * @param  argument: Not used 
  * @retval None
  */
/* USER CODE END Header_Touch_Task */
void Touch_Task(void const * argument)
{

	GUI_PID_STATE State;    
                 
//  /* init code for FATFS */
//  MX_FATFS_Init();

//  /* init code for LWIP */
//  MX_LWIP_Init();


  GUI_Init();
  GUI_SetBkColor(GUI_BLUE);
  GUI_SetFont(GUI_FONT_32_1);
  GUI_SetColor(GUI_YELLOW);
  GUI_Clear();	
  /* Infinite loop */
  for(;;)
  {			
  		//执行触摸笔检测
		GUI_TOUCH_Exec();	
        //获取触摸笔状态值
		GUI_TOUCH_GetState(&State);
		//是否按下
		if(State.Pressed){
			//打印触摸笔坐标信息
			GUI_DispStringAt("X:",0,0);
			GUI_DispDecAt(State.x,32,0,4);
			GUI_DispStringAt("Y:",0,24);
			GUI_DispDecAt(State.y,32,24,4);			
		}
		osDelay(10);
	}
  /* USER CODE END Touch_Task */
}

测试结果

当触摸笔按下时,显示X(0-480)和Y(0-272)坐标信息

三、emWin开发环境搭建

开发环境压缩包请自提:链接:https://pan.baidu.com/s/12lO9r7CG43mPEpcIbQ3fug?pwd=6pqm

提取码:6pqm

开发环境介绍

CodeBlocks
GUIBuilder
Simulation(模拟器)

emwin Simulation (模拟器)

仿真模拟器是在window开发环境下的C工程,可以通过VC6或者codeblockd IDE环境下进行开发仿真

  1. 下载地址
    https://www.segger.com/downloads/emwin/

根据自己使用的STemwin库,进行下载,我们采用V5.44版本

  1. 添加工程到codeblockd
    1. 打开外部工程
    2. 选择.cbp文件
  1. 添加工程到codeblockd
    1. 点击编译运行按钮
    2. 生成模拟器

GUIBuilder 制作界面

Created with Raphaël 2.3.0 放置窗口 放置按钮 放置文本 生成代码

界面添加到模拟器

修改模拟器原始工程

  1. 把无用的代码移除工程
  1. 添加WindowDLG.c到工程中
    1. 首先复制WindowDLG.c到SeggerEval_WIN32_MSVC_MinGW_GUI_V544\Application目录下

    2. 添加WindowDLG.c到工程中

    3. 选择Application工程目录,新建mainTask.c

    4. 在mainTask.c添加代码

c 复制代码
#include "dialog.h"//包含window对话框 头文件
void MainTask(void)

{

    GUI_Init();                     //初始化emWin
    CreateWindow();                 //创建窗体,父窗体是桌面背景
    while(1) {GUI_Delay(20);}       //调用GUI_Delay函数延时20MS(最终目的是调用GUI_Exec()函数)

}

四、emWin运行原理分析

emWin使用说明书(第三点中压缩包包含)

emWin初始化

执行模型

单任务使用注意事项

c 复制代码
我们在操作系统下使用emwin,必须要创建一个任务,用来调用emWin函数,并且官方说,此任务的优先级配置为最低
    
 emWin函数指的是什么???

时间调度


指针输入设备

五、emWin应用编程方法

GUIBuilder

对话框

资源表

c 复制代码
资源:我们这个对话框内部的小工具(小窗口)------- window 按钮 文本 图片

窗口管理器

窗口消息

窗口对象

物联网智慧教室项目(五):人机交互功能开发(模拟器)

一、界面构思

(一)原型分析

1、主界面

c 复制代码
主界面提供了,整个系统的交互接口,也就是说,我们都有哪些需要提供给用户展示的
1、数据展示功能
    1.1、温度
    1.2、湿度
    1.3、光照
2、控制操作功能
	2.1、灯光
    2.2、风扇
    2.3、报警
    
分析:
    对用户来说,需要点击相应的图标下,就可以进入相关的界面,很简单,我使用button小工具进行实现,其实就是在主界面创建6个button小工具

2、子界面

控制界面
c 复制代码
控制界面应该有什么?
1、界面的标识--- 要给用户指示这个界面有什么用
    使用text小工具就可以实现
2、控制操作
    开状态-当用户点击图标,灯泡点亮
    关状态-当用户点击图片,灯泡熄灭
3、返回主菜单
    当用户点击主菜单按钮,返回主界面
    
 控制和返回主界面都用button实现即可
    
数据界面
c 复制代码
控制界面应该有什么?
1、界面的标识--- 要给用户指示这个界面有什么用
    使用text小工具就可以实现
2、数据图标展示
 	显示一个图片就可以,用图片小工具
3、返回主菜单
    当用户点击主菜单按钮,返回主界面,用button小工具

(二)界面构思

1、主界面

2、传感器数据展示界面

3、控制设备界面

(三)素材设计

1、主界面素材

2、子界面素材

注意事项

c 复制代码
图片最终是用过代码,进行展示的
    我们必须针对图片命名时候,有规范,有要求
1、主界面命名
    MainAlarm
    MainLed
2、子界面
    SubAlarmOpen
    SubAlarmClose
    SubHum

二、交互设计

(一)窗口初始化

业务流程

Created with Raphaël 2.3.0 设置窗口背景色 设置按钮位图

窗口初始化函数接口

句柄的概念

c 复制代码
句柄:在C语言里面,它其实就是一个指针变量
WINDOW_SetBkColor
WM_GetDialogItem
BUTTON_SetBitmap

(二)按键处理

Created with Raphaël 2.3.0 子窗口消息通知 获取子窗口ID 获取子窗口消息 子窗口消息处理

按键处理接口

WM_NOTIFY_PARENT
按钮通知代码

(三)界面切换

Created with Raphaël 2.3.0 结束当前对话框 创建需要切换的对话框

界面切换接口
GUI_EndDialog
GUI_CreateDialogBox

三、GUIBuilder创建代码框架

GUIBuilder创建主界面

c 复制代码
1、创建window小工具
    1.1、设置window x y 尺寸 472 280
    1.2、设置背景色 2 33 79 RGB
2、创建6个button小工具
    2.1、设置button尺寸为 100 100 
    2.2、button布局
  /*
  { WINDOW_CreateIndirect, "", ID_WINDOW_0, 0, 0, 472, 280, 0, 0x0, 0 },
  { BUTTON_CreateIndirect, "", ID_BUTTON_0, 43, 30, 100, 100, 0, 0x0, 0 },
  { BUTTON_CreateIndirect, "", ID_BUTTON_1, 43, 150, 100, 100, 0, 0x0, 0 },
  { BUTTON_CreateIndirect, "", ID_BUTTON_2, 186, 150, 100, 100, 0, 0x0, 0 },
  { BUTTON_CreateIndirect, "", ID_BUTTON_3, 329, 150, 100, 100, 0, 0x0, 0 },
  { BUTTON_CreateIndirect, "", ID_BUTTON_4, 329, 30, 100, 100, 0, 0x0, 0 },
  { BUTTON_CreateIndirect, "", ID_BUTTON_5, 186, 30, 100, 100, 0, 0x0, 0 },
  */
添加代码到模拟器工程
修改窗口创建函数名称及文件名称
代码添加到工程
模拟器运行

创建温度展示界面

创建湿度展示界面

创建光照展示界面

创建风扇控制界面

创建LED控制界面

创建报警控制界面

四、BmpCvtST生成图片流

将主界面、子界面图片通过"位图转换工具"转换成c语言文件,然后复制到SeggerEval_WIN32_MSVC_MinGW_GUI_V544文件下的Application文件夹中。

位图转换工具及界面用到的图片素材:

链接:https://pan.baidu.com/s/1KnK-yzwmuKXJQRIgQmIMDQ?pwd=8hyn

提取码:8hyn

(一)主界面添加图片流

c 复制代码
// USER START (Optionally insert additional static data)
extern GUI_CONST_STORAGE GUI_BITMAP bmMainAlarm;
extern GUI_CONST_STORAGE GUI_BITMAP bmMainFan;
extern GUI_CONST_STORAGE GUI_BITMAP bmMainHum;
extern GUI_CONST_STORAGE GUI_BITMAP bmMainLed;
extern GUI_CONST_STORAGE GUI_BITMAP bmMainLight;
extern GUI_CONST_STORAGE GUI_BITMAP bmMainTemp;
// USER END
c 复制代码
    // USER START (Optionally insert additional code for further widget initialization)
    hItem = WM_GetDialogItem(pMsg->hWin, ID_BUTTON_0);
    BUTTON_SetBitmap(hItem, BUTTON_BI_UNPRESSED,&bmMainAlarm);
    hItem = WM_GetDialogItem(pMsg->hWin, ID_BUTTON_1);
    BUTTON_SetBitmap(hItem, BUTTON_BI_UNPRESSED,&bmMainFan);
    hItem = WM_GetDialogItem(pMsg->hWin, ID_BUTTON_2);
    BUTTON_SetBitmap(hItem, BUTTON_BI_UNPRESSED,&bmMainHum);
    hItem = WM_GetDialogItem(pMsg->hWin, ID_BUTTON_3);
    BUTTON_SetBitmap(hItem, BUTTON_BI_UNPRESSED,&bmMainLed);
    hItem = WM_GetDialogItem(pMsg->hWin, ID_BUTTON_4);
    BUTTON_SetBitmap(hItem, BUTTON_BI_UNPRESSED,&bmMainLight);
    hItem = WM_GetDialogItem(pMsg->hWin, ID_BUTTON_5);
    BUTTON_SetBitmap(hItem, BUTTON_BI_UNPRESSED,&bmMainTemp);
    // USER END

(二)子界面添加图片流

以HumDLG(湿度子界面)为例,其余五个子界面操作除了命名得修改一下,其余一致,如下:

c 复制代码
// USER START (Optionally insert additional static data)
extern GUI_CONST_STORAGE GUI_BITMAP bmSubHome;
extern GUI_CONST_STORAGE GUI_BITMAP bmSubHum;
// USER END
c 复制代码
    // USER START (Optionally insert additional code for further widget initialization)
    hItem = WM_GetDialogItem(pMsg->hWin, ID_BUTTON_0);
    BUTTON_SetBitmap(hItem, BUTTON_BI_UNPRESSED,&bmSubHome);
    // USER END

五、界面切换功能实现

功能优化

c 复制代码
设置子界面背景色为 8 20 44 16进制 2C 14 08
//TEXT_SetBkColor(hItem, GUI_MAKE_COLOR(0x002C1408));

(一)界面切换功能实现

1、主界面切换到子界面

MainDLG.c(以按键0 Alarm为例,其余五个按键操作一致):

c 复制代码
case ID_BUTTON_0: // Notifications sent by ''
      switch(NCode) {
      case WM_NOTIFICATION_CLICKED:
        // USER START (Optionally insert code for reacting on notification message)
        // USER END
        break;
      case WM_NOTIFICATION_RELEASED:
        // USER START (Optionally insert code for reacting on notification message)
        GUI_EndDialog(pMsg->hWin, 0);
        AlarmCreate();
        // USER END
        break;
      // USER START (Optionally insert additional code for further notification handling)
      // USER END
      }
      break;

2、子界面

a.控制设备(Alarm、Fan、Led,即需要控制开关的),以Alarm为例:

PS:注意是在按键1写入代码,不是按键0

c 复制代码
case ID_BUTTON_1: // Notifications sent by ''
      switch(NCode) {
      case WM_NOTIFICATION_CLICKED:
        // USER START (Optionally insert code for reacting on notification message)
        // USER END
        break;
      case WM_NOTIFICATION_RELEASED:
        // USER START (Optionally insert code for reacting on notification message)
        GUI_EndDialog(pMsg->hWin, 0);
        MainCreate();
        // USER END
        break;
      // USER START (Optionally insert additional code for further notification handling)
      // USER END
      }
      break;

b.传感器(Hum、Light、Temp),以Hum为例

c 复制代码
    case ID_BUTTON_0: // Notifications sent by ''
      switch(NCode) {
      case WM_NOTIFICATION_CLICKED:
        // USER START (Optionally insert code for reacting on notification message)
        // USER END
        break;
      case WM_NOTIFICATION_RELEASED:
        // USER START (Optionally insert code for reacting on notification message)
        GUI_EndDialog(pMsg->hWin, 0);
        MainCreate();
        // USER END
        break;
      // USER START (Optionally insert additional code for further notification handling)
      // USER END
      }

(二)控制操作图标切换

也就是实现Alarm、Fan、Led点击图片按钮可以实现开关图片的切换,以Alarm为例:
c 复制代码
// USER START (Optionally insert additional static data)
extern GUI_CONST_STORAGE GUI_BITMAP bmSubAlarmClose;
extern GUI_CONST_STORAGE GUI_BITMAP bmSubAlarmOpen;
extern GUI_CONST_STORAGE GUI_BITMAP bmSubHome;
static int status = 0;
// USER END
c 复制代码
    // USER START (Optionally insert additional code for further widget initialization)
    hItem = WM_GetDialogItem(pMsg->hWin, ID_BUTTON_0);
    if(status){
        BUTTON_SetBitmap(hItem, BUTTON_BI_UNPRESSED, &bmSubAlarmOpen);
    }else{
        BUTTON_SetBitmap(hItem, BUTTON_BI_UNPRESSED, &bmSubAlarmClose);
    }
    hItem = WM_GetDialogItem(pMsg->hWin, ID_BUTTON_1);
    BUTTON_SetBitmap(hItem, BUTTON_BI_UNPRESSED,&bmSubHome);
    // USER END

PS:在按键0中添加代码

c 复制代码
    case ID_BUTTON_0: // Notifications sent by ''
      switch(NCode) {
      case WM_NOTIFICATION_CLICKED:
        // USER START (Optionally insert code for reacting on notification message)
        // USER END
        break;
      case WM_NOTIFICATION_RELEASED:
        // USER START (Optionally insert code for reacting on notification message)
        status = !status;
        if(status){
            BUTTON_SetBitmap(pMsg->hWinSrc, BUTTON_BI_UNPRESSED, &bmSubAlarmOpen);

        }else{
            BUTTON_SetBitmap(pMsg->hWinSrc, BUTTON_BI_UNPRESSED, &bmSubAlarmClose);
        }
        // USER END
        break;
      // USER START (Optionally insert additional code for further notification handling)
      // USER END
      }
      break;

物联网智慧教室项目(六):人机交互功能开发(stm32)

一、中文字库生成原理

(一)点阵字体及字符编码

点阵字体

字符编码

ASC||
GB2312
Unicode
UTF-8
c 复制代码
只要在点阵显示之前,进行调用一次就可以,一般都是在我们GUI_INIT 之后

emWin创建中文字库

FontCvtST

基于windows字库创建emWin字库

c 复制代码
生成全部字库,需要占用35.2MB   STM32 flash也就是几百k

创建小型字库

(三)生成项目所需中文字库

复制代码
温度
湿
光照
报警
灯光
风扇
传感器
控制

二、实现界面中文显示

Created with Raphaël 2.3.0 使能UTF-8编码方案 添加字库到工程中 设置c源文件(需要显示中文)为UTF-8编码 程序中引用字库 text小工具选择字库 打印字符内容

三、代码移植到STM32(上)

Created with Raphaël 2.3.0 文件拷贝 MDK工程添加 编译

(一)MDK工程编码格式修改为utf-8

(二)需要创建include_dlg.h

h 复制代码
//在需要引用的地方进行头文件包含
#ifndef 	_DLG_H
#define 	_DLG_H
#include "DIALOG.h"

extern WM_HWIN TempCreate(void);

extern WM_HWIN MainCreate(void);

extern WM_HWIN LightCreate(void);

extern WM_HWIN LedCreate(void);

extern WM_HWIN HumCreate(void);

extern WM_HWIN FanCreate(void);

extern WM_HWIN AlarmCreate(void);
#endif

(三)在freeRTOS GUItask里调用 MainTask

c 复制代码
/**
* @brief Function implementing the GuiTask thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_Gui_Task */
void Gui_Task(void const * argument)
{
  /* USER CODE BEGIN Gui_Task */
  /* Infinite loop */
	MainTask();
  /* USER CODE END Gui_Task */
}

(四)在Touch任务里保留 Touch检测

c 复制代码
/* USER CODE BEGIN Header_Touch_Task */
/**
  * @brief  Function implementing the TouchTask thread.
  * @param  argument: Not used 
  * @retval None
  */
/* USER CODE END Header_Touch_Task */
void Touch_Task(void const * argument)
{
    
                 
//  /* init code for FATFS */
//  MX_FATFS_Init();

//  /* init code for LWIP */
//  MX_LWIP_Init();
  /* USER CODE BEGIN Touch_Task */
	printf("system is runing!\r\n");
  /* Infinite loop */
  for(;;)
  {
		//ִ触摸屏需要轮询检测,不然emWin没有办法触发事件
		GUI_TOUCH_Exec();	
		osDelay(20);
  }
  /* USER CODE END Touch_Task */
}

四、代码移植到STM32(下)

Created with Raphaël 2.3.0 烧录测试 控制实际的硬件 采集实际的传感器

测试出的bug

c 复制代码
bug1:当切换界面时,出现闪屏动作,体验感非常差
bug2:当频繁切换界面时,出现花屏或者白屏
在下一节进行解决

五、人机交互功能测试验证

(一)切换界面闪屏问题

使用存储设备

自动使能存储设备

使能存储设备API

频繁切换界面花屏问题

c 复制代码
/*********************************************************************
*
*       GUI_X_Config
*
* Purpose:
*   Called during the initialization process in order to set up the
*   available memory for the GUI.
*/
void GUI_X_Config(void) {
  //
  // 32 bit aligned memory area
  //
	volatile U32* aMemory = (volatile U32*)(SRAM_BANK_ADDR);
  //
  // Assign memory to emWin
  GUI_ALLOC_AssignMemory((void *)aMemory, GUI_NUMBYTES);
  GUI_ALLOC_SetAvBlockSize(GUI_BLOCKSUZE);
  //
  // Set default font
  //
  GUI_SetDefaultFont(GUI_FONT_32_1);
}

物联网智慧教室项目(七):网页交互功能开发(前端)

c 复制代码
源码已通过百度云上传,推荐使用谷歌浏览器打开,使用开发者模式浏览学习,也可自行修改样式。
链接:https://pan.baidu.com/s/1gYXIK8YYXrwZbwsCJSS-MQ?pwd=gbls 
提取码:gbls

一、界面构思

(一)原型分析

(二)界面构思

1、标题栏

布局
标题设计
c 复制代码
大标题:智慧教室管理系统
小标题:灯光,报警,风扇,温度,湿度,光照
样式设计
  • 背景色配置
  • 字体配置
    • 字体大小,格式,颜色

2、导航栏

布局
导航栏标题
c 复制代码
提示用户用于智能管理使用的,用图片进行设计
导航标签
6个导航标签
c 复制代码
3 个用于传感器
3 个用于控制
每一个标签,都有图标和文件进行展示
6个导航展示界面
c 复制代码
3 个传感器
    显示传感器名称和传感器值
3 个控制
    显示控制名称和控制按钮
样式设计
c 复制代码
当鼠标每点击一个导航标签,我都需要进行导航界面展示
前后台交互设计
  • 定时刷新传感器数据值(当用户点击传感器标签,需要把传感器值实时的展示给用户)
  • 控制下发(当用户点击控制标签,需要根据用户下发的命令进行控制实际的硬件,并且在主题栏进行动态展示)

3、主题栏

布局
  • 主题界面

    c 复制代码
    给用户展示一个智慧教室的图片
  • 风扇图标

    c 复制代码
    当用户开关风扇时,这个图标要进行动态效果
  • 报警器图标

    c 复制代码
    当用户开关报警器时,这个图标要进行动态效果
  • 灯光控制

    c 复制代码
    当用户开关灯泡时,我们教室的主界面要展示动态效果

(三)素材设计

1、导航栏素材

2、主题栏素材

二、前端开发技术

(一)开发工具

c 复制代码
https://code.visualstudio.com/

(二)前端开发基本技能

html

CSS

JavaScript

jQuery

c 复制代码
http://jquery.cuishifeng.cn/index.html

(三)基于框架开发

BootStrap

c 复制代码
https://www.bootcss.com/

Vue

c 复制代码
https://cn.vuejs.org/

react

c 复制代码
https://react.docschina.org/

项目用到的技术

三、Web页面布局

(一)标题栏布局

html 复制代码
    <div class="header">
        <div class="content">
            <h1>智慧教室管理系统
                <small>温度、湿度、风扇、报警,光照
                </small>
            </h1>
        </div>
    </div>

(二)导航栏布局

html 复制代码
<div class="body-left">
    <img src="images/left-title.png" style="margin:26px 0;">
    <div>
        <ul class="left-nav">
            <li class="line active">
                <a href="#title1" data-toggle="tab">
                    <img src="images/tubiao01.png" width="40px">温度传感器
                </a>
            </li>
            <li class="line">
                <a href="#title2" data-toggle="tab">
                    <img src="images/tubiao02.png" width="40px">湿度传感器
                </a>
            </li>
            <li class="line">
                <a href="#title3" data-toggle="tab">
                    <img src="images/tubiao03.png" width="40px">光照传感器
                </a>
            </li>
            <li class="line">
                <a href="#title4" data-toggle="tab">
                    <img src="images/tubiao04.png" width="40px">LED
                </a>
            </li>
            <li class="line">
                <a href="#title5" data-toggle="tab">
                    <img src="images/tubiao05.png" width="38px">风扇
                </a>
            </li>
            <li class="line">
                <a href="#title6" data-toggle="tab">
                    <img src="images/tubiao06.png" width="40px">报警器
                </a>
            </li>
        </ul>
        <div class="content">
            <div class="box fade in active" id="title1">
                <p>温度值<br /><span>
                        <lable id="temperature"></lable>
                    </span></p>
            </div>
            <div class="box fade" id="title2">
                <p>湿度值<br /><span>
                        <lable id="humidity"></lable>
                    </span></p>
            </div>
            <div class="box fade" id="title3">
                <p>光照值<br /><span>
                        <lable id="light"></lable>
                    </span></p>
            </div>
            <div class="box fade" id="title4">
                <h3>开关</h3>
                <img id="button01" src="images/an-off.png" onclick="anniu01()" />
            </div>
            <div class="box fade" id="title5">
                <h3>开关</h3>
                <img id="button02" src="images/an-off.png" onclick="anniu02()" />
            </div>
            <div class="box fade" id="title6">
                <h3>开关</h3>
                <img id="button03" src="images/an-off.png" onclick="anniu03()" />
            </div>
        </div>
    </div>

(三)背景栏布局

html 复制代码
<div class="body-right">
    <img id="sgbj" class="sgbj" src="images/sgbj-off.png" />
    <img id="fan" class="fan" src="images/fan-off.png" />
    <img id="bg" src="images/sys-bg.jpg" />
    <audio id="alarm" src="music/alarm.mp3"></audio>
</div>

四、Web页面样式设计

(一)外部文件引入

html 复制代码
<link rel="stylesheet" type="text/css" href="css/bootstrap.css" />
<script src="js/jquery-1.11.0.min.js" type="text/javascript"></script>
<script src="js/bootstrap.min.js" type="text/javascript"></script>

(二)标题栏样式设计

css 复制代码
        h1 {
            padding: 0; 
            margin: 0 0 0 15px;
            font-size: 24px;
            color: #fff;
            line-height: 3em;
            font-weight: 100;
        }

        h1 small {
            font-size: 12px;
            color: #fff;
            margin-left: 10px;
        }

        .content {
            width: 1200px;
            margin: 0 auto;
        }

(三)导航栏样式设计

css 复制代码
.body-left {
    float: left;
    width: 311px;
    margin-right: 10px;
    text-align: center;
    color: #fff;
}

.body-left .left-nav {
    float: left;
    width: 180px;
    list-style: none;
    margin: 0;
    padding: 0;
    text-align: left;
}
.body-left .left-nav .line {
    background: #010146;
}

.body-left .left-nav .active {
    background: #020220;
}

.body-left .left-nav a {
    position: relative;
    display: block;
    line-height: 63px;
    text-decoration: none;
    color: #fff;
    padding-left: 60px;
}

.body-left .left-nav a img {
    position: absolute;
    top: 7px;
    left: 10px;
}

.body-left .content {
    float: left;
    width: 131px;
    background: #020220;
    height: 378px;
}

.body-left .content .box {
    position: absolute;
    margin: 20px 10px;
    width: 111px;
}

.body-left .content .box h3 {
    font-size: 16px;
    padding: 0;
    margin: 20px 0;
}

.body-left .content .box p {
    padding: 0;
    margin: 10px 0;
}

.body-left .content .box p span {
    font-size: 20px;
    color: #FFFF00;
}

.body-left .content .active {
    z-index: 10;
}

#button {
    width: 80px;
}

(四)主题栏样式设计

css 复制代码
.body-right {
    position: relative;
    float: left;
    width: 879px;
}

.body-right .sgbj {
    position: absolute;
    top: 135px;
    left: 12px;
    width: 50px;
}

.body-right .fan {
    position: absolute;
    top: 186px;
    left: 473px;
    width: 40px;
}

(五)样式效果设计

1、bootstrap教程

url 复制代码
https://www.runoob.com/bootstrap/bootstrap-tab-plugin.html

2、导航标签样式设计

html 复制代码
<li class="line active">
    <a href="#title1" data-toggle="tab">
        <img src="images/tubiao01.png" width="40px">温度传感器
    </a>
</li>

3、导航标签内容设计

html 复制代码
<div class="box fade in active" id="title1">
    <p>温度值<br /><span>
            <lable id="temperature"></lable>
        </span></p>
</div>

五、前后台交互设计

(一)传感器数据获取

1、传感器数据初始化

js 复制代码
$("#temperature").text("25℃");
$("#humidity").text("45%");
$("#light").text("1233lux");

2、ajax后台数据获取

js 复制代码
//获取传感器数据
function ReadData() {
    $.ajax({
        url: "/DATA/Sensor",
        type: "get",
        cache: false,
        timeout: 2000,
        dataType: "json",
        success: function (data) {
            console.log(data);
            $("#temperature").text(data.temperature + "℃");
            $("#humidity").text(data.humidity + "%");
            $("#light").text(data.light + "lux");
        }
    })
}
js 复制代码
        //命令交互
        function SendCmd(type, cmd) {
            $.ajax({
                url: "/CMD/" + type,
                type: "post",
                cache: false,
                timeout: 2000,
                data: cmd,
                success: function (data) {
                    console.log(data);
                }
            })


        }

(二)定时刷新传感器数据

js 复制代码
setInterval(ReadData, 5000);

(三)按钮命令交互

1、灯光控制

js 复制代码
function anniu01() {
    var anNiu = $("#button01")[0];
    var bG = $("#bg")[0];
    if (LightStatus) {
        //发送关闭灯光命令
        SendCmd("Light", "Off");
        anNiu.src = ("images/an-off.png");
        bG.src = ("images/sys-bg-off.jpg");
        bG = $("#fan")[0];
        if (FanStatus) {
            bG.src = ("images/bg-fan-on.png");
        }
        else {
            bG.src = ("images/bg-fan-off.png");
        }
        LightStatus = false;
    } else {
        //发送开启灯光命令
        SendCmd("Light", "On");
        anNiu.src = ("images/an-on.png");
        bG.src = ("images/sys-bg.jpg");
        bG = $("#fan")[0];
        if (FanStatus) {
            bG.src = ("images/fan-on.png");
        }
        else {
            bG.src = ("images/fan-off.png");
        }
        LightStatus = true;

    }
}

2、风扇控制

js 复制代码
function anniu02() {
    var anNiu = $("#button02")[0];
    var bG = $("#fan")[0];
    if (FanStatus) {
        //发送关闭风扇命令
        SendCmd("Fan", "Off");
        anNiu.src = ("images/an-off.png");
        if (LightStatus) {
            bG.src = ("images/fan-off.png");
        }
        else {
            bG.src = ("images/bg-fan-off.png");
        }

        FanStatus = false;
    }
    else {
        //发送开启风扇命令
        SendCmd("Fan", "On");
        anNiu.src = ("images/an-on.png");
        if (LightStatus) {
            bG.src = ("images/fan-on.png");
        }
        else {
            bG.src = ("images/bg-fan-on.png");
        }

        FanStatus = true;
    }
}

3、报警控制

js 复制代码
function anniu03() {
    var anNiu = $("#button03")[0];
    var bG = $("#sgbj")[0];
    var AS = $("#alarm")[0];
    if (AlarmStatus) {
        //发送关闭风扇命令
        SendCmd("Alarm", "Off");
        anNiu.src = ("images/an-off.png");
        bG.src = ("images/sgbj-off.png");
        AS.removeEventListener('ended', loop, false);
        AS.pause();
        AlarmStatus = false;
    }
    else {
        //发送开启风扇命令
        SendCmd("Alarm", "On");
        anNiu.src = ("images/an-on.png");
        bG.src = ("images/sgbj-on.gif");
        AS.addEventListener('ended', loop, false);
        AS.play();
        AlarmStatus = true;
    }
}

物联网智慧教室项目(八):网页web服务器功能开发(stm32)

一、WebServer功能设计

(一)WebServer需要做什么

当用户访问网址(url)时,加载网页文件

当用户点击控制按钮,传感器定时刷新与服务器进行交互

(二)EasyWebSvr工具介绍

菜单界面

设置界面

运行日志

(三)EasyWebSvr搭建Web服务器

配置服务器主目录

浏览器输入服务器地址

url 复制代码
127.0.0.1/index.html

分析Web服务器运行日志

(四)WebServer功能设计

文件请求响应

Browser WebServer FileSystem GET /index.html open(index.html) index.html Response index.html Browser WebServer FileSystem

传感器数据请求响应

Browser WebServer SensorTask GET /DATA/Sensor Read SensorData SensorData Response SensorData Browser WebServer SensorTask

命令请求响应

Browser WebServer CMDTask POST /CMD/Light Send Cmd Cmd Status Response Cmd Status Browser WebServer CMDTask

二、WebServer主线程实现

(一)WebServer代码移植

文件移植

c 复制代码
智慧教室项目实战\day08\03-WebServer移植文件

把网页文件拷贝到SD卡中

c 复制代码
1.拷贝文件到SD卡"根目录下"
2.开发板断电插入SD卡
3.烧录程序看现象(浏览器输入192.168.1.7)

访问STM32服务器,网页图片加载需要多次刷新问题

c 复制代码
1、我们webserver是一个单线程任务,http属于短链接//http没有记忆功能,在一次socket通信中能获取多少数据,就只能获取多少数据
2、但是浏览器有缓存,我们使用时只需要多刷新几次就可以了
3、也可以通过在前端增加一些js代码(循环加载前端资源(文件))
4、STM32内存太小了,没有办法做长连接,短连接模式可以实现多个客户端连接

(二)WebServer主线程实现

http_server_socket_thread

c 复制代码
/**
  * @brief  http server thread 
  * @param arg: pointer on argument(not used here) 
  * @retval None
  */
void http_server_socket_thread(void *p_arg)
{
	int sock, newconn, size;
	struct sockaddr_in address, remotehost;

	/* create a TCP socket */
	if ((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1)
	{
		printf("http_server can not create socket");
		return;
	}

	/* bind to port 80 at any interface */
	address.sin_family = AF_INET;
	address.sin_port = htons(80);
	address.sin_addr.s_addr = INADDR_ANY;

	if (bind(sock, (struct sockaddr *)&address, sizeof(address)) == -1)
	{
		printf("http_server can not bind socket");
		return;
	}

	/* listen for incoming connections (TCP listen backlog = 5) */
	listen(sock, 5);

	size = sizeof(remotehost);
/*先不关心,但是很重要
	printf("\r\n--------------Web_Server_Task----------------\r\n");
	WEB_Service_Registration(&CONTROL_LIGHT_CMD_POST);
	WEB_Service_Registration(&CONTROL_FAN_CMD_POST);
	WEB_Service_Registration(&CONTROL_ALARM_CMD_POST);
*/
// 循环等待客户端的接入
	while (1)
	{
        //等待客户端接入
		newconn = accept(sock, (struct sockaddr *)&remotehost, (socklen_t *)&size);
        //针对客户端做服务处理  请求-->响应
		http_server_serve(newconn);
	}
}

http_server_serve

c 复制代码
/**
  * @brief serve tcp connection  
  * @param conn: connection socket 
  * @retval None
  */
void http_server_serve(int conn)
{
	int ret;
	/* Read in the request */
	ret = read(conn, (unsigned char *)Request_Buf, 1500);

	if (ret < 0)
		return;
	else
	{
         //把请求内容最后一个字节填充\0,以后直接用字符串解析
		*(Request_Buf + ret) = 0;
#ifdef WEB_DEBUG
		printf("\r\nWEB服务器,接收请求,内容:\r\n%s\r\n", (const char *)Request_Buf);
#endif
        //请求响应代码
		Respond_Http_Request(conn, (char *)Request_Buf);
	}
	//关闭socket 这就一个短链接的实现
	close(conn);
}

(三)http解析业务流程

请求解析 方法类型 ERROR GET POST 错误响应 GET类型 数据读取响应 文件读取响应 POST类型 命令下发响应 文件写入响应

三、Http文件请求响应

(一)数据结构

http文件请求响应首部封装

c 复制代码
#define HOMEPAGE_DEFAULT "index.html"

#define INVALID_CMD "HTTP/1.0 200 OK\r\nContent-Type: text/plain\r\nConnection: close\r\nContent-Length: 16\r\n\r\ninvalid cmd!"
#define POST_REQUEST_OK "HTTP/1.0 200 OK\r\nServer: WFM-Control\r\nConnection: close\r\nContent-Type: text/css\r\nCache-control: no-cache\r\nExpires: Thu, 15 Apr 2000 20:00:00 GMT\r\nContent-Length: 18\r\n\r\nPOST Successfully!"
#define POST_REQUEST_FAIL "HTTP/1.0 200 OK\r\nServer: WFM-Control\r\nConnection: close\r\nContent-Type: text/css\r\nCache-control: no-cache\r\nExpires: Thu, 15 Apr 2000 20:00:00 GMT\r\nContent-Length: 13\r\n\r\nPOST Failure!"
#define RETURN_cmd_OK "HTTP/1.0 200 OK\r\nContent-Type: text/plain\r\nCache-Control: no-cache, no-store, max-age=0\r\nExpires: 1L\r\nConnection: close\r\nContent-Length: "
/* html文件请求错误响应 */
const char ERROR_HTML_PAGE[] = "HTTP/1.0 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\nContent-Length: 78\r\n\r\n<HTML>\r\n<BODY>\r\nSorry, the page you requested was not found.\r\n</BODY>\r\n</HTML>\r\n\0";
/* 数据命令请求错误响应 */
const char ERROR_REQUEST_PAGE[] = "HTTP/1.0 500 Pafe Not Found\r\nConnection: close\r\nContent-Type: text/html\r\nContent-Length: 50\r\n\r\n<HTML>\r\n<BODY>\r\nInvalid request.\r\n</BODY>\r\n</HTML>\r\n\0";
/* Response header for HTML*/
const char RES_HTMLHEAD_OK[] = "HTTP/1.0 200 OK\r\nServer: WFM-Control\r\nConnection: close\r\nContent-Type: text/html\r\nCache-control: max-age=315360000000\r\nExpires: Thu, 15 Apr 2100 20:00:00 GMT\r\nContent-Length: ";
/* Response head for TEXT */
const char RES_TEXTHEAD_OK[] = "HTTP/1.0 200 OK\r\nServer: WFM-Control\r\nConnection: close\r\nContent-Type: text/plain\r\nCache-control: max-age=315360000000\r\nExpires: Thu, 15 Apr 2100 20:00:00 GMT\r\nContent-Length: ";
/* Response head for GIF */
const char RES_GIFHEAD_OK[] = "HTTP/1.0 200 OK\r\nServer: WFM-Control\r\nConnection: close\r\nContent-Type: image/gif\r\nCache-control: max-age=315360000000\r\nExpires: Thu, 15 Apr 2100 20:00:00 GMT\r\nContent-Length: ";
/* Response head for JPEG */
const char RES_JPEGHEAD_OK[] = "HTTP/1.0 200 OK\r\nServer: WFM-Control\r\nConnection: close\r\nContent-Type: image/jpeg\r\nCache-control: max-age=315360000000\r\nExpires: Thu, 15 Apr 2100 20:00:00 GMT\r\nContent-Length: ";
/* Response head for MPEG */
const char RES_PNGHEAD_OK[] = "HTTP/1.0 200 OK\r\nServer: WFM-Control\r\nConnection: close\r\nContent-Type: image/png\r\nCache-control: max-age=315360000000\r\nExpires: Thu, 15 Apr 2100 20:00:00 GMT\r\nContent-Length: ";
/* Response head for MP3 */
const char RES_MP3HEAD_OK[] = "HTTP/1.0 200 OK\r\nServer: WFM-Control\r\nConnection: close\r\nContent-Type: audio/mpeg\r\nContent-Range: bytes 0-40123/40124\r\nCache-control: max-age=315360000000\r\nExpires: Thu, 15 Apr 2100 20:00:00 GMT\r\nContent-Length: ";
/* Response head for JS */
const char RES_JSHEAD_OK[] = "HTTP/1.0 200 OK\r\nServer: WFM-Control\r\nConnection: close\r\ncontent-type:application/x-javascript\r\nCache-control: max-age=315360000000\r\nExpires: Thu, 15 Apr 2100 20:00:00 GMT\r\nContent-Length: ";
/* Response head for ICO */
const char RES_ICOHEAD_OK[] = "HTTP/1.0 200 OK\r\nServer: WFM-Control\r\nConnection: close\r\nContent-Type: image/x-icon\r\nCache-control: max-age=315360000000\r\nExpires: Thu, 15 Apr 2100 20:00:00 GMT\r\nContent-Length: ";
/* Response head for CSS */
const char RES_CSSHEAD_OK[] = "HTTP/1.0 200 OK\r\nServer: WFM-Control\r\nConnection: close\r\nContent-Type: text/css\r\nCache-control: max-age=315360000000\r\nExpires: Thu, 15 Apr 2100 20:00:00 GMT\r\nContent-Length: ";
/* Response head for APP */
const char RES_APP_OK[] = "HTTP/1.0 200 OK\r\nServer: WFM-Control\r\nConnection: close\r\nContent-Type: application/octet-stream\r\nCache-control: max-age=315360000000\r\nExpires: Thu, 15 Apr 2100 20:00:00 GMT\r\nContent-Length: ";

http消息结构体

c 复制代码
typedef struct
{
	char Method;				 //请求方法: GET 、HEAD 、POST
	char *URL;					 //URL
	char FileType;				 //文件类型
	char *Post_Data;			 //POST数据
	unsigned int Content_Length; //POST数据的长度
} Http_Request_MSG_Type;		 //定义HTTP请求报文消息结构体

(二)Respond_Http_Request

c 复制代码
/*******************************************************************************
* 函数名称:  void Respond_Http_Request(SOCKET ch ,char* Request_Msg)
* 函数说明: 响应HTTP请求
* 输入参数: socket 端口:ch ;HTTP请求数据包指针	:Request_Msg
* 返回参数: 无
*******************************************************************************/
void Respond_Http_Request(char ch, char *Request_Msg)
{
    //创建http解析结构体
	Http_Request_MSG_Type Http_Request_MSG;
    //定义两个指针,用于解析数据记录跟踪
	char *thisstart = NULL;
	char *nextstart = NULL;
    //缓冲数据指针
	char *buf;
    //缓存数据长度
	int length = 0;
    //进行数据解析,解析完毕后,会填充Http_Request_MSG
	Parse_Http_Request(Request_Msg, &Http_Request_MSG); //解析HTTP请求类型
	switch (Http_Request_MSG.Method)
	{
	case METHOD_ERR:
         //把错误响应返回
		write(ch, (const unsigned char *)ERROR_REQUEST_PAGE, sizeof(ERROR_REQUEST_PAGE));
		break;
	case METHOD_HEAD:
	case METHOD_GET:
		if (strstr((const char *)Http_Request_MSG.URL, "/DATA/")) //判断是否是通讯指令,并解析指令帧
		{
			//传感器数据请求响应
		}
		else
		{
            //响应文件请求
			Send_Response_File(ch, &Http_Request_MSG); //发送请求的文件
		}
		break;
	case METHOD_POST:
		//post响应
	}	
}

(三)Parse_Http_Request

c 复制代码
/*******************************************************************************
* 函数名称:  void Parse_Http_Request(char * Request_Msg ,Http_Request_MSG_Type *Http_Request_MSG)
* 函数说明: 解析HTTP请求
* 输入参数: 请求数据包指针:Request_Msg ;  请求信息类型 :Http_Request_MSG
* 返回参数: 无
*******************************************************************************/
void Parse_Http_Request(char *Request_Msg, Http_Request_MSG_Type *Http_Request_MSG)
{
	char *thisstart = NULL;
	char *nextstart = NULL;

	thisstart = strtok_r(Request_Msg, " ", &nextstart);
	if (thisstart == NULL)
	{
		Http_Request_MSG->Method = METHOD_ERR;
		Http_Request_MSG->URL = NULL;
		return;
	}
	//解析请求所用的方法:GET,HEAD,POST
	if (!strcmp(thisstart, "GET") || !strcmp(thisstart, "get"))
	{
		Http_Request_MSG->Method = METHOD_GET;
	}
	else if (!strcmp(thisstart, "HEAD") || !strcmp(thisstart, "head"))
	{
		Http_Request_MSG->Method = METHOD_HEAD;
	}
	else if (!strcmp(thisstart, "POST") || !strcmp(thisstart, "post"))
	{
		Http_Request_MSG->Method = METHOD_POST;
	}
	else
	{
		Http_Request_MSG->Method = METHOD_ERR;
		Http_Request_MSG->URL = NULL;
		return;
	}

	if (nextstart == NULL)
	{
		Http_Request_MSG->Method = METHOD_ERR;
		Http_Request_MSG->URL = NULL;
		return;
	}

	Http_Request_MSG->URL = strtok_r(NULL, " ?", &nextstart);				 //解析URL
	if (Http_Request_MSG->URL[0] == '/' && Http_Request_MSG->URL[1] == '\0') //如果url仅是一个"/",则默认为主页
	{
		Http_Request_MSG->URL = HOMEPAGE_DEFAULT; //设置默认页
	}
	Http_Request_MSG->Post_Data = nextstart; //保存下一字符串指针
}

(四)Send_Response_File

c 复制代码
/*******************************************************************************
* 函数名称:  void Send_Response_File(SOCKET ch,Http_Request_MSG_Type *Http_Request_MSG) 
* 函数说明: 回应请求的文件
* 输入参数: SOCKET 通道号  ,Http_Request_MSG_Type  文件类型信息
* 返回参数: 无
*******************************************************************************/
void Send_Response_File(char ch, Http_Request_MSG_Type *Http_Request_MSG)
{
	FRESULT res;
	FIL *f;
	unsigned int bytes_ret;
	unsigned char *buf;
	unsigned char *buf1;
	uint32_t fSize;
	
	f = (FIL *)pvPortMalloc(sizeof(FIL));		//开辟内存空间
	buf = (unsigned char *)pvPortMalloc(1500);	//开辟内存空间
	buf1 = (unsigned char *)pvPortMalloc(1500); //开辟内存空间
	if (f == NULL || buf == NULL || buf1 == NULL)
	{
		printf("内存分配失败\r\n");
		vPortFree(f);			 //释放内存空间
		vPortFree(buf);			 //释放内存空间
		vPortFree(buf1);		 //释放内存空间
		
		write(ch, (const unsigned char *)ERROR_REQUEST_PAGE, sizeof(ERROR_REQUEST_PAGE));
		return;
	}
    //打开文件
	res = f_open(f, Http_Request_MSG->URL, FA_OPEN_EXISTING | FA_READ);
    //获取文件大小
	fSize = f_size(f);
	printf("http file size is %d\r\n", fSize);
	if (res == FR_OK)
	{
		Http_Request_MSG->FileType = Parse_URL_File_Type(Http_Request_MSG->URL); //分析请求URL中包含的文件的文件类型
		Make_http_response_head((char *)buf, Http_Request_MSG->FileType, fSize); //生成HTTP报文
        //响应首部
		write(ch, (const unsigned char *)buf, strlen((const char *)buf));
        //响应主题------- 文件
		while (1)
		{
            //读取文件
			res = f_read(f, buf, 1500, &bytes_ret);
            //读取文件错误
			if (res != FR_OK)
			{
				printf("读取文件失败!文件名:%s,错误代码:0x%02x 文件大小:%d\r\n", (const char *)Http_Request_MSG->URL, res, bytes_ret);
				SD_initialize(0);
				break;
			}
            //读取文件内容为空
			if (bytes_ret == 0)
				break;
            //读取文件正确,写回socket
			write(ch, (const unsigned char *)buf, bytes_ret);
            //继续读取文件
			res = f_read(f, buf1, 1500, &bytes_ret);
			if (res != FR_OK)
			{
				printf("读取文件失败!文件名:%s,错误代码:0x%02x 文件大小:%d\r\n", (const char *)Http_Request_MSG->URL, res, bytes_ret);
				SD_initialize(0);
				break;
			}
			if (bytes_ret == 0)
				break;
            //读取文件正确写回
			write(ch, (const unsigned char *)buf1, bytes_ret);
		}
        //关闭文件
		f_close(f);
		vPortFree(f);			 //释放内存空间
		vPortFree(buf);			 //释放内存空间
		vPortFree(buf1);		 //释放内存空间
		
	}
	else //文件打开错误
	{
		f_close(f);
		vPortFree(f);			 //释放内存空间
		vPortFree(buf);			 //释放内存空间
		vPortFree(buf1);		 //释放内存空间
		//写入错误请求响应
		write(ch, (const unsigned char *)ERROR_REQUEST_PAGE, sizeof(ERROR_REQUEST_PAGE));
		printf("打开文件失败!文件名:%s,错误代码:0x%02x\r\n", (const char *)Http_Request_MSG->URL, res);
		SD_initialize(0);
	}
}

四、前后台交互方法设计

(一)数据结构

c 复制代码
typedef struct Web_s{
	struct  Web_s 	*next;   //单链表节点 
	const char      *cmd;    //get&post具体消息内容存放位置
	void            (*function)(void *,void *);//不同响应的处理方法
} WEB_Server_Struct;

(二)交互数据结构封装

字符串封装

c 复制代码
const char Sensor[] = "Sensor";	// get方法内消息 /DATA/Sensor 用来解析此请求
const char Light[] = "Light";//post方法内消息 /CMD/Light 用来解析此请求
const char Fan[] = "Fan";//post方法内消息 /CMD/Fan 用来解析此请求
const char Alarm[] = "Alarm";//post方法内消息 /CMD/Alarm 用来解析此请求
const char On[] = "On";//post方法内消息 /CMD/xxx 消息内容为开启
const char Off[] = "Off";//post方法内消息 /CMD/xxx 消息内容为关闭

数据节点封装

c 复制代码
//我们项目中需要4中交互,传感器数据获取,开关灯,风扇,报警器,定义下面结构体
WEB_Server_Struct SENSOR_WEB_DATA_GET = {NULL, Sensor, Get_SensorValue};
WEB_Server_Struct CONTROL_LIGHT_CMD_POST = {NULL, Light, Post_Cmd_Light};
WEB_Server_Struct CONTROL_FAN_CMD_POST = {NULL, Fan, Post_Cmd_Fan};
WEB_Server_Struct CONTROL_ALARM_CMD_POST = {NULL, Alarm, Post_Cmd_Alarm};

交互函数封装

c 复制代码
/****************************************************************************************************
 函数原型:void Get_SensorValue(void *buffer,void *value)
 入口参数:发送缓冲区指针,设定值指针
 出口参数:无
 函数功能:登录验证
 ****************************************************************************************************/
static void Get_SensorValue(void *buffer, void *value)
{
	//把传感器数据填充到我们的buffer里面,之后进行响应就ok了
	sprintf(buffer, "{\"temperature\":\"%d\",\"humidity\":\"%d\",\"light\":\"10021.1\"}", SensorData[0], SensorData[1]);
}

/****************************************************************************************************
 函数原型:void Post_Cmd_Light(void *buffer,void *value)
 入口参数:发送缓冲区指针,设定值指针
 出口参数:无
 函数功能:登录验证
 ****************************************************************************************************/
static void Post_Cmd_Light(void *buffer, void *value)
{
	//判断value是on还是off
	if (strstr(value, On))
	{   
         //响应状态为on  Status:On
		sprintf(buffer, "{\"Status\":\"On\"}");
		HAL_GPIO_WritePin(GPIOF, D6_Pin | D7_Pin | D8_Pin | D9_Pin, GPIO_PIN_RESET);
	}
	else if (strstr(value, Off))
	{	
          //响应状态为Off Status:Off
		sprintf(buffer, "{\"Status\":\"Off\"}");
		HAL_GPIO_WritePin(GPIOF, D6_Pin | D7_Pin | D8_Pin | D9_Pin, GPIO_PIN_SET);
	}
	else
	{
         //响应错误
		sprintf(buffer, "{\"Status\":\"Error\"}");
	}
}

/****************************************************************************************************
 函数原型:void Post_Cmd_Fan(void *buffer,void *value)
 入口参数:发送缓冲区指针,设定值指针
 出口参数:无
 函数功能:登录验证
 ****************************************************************************************************/
static void Post_Cmd_Fan(void *buffer, void *value)
{
	if (strstr(value, On))
	{
		sprintf(buffer, "{\"Status\":\"On\"}");
        //风扇控制  ----- 后面zigbee项目讲解
		FanControl(0x01);
	}
	else if (strstr(value, Off))
	{
		sprintf(buffer, "{\"Status\":\"Off\"}");
		FanControl(0x0);
	}
	else
	{
		sprintf(buffer, "{\"Status\":\"Error\"}");
	}
}

/****************************************************************************************************
 函数原型:void Post_Cmd_Alarm(void *buffer,void *value)
 入口参数:发送缓冲区指针,设定值指针
 出口参数:无
 函数功能:登录验证
 ****************************************************************************************************/
static void Post_Cmd_Alarm(void *buffer, void *value)
{
	if (strstr(value, On))
	{
		sprintf(buffer, "{\"Status\":\"On\"}");
		HAL_GPIO_WritePin(BUZ_GPIO_Port, BUZ_Pin, GPIO_PIN_SET);
	}
	else if (strstr(value, Off))
	{
		sprintf(buffer, "{\"Status\":\"Off\"}");
		HAL_GPIO_WritePin(BUZ_GPIO_Port, BUZ_Pin, GPIO_PIN_RESET);
	}
	else
	{
		sprintf(buffer, "{\"Status\":\"Error\"}");
	}
}

(三)交互数据结构处理

c 复制代码
/*******************************************************************************
* 函数名称:  void WEB_Service_Registration(WEB_Server_Struct *next)
* 函数说明: WEB数据服务注册,与应用程序间的映射建立。
* 输入参数: 相应应用程序的链表类型指针
* 返回参数: 无
*******************************************************************************/
void WEB_Service_Registration(WEB_Server_Struct *next)
{
    //首先获取头结点
	WEB_Server_Struct *f = WEB_Registry_Head;
    //传入的节点下一个指向空
	next->next = NULL;
    //遍历找到空节点位置
	while (f->next != NULL)
	{
		f = f->next;
	}
    //把节点插入到链表中
	f->next = next;
}

/*******************************************************************************
* 函数名称:  char Search_match_the_analytical_method(const char *cmd , char *body_Buf)
* 函数说明: 根据命令搜寻匹配解析方法
* 输入参数: WBE网页发来的命令
* 返回参数: 搜寻匹配成功返回0  ; 未找到匹配命令返回1
*******************************************************************************/
char Search_match_the_analytical_method(const char *cmd, char *body_Buf)
{
    //找到头结点
	WEB_Server_Struct *f = WEB_Registry_Head;
	char *p;

	printf("CMD:\r\n");
	printf("%s\r\n", cmd);
	for (f = WEB_Registry_Head; f != NULL; f = f->next)
	{
        //判断cmd是否在链表内
		p = strstr(cmd, f->cmd);
		if (p != NULL)
		{
             //获取命令数据
			p = (char *)cmd;
            //获取命令 value指针
			p = p + strlen(f->cmd) + 1;
            //进行响应处理
			f->function(body_Buf, p); //此函数不可重入,所以停止任务调度
			return 1;
		}
	}
	return 0;
}
/*******************************************************************************
* 函数名称:  char POST_Search_match_the_analytical_method(char *cmd,char *dat)
* 函数说明: POST方式下获取的命令和数据,根据命令搜寻匹配解析方法
* 输入参数: WBE网页POST发来的命令和数据包
* 返回参数: 搜寻匹配成功返回0  ; 未找到匹配命令返回1
*******************************************************************************/
char POST_Search_match_the_analytical_method(char *cmd, char *body_buf, char *dat)
{
	WEB_Server_Struct *f = WEB_Registry_Head;

	for (f = WEB_Registry_Head; f != NULL; f = f->next)
	{
		if (strstr(cmd, f->cmd))
		{
            //dat在上层应用获取到,直接传入value值
			f->function((char *)body_buf, dat); //此函数不可重入,所以停止任务调度
			return 1;
		}
	}
	return 0;
}

五、前后台交互实现

(一)初始化

c 复制代码
//把我们传感器数据获取节点,定义为链表的头结点
#define WEB_Registry_Head 	 &SENSOR_WEB_DATA_GET
/**
  * @brief  http server thread 
  * @param arg: pointer on argument(not used here) 
  * @retval None
  */
void http_server_socket_thread(void *p_arg)
{
    //.......

	printf("\r\n--------------Web_Server_Task----------------\r\n");
    //把CMD相关的节点插入到链表当中
	WEB_Service_Registration(&CONTROL_LIGHT_CMD_POST);
	WEB_Service_Registration(&CONTROL_FAN_CMD_POST);
	WEB_Service_Registration(&CONTROL_ALARM_CMD_POST);
    
    //........

}

(二)前台交互解析

c 复制代码
/*******************************************************************************
* 函数名称:  void Respond_Http_Request(SOCKET ch ,char* Request_Msg)
* 函数说明: 响应HTTP请求
* 输入参数: socket 端口:ch ;HTTP请求数据包指针	:Request_Msg
* 返回参数: 无
*******************************************************************************/
void Respond_Http_Request(char ch, char *Request_Msg)
{
	Http_Request_MSG_Type Http_Request_MSG;
	char *thisstart = NULL;
	char *nextstart = NULL;
	char *buf;
	int length = 0;
	Parse_Http_Request(Request_Msg, &Http_Request_MSG); //解析HTTP请求类型
	switch (Http_Request_MSG.Method)
	{
	case METHOD_ERR:
		write(ch, (const unsigned char *)ERROR_REQUEST_PAGE, sizeof(ERROR_REQUEST_PAGE));
		break;
	case METHOD_HEAD:
	case METHOD_GET:
		if (strstr((const char *)Http_Request_MSG.URL, "/DATA/")) //判断是否是通讯指令,并解析指令帧
		{
			char *buf;
			buf = (char *)pvPortMalloc(128);									
			if (Search_match_the_analytical_method((const char *)(Http_Request_MSG.URL + 6), buf)) //匹配解析方法,匹配成功,执行相应动作
			{
				Send_Web_Service_Data(ch, buf); //回应请求的数据
			}
			else
			{
				write(ch, INVALID_CMD, sizeof(INVALID_CMD)); //无此指令,回应无效请求
			}
			vPortFree(buf); //释放内存空间
		}
		else
		{
            //发送请求的文件
		}
		break;
	case METHOD_POST:
		//获取POST内容
		thisstart = strstr(Http_Request_MSG.Post_Data, "Content-Length:");
		if (thisstart != NULL)
		{
			Http_Request_MSG.Content_Length = atoi(thisstart + 15); //获取POST内容的大小
		}
		thisstart = strstr(thisstart, "\r\n\r\n") + 4;
         //Post_Data = "On"/"Off"
		Http_Request_MSG.Post_Data = thisstart;
		length = strlen(thisstart); //修改bug

		//解析POST内存
		if (strstr((const char *)Http_Request_MSG.URL, "/CMD/")) //判断是否是通讯指令,并解析指令帧
		{
			//本次接收的数据尾指针获取
			nextstart = thisstart + length;
             //nextstart - thisstart 本次socket接收数据的内容
            // Content_Length 是这次post body数据的长度
			while ((nextstart - thisstart) < Http_Request_MSG.Content_Length) //可能数据量很大,获取POST完整内容
			{
                 //后续还有数据,再调用read进行读取
				length = read(ch, (unsigned char *)nextstart, 1500);
				if (length > 0)
                      //把数据尾指针更新
					nextstart += length;
                      //没有后续数据
				else if (length < 0)
					break;
			}
             //填充\0保证一个完整字符串
			*nextstart = '\0';
             //更新整个post下发的长度字段
			Http_Request_MSG.Content_Length = nextstart - thisstart;

			//解析数据包
			buf = (char *)pvPortMalloc(128); //开辟内存空间--原先2048
			if (POST_Search_match_the_analytical_method(Http_Request_MSG.URL + 5, buf, Http_Request_MSG.Post_Data))
			{
				Send_Web_Service_Data(ch, buf);
			}
			else
			{
				write(ch, INVALID_CMD, sizeof(INVALID_CMD));
			}
			vPortFree(buf);
		}
		else
		{
            	//下发文件
		}
		break;
	default:
		break;
	}
}

(三)前后台交互响应

c 复制代码
/*******************************************************************************
* 函数名称:  void Send_Web_Service_Data(SOCKET ch,char*body_buf) 
* 函数说明: 发送Web回应数据包 
* 输入参数: SOCKET 通道号 ,body_buf 要发送的数据包
* 返回参数: 无
*******************************************************************************/
void Send_Web_Service_Data(char ch, char *body_buf)
{
	char *buf;
	char *P_index;
	short length = 0;
    //获取body长度
	length = strlen((const char *)body_buf);
    //开辟内存空间
	buf = (char *)pvPortMalloc(length + sizeof(RETURN_cmd_OK) + 50); //开辟内存空间
	sprintf(buf, "%s%u\r\n\r\n%s", RETURN_cmd_OK, length, body_buf);
	length = strlen((const char *)buf);
	P_index = buf;
    //写入socket按照1500个字节进行传输
	while (length > 1500)
	{
         //如果write单次发送的长度太大,lwip占用动态内存比较多,这是时候,我们需要手动分片
		write(ch, (const unsigned char *)P_index, 1500);
		length -= 1500;
		P_index += 1500;
	}
	if (length > 0)
	{
		write(ch, (const unsigned char *)P_index, length);
	}
	vPortFree(buf); //释放内存空间
}

物联网智慧教室项目(九):ZigBee采集控制功能开发

一、zigbee采集控制功能实现

(一)zigbee采集控制功能硬件连接

(二)zigbee组网代码介绍及烧写

1、硬件连接

协调器连接
温湿度&风扇连接

2、代码路径

文件路径
c 复制代码
智慧教室项目实战\day09\03-Zigbee源码
IAR工程路径
c 复制代码
智慧教室项目实战\day09\03-Zigbee源码\Projects\zstack\Samples\ZigbeeApp\CC2530DB

3、代码编译配置

协调器编译
温湿度节点编译
风扇节点编译

三、STM32实现代码移植及分析

文件路径

c 复制代码
智慧教室项目实战\02-智慧教室项目实战\day09\02-STM32需要移植的代码

zigbee应用协议解析实现

c 复制代码
protocol.c protocol.h

zigbee串口数据接收

c 复制代码
usart.c
    //主要配置使能uart1空闲中断
    
freertos.c
	//创建zigbee消息队列
    //在zigbee任务中进行接收消息,解析消息
stm32f4xx_it.c
    //zigbee消息入队

数据封装

c 复制代码
CmdAndSensor.c
    //增加了风扇控制

webserver

c 复制代码
httpserver-socket.c
    //传感器数据获取
    //风扇控制

触摸屏展示

c 复制代码
TempDLG.c
HumDLG.c
LightDLG.c
    //增加传感器数据动态展示功能
FanDLG.c
    //增加实际的风扇控制功能

整体功能验证

二、Zigbee组网设计

(一)组网架构

使用Zigbee技术组建无线网络,网络中包含温湿度采集节点、风扇控制节点、Zigbee协调器

  • Zigbee协调器负责Zigbee网络组建,通过串口与STM32进行通信
  • STM32负责Zigbee传感器数据解析,和控制命令下发

(二)通信协议

协议格式

  1. 包头/版本:表示一包完整的数据/当前使用的协议版本

  2. 节点ID:在Zigbee网络里的节点ID号

  3. 节点类型:

    1. 0x0:温湿度传感器
    2. 0x01:风扇控制
  4. 数据长度:

    1. 范围:0~255
    2. 单位:字节
  5. 数据域:

  6. 温湿度传感器:温度+湿度(1+1字节)

  7. 风扇:风扇状态 (1个字节)

  8. CRC-8

  9. 8位循环冗余校验(保证通信可靠)

  10. 校验域为节点ID~数据域

包头 节点ID 节点类型 数据长度 数据域 CRC-8 包尾/版本
0x99 0x0~0XxFFFF 0x0~0xFF 0x0~0xFF xx CRC 0x01
温湿度协议格式

上行

包头 节点ID 节点类型 数据长度 数据域 CRC-8 包尾/版本
0x99 0x0001 0x00 2 温度:25℃,湿度:80% CRC 0x01
风扇协议格式

上行

包头 节点ID 节点类型 数据长度 数据域 CRC-8 包尾/版本
0x99 0x0002 0x01 1 开:0x01,关:0x00 CRC 0x01

下行

包头 节点ID 节点类型 数据长度 数据域 CRC-8 包尾/版本
0x99 0x0002 0x01 1 开:0x01,关:0x00 CRC 0x01

三、Zigbee组网设计

通信方式

Zigbee支持单播,组播,广播,绑定通信,根据我们功能定义,协调器可以采用广播通信,节点采用单播通信

协调器地址恒为:0x0000

广播地址为:0xFFFF

节点地址:采用短地址通信,但是地址是有协调器进行动态分配的

协议解析 组包设计

在实际的网络的通信中即有上行又有上行,需要我们通过代码具体实现

####设计思路

  1. 判断接收协议是否合法
    1. 检查协议头
    2. 检查协议尾
    3. 校验CRC
  2. 获取有效数据
  3. 解析数据
  4. 创建协议数据

四、协议组包拆包功能实现

CRC教研算法

c 复制代码
/***************************************************************************
* File                  : protocol.c
* Function Name         : 
* Description           : 协议处理
* Version               : v1.0
* Author                : zhengdao.liu
* Date                  : 2019/8/22
* Parameter             :
* Return                :
* Note                  :
***************************************************************************/

#include "protocol.h"

#include "stdio.h"

#include "string.h"

extern UART_HandleTypeDef huart1;

ProtocolDataType  ProtocolData = {
    HEAD,
    NODE_ID,
    NODE_TYPE,
    DATA_LEN,
    0x0,
    0x0,
    TAIL,
};


/******************************************************************************
 * Name:    CRC-8               x8+x2+x+1
 * Poly:    0x07
 * Init:    0x00
 * Refin:   False
 * Refout:  False
 * Xorout:  0x00
 * Note:
 *****************************************************************************/
uint8 crc8(uint8 *data, uint8 length)
{
    uint8 i;
    uint8 crc = 0;        // Initial value
    while(length--)
    {
        crc ^= *data++;        // crc ^= *data; data++;
        for ( i = 0; i < 8; i++ )
        {
            if ( crc & 0x80 )
                crc = (crc << 1) ^ 0x07;
            else
                crc <<= 1;
        }
    }
    return crc;
}

/***************************************************************************
* Function Name         : ProtocolCheck
* Description           : 检查协议是否合法
* Parameter             :Data:协议数据包 DataLen:数据包长度
* Return                :合法:1,非法:-1
* Note                  : NULL
***************************************************************************/
uint8  ProtocolCheck(uint8 *Data,uint16 DataLen){
  
  uint8 crc = 0;
//  1.检查协议头
  if(Data[0] != HEAD){
    printf("Protocol Head Error!\r\n");
    return 0;
  }
//  2.检查协议尾
  if(Data[DataLen-1] != TAIL){
    printf("Protocol Tail Error!\r\n");
    return 0;
  }
//  3.校验CRC
  
  crc = crc8(&Data[1],DataLen-3);
  if(crc != Data[DataLen-2]){
    printf("Protocol CRC Error!\r\n");
    return 0;
  }
  return 1;

}
/***************************************************************************
* Function Name         : 
* Description           : 协议处理
* Parameter             :
* Return                :
* Note                  :
***************************************************************************/
void ProtocolParse(uint8 *Data,uint16 DataLen){

  if(ProtocolCheck(Data,DataLen) == 0){
    return;
  
  }
  switch(Data[3]){
  case 0x0:
		SensorData[0] = Data[5];
	  SensorData[1] = Data[6];
    break;
  case 0x01:
    SensorData[2] = Data[5];
    break;
  default:
    break;
  
  }
 


}
/***************************************************************************
* Function Name         : 
* Description           : 协议处理
* Parameter             :
* Return                :
* Note                  :
***************************************************************************/
uint16  CreateData(uint8 *Data){

  //检查是否有错误
  if(Data !=NULL){
    Data[0] = ProtocolData.Head;
    Data[1] = ProtocolData.NodeId>>8;  
    Data[2] = ProtocolData.NodeId;  
    Data[3] = ProtocolData.NodeType;  
    Data[4] = ProtocolData.DataLen; 
		Data[5] = ProtocolData.Data; 
    Data[6] = crc8(&Data[1],5);   
    Data[7] = ProtocolData.Tail;
    return 8;  
  }
  else{
    printf("CreateData Error!\r\n");
    return 0;
  }

}


void FanControl(uint8_t Status){
	
		uint8_t DataBuff[10];
		uint8_t DataLen = 0;

		ProtocolData.Data = Status;
		DataLen = CreateData(DataBuff);
		HAL_UART_Transmit(&huart1,DataBuff,DataLen,100);

}

三、Zigbee协调器功能开发

(一)zigbeeAPP初始化

Created with Raphaël 2.3.0 串口初始化 zigbee组网初始化

(二)zigbeeAPP接收数据

Created with Raphaël 2.3.0 空中事件产生 消息接收处理

(三)zigbeeAPP发送数据

Created with Raphaël 2.3.0 串口接收数据 创建串口事件 串口事件发送广播数据

四、Zigbee采集节点功能开发

(一)ZigbeeAPP初始化

Created with Raphaël 2.3.0 串口初始化 zigbee组网初始化

(二)传感器采集驱动

Created with Raphaël 2.3.0 GPIO初始化 温湿度传感器采集(DHT11)

(三)传感器数据定时发送

Created with Raphaël 2.3.0 定时发送触发 定时发送处理

五、Zigbee控制节点功能开发

(一)风扇控制驱动


Created with Raphaël 2.3.0 GPIO初始化 风扇控制

(二)风扇控制解析

Created with Raphaël 2.3.0 空中数据事件 空中数据解析

相关推荐
k↑6 小时前
物联网之使用Vertx实现MQTT-Server最佳实践【响应式】
物联网·mqtt·微服务·响应式
通信大模型8 小时前
基于注意力机制的无人机轨迹优化方法:面向无线能量传输的物联网系统
人工智能·深度学习·物联网·无人机·信息与通信
中科岩创1 天前
某公园楼栋自由曲面薄壳结构自动化监测
大数据·网络·物联网·自动化
zskj_zhyl1 天前
数字康养新范式:七彩喜平台重构智慧养老生态的深度实践
大数据·人工智能·物联网
Cynthia AI1 天前
射频前端模组芯片(PA)三伍微电子GSR2337 兼容替代SKY85337, RTC7646, KCT8247HE
物联网·智能手机·智能路由器·智能音箱·射频开关芯片
程序猫A建仔1 天前
【物联网】基于树莓派的物联网开发【1】——初识树莓派
物联网
TDengine (老段)1 天前
TDengine 在智慧油田领域的应用
大数据·数据库·物联网·时序数据库·iot·tdengine·iotdb
dqsh062 天前
树莓派5+Ubuntu24.04 LTS串口通信 保姆级教程
人工智能·python·物联网·ubuntu·机器人
yuanlaile2 天前
HarmonyOS 鸿蒙操作物联网设备蓝牙模块、扫描蓝牙、连接蓝牙和蓝牙通信
物联网·华为·harmonyos·鸿蒙蓝牙·harmonyos 蓝牙模块