物联网智慧教室项目(一):智慧教室项目解决方案
一、智慧教室项目设计
(一)环境信息采集控制功能
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
/*
1.STM32CubeMX 要求版本 5.0.0以上
*/
/*
2.STM32Cube MCU Package for STM32F4 要求版本1.23.0
*/
MDK-ARM
/*
MDK-ARM 要求版本5.23.0.0 以上
*/
/*
Keil.STM32F4xx_DFP.2.12.0.pack
*/
zigbee
IAR for 8051
/*
EW8051-EV-8103-Web
*/
Zstack
/*
EW8051-EV-8103-Web
*/
串口调试工具
(二)硬件环境
STM32
STM32F407开发板
STLINK
/*需要安装驱动程序*/
USB转RS232
/*需安装USB串口驱动*/
读卡器
网线
zigbee
CC2530开发板
/*需安装SmartRF04EB驱动*/
物联网智慧教室项目(二):智慧教室项目驱动开发
一、环境监控驱动开发
外设驱动开发流程
(一)时钟&SWD配置
原理图分析
时钟原理图
如下图所示,STM32F407外部高速晶振为25MHz,分别连接到PH0和PH1引脚!
SWD原理图
如下图所示,STM32F407仿真接口SWD分别连接到PA13和PA14引脚!
外设配置
时钟外设配置
使能时钟源
- 选择RCC外设
- 选择高速时钟为外部时钟源
- PH0和PH1引脚自动高亮
配置时钟树
- 锁相环时钟源为25MHz外部高速时钟
- 高速时钟分频系数配置为25,输出为1MHz
- 倍频系数配置为336
- 分频系数配置为2,输出为168MHz
- 系统时钟源选择PLL
- APB1配置为4分频,为42MHz
- APB2配置为2分频,为84MHz
SWD外设配置
- 选择SYS外设
- 配置debug接口为串行接口(SWD)
- 引脚自动高亮
(二)串口配置
原理图分析
Zigbee通信接口
- J28为Zigbee模块底座,其中Z_W_R和Z_W_T分别Zigbee串行通信接口
- Z_W_R和Z_W_T网络连接到J13,通过J13选择连接STM32还是USB转串口,我们选择连接到STM32上的USART1(PA9和PA10)
串行调试接口
如下图所示:
- 板载两个串行通信接口,串口1连接到USART1,串口2连接到USART3,我们选择USART3
- 由于USART3可以用于串口和485通信,我们选择485必须要把CON4和CON5拨到串口通信
外设配置
USART1配置
- 配置PA9和PA10为USART1模式
- 打开USART1,配置为异步通信模式
3.使能NVIC,优先级先默认为0
USART3配置
- 配置PB10和PB11为USART3模式
- 打开USART3外设,并配置为异步通信模式
(三)GPIO配置
原理图分析
- PF7 PF8 PF9 PF10 控制板载的D6 D7 D8 D9
- PF6控制板载蜂鸣器
GPIO外设配置
- 配置PF6-PF10为输出模式
- PF6默认输出低
- PF7-PF10默认输出高
二、人机交互驱动开发
(一)FSMC
1、SRAM
原理图分析
通过下图所示:
- 采用IS61LV51216 SRM 为1MB,其实为了节约成本焊接的为IS61LV25616 为512KB
- 占用地址总线为18bit,数据总线为16bit
- 内存访问起始地址为0x6800 0000
数据手册分析
read周期
地址建立时间 <7ns
数据建立+保持时间 = 7 + 2.5ns = 10ns
write周期
数据建立周期 = 5 + 3 = 8 + 5ns = 13ns
FSMC读写周期
read
write
外设配置
- 打开FSMC外设
- 配置FSMC
- 选择存储块为NE3
- 内存类型为SRAM
- 寻址长度为18bit
- 数据宽度为16bit
- 配置FSMC时序
- 地址建立时间为1分频 1/168 = 0.005 = 5ns
- 数据建立时间为3分频
- 字节访问使能
2、LCD
原理图分析
- 如下图所示,LCD采用8080接口,CS片选,D/C命令/数据切换,RD读操作,WR写操作,D[23:0]数据总线
- 如下图所示,数据总线D[0:15]连接FSMC总线接口处,RS起始就D/C接口,连接到FSMC地址总线A0,CS片选总线连接到FSMC_NE4上,WR写操作连接FSMC_NWE总线上,RD读操作总线连接到FSMC_NOE上,背光控制连接到PC7上
- 写命令操作0x6C00 0000
- 写数据操作0x6C00 0002
数据手册分析
-
分析LCD驱动芯片时序图,计算得出地址和数据总线建立时间
地址保持周期 = 2ns
数据建立周期 = 12 + 1ns = 15ns
外设配置
- 打开FSMC外设
- 配置FSMC参数
- 内存块为NE4
- 内存类型为LCD
- LCD数据/命令切换映射到A0
- 数据宽度为16bit
- 配置PC7为输出模式
- 上电默认输出高电平
- 上电默认输出高电平
SPI
FLASH
原理图分析
- 如下图所示,SPI接口,CS连接到PH2,MISO连接到PB4,MOSI连接到PB5,CLK连接到PA5
数据手册分析
1.时钟极性及相位
通过时序图分析,SPI CLK 高有效 时钟极性为高
时钟边沿为奇数边沿
2.SPI速率
满足数据传输速率,最大不能超过33MHz
外设配置
- 配置SPI时钟和数据引脚
- 配置SPI为全双工主机模式
- 配置SPI参数
- 通信速率为系统时钟4分频
- 时钟极性为低电平
- 相位为奇数边沿
- 配置片选引脚PH2默认输出高
Touch
原理图分析
- 如下图所示,SPI接口,CS连接到PG15,MISO连接到PI2,MOSI连接到PI3,SCK连接到PI1
- 触摸中断连接到PG7
数据手册分析
- 通过计算TCH+TCL得出SPI通信速率
- 通过时序图分析,SPI不工作时为低电平
- 时钟边沿为奇数边沿
- 通信速率最小为400ns,大概2Mbit/S左右
外设配置
- 配置SPI时钟和数据引脚
- 配置SPI为全双工主机模式
- 配置SPI参数
- 通信速率为系统时钟32分频
- 时钟极性为低电平
- 相位为奇数边沿
- 配置SPI片选引脚
- 配置PG15为输出模式
- 配置PG15上电默认输出高,SPI低电平有效
三、WebServer驱动开发
(一)SDIO
原理图分析
根据原理图分析,我们采用SD总线,4bit
配置CLK、DAT0-3
外设配置
- 打开SDIO外设
- 配置SD总线为4bit位宽
- 配置DMA接收和发送
- 使能sdio全局中断 重点配置DMA优先级小于SDIO优先级
(二)ETH
原理图分析
-
如下图所示,以太网PHY采用DP83848芯片,通信模式采用RMII接口
-
配置MDC、CLK、MDIO、DV、RXD0、RXD1;EN、TXD0、TXD1
数据手册分析
PHY地址
PHY寄存器
BMCR
BMSR
PHYSTS
外设配置
四、FreeRTOS配置及任务创建
(一)freeRTOS配置
配置内核定时器
由于我们采用STM32HAL库进行开发,HAL库内部使用systick定时器用于系统延时功能,而FreeRTOS也需要一个定时器用于操作系统内核调度使用, 顾需要修改HAL定时器时钟源
- 打开SYS选项
- 配置时钟源为TIM1
配置FreeRTOS内核功能
- 多数功能在后续程序设计中,需要根据具体功能,进行配置
- 前期只需要配置动态内存空间和创建开始任务就可以
配置动态内存分配空间
- 采用FreeRTOS动态内存分配,开发效率高!顾我们程序内存使用,多数使用动态内存分配方式,分配动态内存总空间为23k=23552byte
2. 使能FreeRTOS功能
3. 分配内存空间为40960
(二)任务创建
任务及优先级划分
任务堆栈划分
1、中断优先级分配
2、printf重定向
- 在main.c文件内添加fputc函数,采用USART3作为调试接口
int fputc(int ch, FILE *p)
{
while(!(USART1->SR & (1<<7)));
USART1->DR = ch;
return ch;
}
- 在touch任务内打印启动信息
五、lwIP配置及网卡驱动
(一)lwIP配置
1、IP组网配置
- 使能lwIP
- 关闭DHCP服务
- 配置IP地址信息
2、lwIP参数配置
- 分配10k内存空间
- 使能链路检测回调功能
(二)ping测试
配置笔记本的网段和STM32开发板的网段一致
以太网断线检测
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介绍
官方网站(可边翻译成中文对照学习)
http://elm-chan.org/fsw/ff/00index_e.html
应用接口
介质访问接口
相关资源
(二)STM32Cube集成FatFS
FatFs 中间件模块架构
FatFS配置
- 打开FatFS
- 使能磁盘为SD卡
- 配置中文编码
- 配置命名空间为HEAP
- 增大C库堆空间
FatFS示例代码
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学习方法
学习步骤
项目当中用到的API
f_mkfs //物理磁盘的格式化 |
---|
f_mount |
f_open |
f_read |
f_write |
f_size |
f_close |
(三)API分析
f_mkfs
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
FRESULT f_mount (
FATFS* fs, /* [IN] File system object */
const TCHAR* path, /* [IN] Logical drive number */
BYTE opt /* [IN] Initialization option */
);
f_open
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
FRESULT f_close (
FIL* fp /* [IN] Pointer to the file object */
);
f_read
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
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
FSIZE_t f_size (
FIL* fp /* [IN] File object */
);
三、FatFS应用编程下
(一)实际文件系统应用案例分析
//远程终端单元( Remote Terminal Unit,RTU)
(二)怎么存储历史数据呢
历史数据的目的是什么
CSV格式文件
(三)历史数据存储功能实现
(四)代码
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
//FatFS 提供的通用驱动文件的实现
/**
* @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;
/**
* @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
//针对SD底层驱动实现,封装成为通用的底层驱动API
//如果使能freeRTOS,在read和Write里面,会用到操作系统的消息队列
bsp_driver_sd.c
//HAL库的二次封装,把所有基于SD卡的操作都在bsp_driver_sd实现
驱动装载
/**
* @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驱动程序
#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移植到项目工程
-
复制STemWin源码到项目工程中
工程目录:SmartClassRoom\Middlewares\Third_Party\STemWin -
在keil工程中添加相关文件
-
新建工作组:Middlewares/STemWin
-
添加需要编译的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源码不开放 |
- 修改库文件格式(keil默认不识别.a文件格式,需要我们手动配置)
(五)移植lcd和touch驱动文件
- 添加lcd.c和Touch.c到Src目录下
- 添加lcd.h和Touch.h到Inc目录下
二、emWin移植下
(一)emWin LCD驱动适配
修改GUIConf.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
只需要完成画点和读取点操作即可
/*********************************************************************
*
* _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
- 定义显示尺寸 480*272
- 定义触摸笔X,Y AD测量值(需要自己测量获得)
- 添加触摸笔校准函数
- 添加lcd初始化函数
#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
#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测试程序编写
测试程序编写
/* 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环境下进行开发仿真
- 下载地址
https://www.segger.com/downloads/emwin/
根据自己使用的STemwin库,进行下载,我们采用V5.44版本
- 添加工程到codeblockd
- 打开外部工程
- 选择.cbp文件
- 添加工程到codeblockd
- 点击编译运行按钮
- 生成模拟器
GUIBuilder 制作界面
界面添加到模拟器
修改模拟器原始工程
- 把无用的代码移除工程
- 添加WindowDLG.c到工程中
-
首先复制WindowDLG.c到SeggerEval_WIN32_MSVC_MinGW_GUI_V544\Application目录下
-
添加WindowDLG.c到工程中
-
选择Application工程目录,新建mainTask.c
-
在mainTask.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初始化
执行模型
单任务使用注意事项
我们在操作系统下使用emwin,必须要创建一个任务,用来调用emWin函数,并且官方说,此任务的优先级配置为最低
emWin函数指的是什么???
时间调度
指针输入设备
五、emWin应用编程方法
GUIBuilder
对话框
资源表
资源:我们这个对话框内部的小工具(小窗口)------- window 按钮 文本 图片
窗口管理器
窗口消息
窗口对象
物联网智慧教室项目(五):人机交互功能开发(模拟器)
一、界面构思
(一)原型分析
1、主界面
主界面提供了,整个系统的交互接口,也就是说,我们都有哪些需要提供给用户展示的
1、数据展示功能
1.1、温度
1.2、湿度
1.3、光照
2、控制操作功能
2.1、灯光
2.2、风扇
2.3、报警
分析:
对用户来说,需要点击相应的图标下,就可以进入相关的界面,很简单,我使用button小工具进行实现,其实就是在主界面创建6个button小工具
2、子界面
控制界面
控制界面应该有什么?
1、界面的标识--- 要给用户指示这个界面有什么用
使用text小工具就可以实现
2、控制操作
开状态-当用户点击图标,灯泡点亮
关状态-当用户点击图片,灯泡熄灭
3、返回主菜单
当用户点击主菜单按钮,返回主界面
控制和返回主界面都用button实现即可
数据界面
控制界面应该有什么?
1、界面的标识--- 要给用户指示这个界面有什么用
使用text小工具就可以实现
2、数据图标展示
显示一个图片就可以,用图片小工具
3、返回主菜单
当用户点击主菜单按钮,返回主界面,用button小工具
(二)界面构思
1、主界面
2、传感器数据展示界面
3、控制设备界面
(三)素材设计
1、主界面素材
2、子界面素材
注意事项
图片最终是用过代码,进行展示的
我们必须针对图片命名时候,有规范,有要求
1、主界面命名
MainAlarm
MainLed
2、子界面
SubAlarmOpen
SubAlarmClose
SubHum
二、交互设计
(一)窗口初始化
业务流程
窗口初始化函数接口
句柄的概念
句柄:在C语言里面,它其实就是一个指针变量
WINDOW_SetBkColor
WM_GetDialogItem
BUTTON_SetBitmap
(二)按键处理
按键处理接口
WM_NOTIFY_PARENT
按钮通知代码
(三)界面切换
界面切换接口
GUI_EndDialog
GUI_CreateDialogBox
三、GUIBuilder创建代码框架
GUIBuilder创建主界面
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
(一)主界面添加图片流
// 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
// 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(湿度子界面)为例,其余五个子界面操作除了命名得修改一下,其余一致,如下:
// USER START (Optionally insert additional static data)
extern GUI_CONST_STORAGE GUI_BITMAP bmSubHome;
extern GUI_CONST_STORAGE GUI_BITMAP bmSubHum;
// USER END
// 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
五、界面切换功能实现
功能优化
设置子界面背景色为 8 20 44 16进制 2C 14 08
//TEXT_SetBkColor(hItem, GUI_MAKE_COLOR(0x002C1408));
(一)界面切换功能实现
1、主界面切换到子界面
MainDLG.c(以按键0 Alarm为例,其余五个按键操作一致):
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
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为例
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为例:
// 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
// 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中添加代码
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
只要在点阵显示之前,进行调用一次就可以,一般都是在我们GUI_INIT 之后
emWin创建中文字库
FontCvtST
基于windows字库创建emWin字库
生成全部字库,需要占用35.2MB STM32 flash也就是几百k
创建小型字库
(三)生成项目所需中文字库
温度
湿
光照
报警
灯光
风扇
传感器
控制
二、实现界面中文显示
三、代码移植到STM32(上)
(一)MDK工程编码格式修改为utf-8
(二)需要创建include_dlg.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
/**
* @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检测
/* 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(下)
测试出的bug
bug1:当切换界面时,出现闪屏动作,体验感非常差
bug2:当频繁切换界面时,出现花屏或者白屏
在下一节进行解决
五、人机交互功能测试验证
(一)切换界面闪屏问题
使用存储设备
自动使能存储设备
使能存储设备API
频繁切换界面花屏问题
/*********************************************************************
*
* 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);
}
物联网智慧教室项目(七):网页交互功能开发(前端)
源码已通过百度云上传,推荐使用谷歌浏览器打开,使用开发者模式浏览学习,也可自行修改样式。
链接:https://pan.baidu.com/s/1gYXIK8YYXrwZbwsCJSS-MQ?pwd=gbls
提取码:gbls
一、界面构思
(一)原型分析
(二)界面构思
1、标题栏
布局
标题设计
大标题:智慧教室管理系统
小标题:灯光,报警,风扇,温度,湿度,光照
样式设计
- 背景色配置
- 字体配置
- 字体大小,格式,颜色
2、导航栏
布局
导航栏标题
提示用户用于智能管理使用的,用图片进行设计
导航标签
6个导航标签
3 个用于传感器
3 个用于控制
每一个标签,都有图标和文件进行展示
6个导航展示界面
3 个传感器
显示传感器名称和传感器值
3 个控制
显示控制名称和控制按钮
样式设计
当鼠标每点击一个导航标签,我都需要进行导航界面展示
前后台交互设计
- 定时刷新传感器数据值(当用户点击传感器标签,需要把传感器值实时的展示给用户)
- 控制下发(当用户点击控制标签,需要根据用户下发的命令进行控制实际的硬件,并且在主题栏进行动态展示)
3、主题栏
布局
-
主题界面
给用户展示一个智慧教室的图片
-
风扇图标
当用户开关风扇时,这个图标要进行动态效果
-
报警器图标
当用户开关报警器时,这个图标要进行动态效果
-
灯光控制
当用户开关灯泡时,我们教室的主界面要展示动态效果
(三)素材设计
1、导航栏素材
2、主题栏素材
二、前端开发技术
(一)开发工具
https://code.visualstudio.com/
(二)前端开发基本技能
html
CSS
JavaScript
jQuery
http://jquery.cuishifeng.cn/index.html
(三)基于框架开发
BootStrap
https://www.bootcss.com/
Vue
https://cn.vuejs.org/
react
https://react.docschina.org/
项目用到的技术
三、Web页面布局
(一)标题栏布局
<div class="header">
<div class="content">
<h1>智慧教室管理系统
<small>温度、湿度、风扇、报警,光照
</small>
</h1>
</div>
</div>
(二)导航栏布局
<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>
(三)背景栏布局
<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页面样式设计
(一)外部文件引入
<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>
(二)标题栏样式设计
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;
}
(三)导航栏样式设计
.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;
}
(四)主题栏样式设计
.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教程
https://www.runoob.com/bootstrap/bootstrap-tab-plugin.html
2、导航标签样式设计
<li class="line active">
<a href="#title1" data-toggle="tab">
<img src="images/tubiao01.png" width="40px">温度传感器
</a>
</li>
3、导航标签内容设计
<div class="box fade in active" id="title1">
<p>温度值<br /><span>
<lable id="temperature"></lable>
</span></p>
</div>
五、前后台交互设计
(一)传感器数据获取
1、传感器数据初始化
$("#temperature").text("25℃");
$("#humidity").text("45%");
$("#light").text("1233lux");
2、ajax后台数据获取
//获取传感器数据
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");
}
})
}
//命令交互
function SendCmd(type, cmd) {
$.ajax({
url: "/CMD/" + type,
type: "post",
cache: false,
timeout: 2000,
data: cmd,
success: function (data) {
console.log(data);
}
})
}
(二)定时刷新传感器数据
setInterval(ReadData, 5000);
(三)按钮命令交互
1、灯光控制
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、风扇控制
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、报警控制
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服务器
配置服务器主目录
浏览器输入服务器地址
127.0.0.1/index.html
分析Web服务器运行日志
(四)WebServer功能设计
文件请求响应
传感器数据请求响应
命令请求响应
二、WebServer主线程实现
(一)WebServer代码移植
文件移植
智慧教室项目实战\day08\03-WebServer移植文件
把网页文件拷贝到SD卡中
1.拷贝文件到SD卡"根目录下"
2.开发板断电插入SD卡
3.烧录程序看现象(浏览器输入192.168.1.7)
访问STM32服务器,网页图片加载需要多次刷新问题
1、我们webserver是一个单线程任务,http属于短链接//http没有记忆功能,在一次socket通信中能获取多少数据,就只能获取多少数据
2、但是浏览器有缓存,我们使用时只需要多刷新几次就可以了
3、也可以通过在前端增加一些js代码(循环加载前端资源(文件))
4、STM32内存太小了,没有办法做长连接,短连接模式可以实现多个客户端连接
(二)WebServer主线程实现
http_server_socket_thread
/**
* @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
/**
* @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解析业务流程
三、Http文件请求响应
(一)数据结构
http文件请求响应首部封装
#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消息结构体
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
/*******************************************************************************
* 函数名称: 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
/*******************************************************************************
* 函数名称: 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
/*******************************************************************************
* 函数名称: 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);
}
}
四、前后台交互方法设计
(一)数据结构
typedef struct Web_s{
struct Web_s *next; //单链表节点
const char *cmd; //get&post具体消息内容存放位置
void (*function)(void *,void *);//不同响应的处理方法
} WEB_Server_Struct;
(二)交互数据结构封装
字符串封装
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 消息内容为关闭
数据节点封装
//我们项目中需要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};
交互函数封装
/****************************************************************************************************
函数原型: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\"}");
}
}
(三)交互数据结构处理
/*******************************************************************************
* 函数名称: 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;
}
五、前后台交互实现
(一)初始化
//把我们传感器数据获取节点,定义为链表的头结点
#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);
//........
}
(二)前台交互解析
/*******************************************************************************
* 函数名称: 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;
}
}
(三)前后台交互响应
/*******************************************************************************
* 函数名称: 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、代码路径
文件路径
智慧教室项目实战\day09\03-Zigbee源码
IAR工程路径
智慧教室项目实战\day09\03-Zigbee源码\Projects\zstack\Samples\ZigbeeApp\CC2530DB
3、代码编译配置
协调器编译
温湿度节点编译
风扇节点编译
三、STM32实现代码移植及分析
文件路径
智慧教室项目实战\02-智慧教室项目实战\day09\02-STM32需要移植的代码
zigbee应用协议解析实现
protocol.c protocol.h
zigbee串口数据接收
usart.c
//主要配置使能uart1空闲中断
freertos.c
//创建zigbee消息队列
//在zigbee任务中进行接收消息,解析消息
stm32f4xx_it.c
//zigbee消息入队
数据封装
CmdAndSensor.c
//增加了风扇控制
webserver
httpserver-socket.c
//传感器数据获取
//风扇控制
触摸屏展示
TempDLG.c
HumDLG.c
LightDLG.c
//增加传感器数据动态展示功能
FanDLG.c
//增加实际的风扇控制功能
整体功能验证
二、Zigbee组网设计
(一)组网架构
使用Zigbee技术组建无线网络,网络中包含温湿度采集节点、风扇控制节点、Zigbee协调器
- Zigbee协调器负责Zigbee网络组建,通过串口与STM32进行通信
- STM32负责Zigbee传感器数据解析,和控制命令下发
(二)通信协议
协议格式
-
包头/版本:表示一包完整的数据/当前使用的协议版本
-
节点ID:在Zigbee网络里的节点ID号
-
节点类型:
- 0x0:温湿度传感器
- 0x01:风扇控制
-
数据长度:
- 范围:0~255
- 单位:字节
-
数据域:
-
温湿度传感器:温度+湿度(1+1字节)
-
风扇:风扇状态 (1个字节)
-
CRC-8
-
8位循环冗余校验(保证通信可靠)
-
校验域为节点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
节点地址:采用短地址通信,但是地址是有协调器进行动态分配的
协议解析 组包设计
在实际的网络的通信中即有上行又有上行,需要我们通过代码具体实现
####设计思路
- 判断接收协议是否合法
- 检查协议头
- 检查协议尾
- 校验CRC
- 获取有效数据
- 解析数据
- 创建协议数据
四、协议组包拆包功能实现
CRC教研算法
/***************************************************************************
* 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);
}