项目说明

使用STM32为主要控制器,检测到周围光照强度高于一定值或者无人情况下自动关闭路灯,光照强度小于一定值且有人时自动开灯。

功能如下:
1、使用 BH1750 获得光照强度。
2、人体检测传感器判断是否有人。
3、根据检测情况自动开关路灯。
4、使用 OLED 屏幕显示检测状态。
5、使用 ESP8266 连接 WIFI 通过 MQTT 实时上报检测数据。
6、可使用 MQTT 软件检测上报数据。
7、可使用 APP 查看软件检测上报数据。

项目开源链接

本项目资料完全开源。资料包获取方式:

githubhttps://github.com/snqx-lqh/ProjectReleasePage

gitee(国内镜像)https://gitee.com/snqx-lqh/ProjectOpenSourceReleasePage

项目属于 32 的编号 B001 ,在发布页中,找到对应项目获取方式。

硬件设计

硬件设计如图所示。

在这里插入图片描述

实际接线如下:
BH1750:

  • SCL->PB6
  • SDA->PB7
  • ADDR->GND
  • VCC->3V3
  • GND->GND
    OLED:
  • SCK->PB8
  • SDA->PB9
  • VCC->3V3
  • GND->GND
    HC-SR501:
  • VCC->5V
  • OUT->PA0
  • GND->GND
  • 模块上的功能跳帽接到 L
    ESP8266:
  • 3V3->3V3
  • GND->GND
  • TX->PB11
  • RX->PB10
    USB转TTL:
  • TX->PA10
  • RX->PA9
  • GND->GND

软件设计

软件设计包含驱动设计和具体功能设计。

驱动设计

BH1750驱动

关于 BH1750 ,我们只需要知道怎么使用 IIC 读取值和写入值就可以了。我感觉它和常规IIC器件不同,因为一般都是先写设备地址,然后找寄存器,然后再读写数,他好像没有寄存器地址这一说法,写数据就先写地址再写数据就行,读也是先写地址,然后紧接着读两个数。

更完善的内容可以看我之前写的博客文章。

STM32驱动BH1750:https://blog.csdn.net/wan1234512/article/details/157580681?spm=1011.2124.3001.6209

HC-SR501驱动

这个设备,由于它是检测到人后产生电平变化,所以使用了引脚中断来检测他的变化。检测到人的话,模块电平输出会由低到高,我们只需要抓这个上升沿即可,再使用一个全局变量状态标志位,标志这个中断已被触发。

void EXTI0_IRQHandler(void)
{
	if(EXTI_GetITStatus(EXTI_Line0)!= RESET)
	{
		hc_sr50_state = 1;
		EXTI_ClearITPendingBit(EXTI_Line0);
	}
}
ESP01S驱动

ESP01S使用的固件是 (1471)ESP8266-AT-1M.bin。这个在我的开源文件中包含。

关于此设备驱动,最重要的是怎么处理它返回值的不定长以及不是及时响应,一个命令可能会隔段时间才会回应,还不连续。所以,我们使用一个环形缓冲区把接收到的内容先暂存,然后再处理。

环形缓冲区这里使用了 RT-Thread 中的环形缓冲区思想,但是只保留了一些常用函数。环形缓冲区可以当作被两个索引管理的数组。我们只谈使用。在串口接收中断中,我们将接收到的数值放到环形缓冲区中,在主任务中进行提取处理。

主要的接收处理如下:

void USART3_IRQHandler(void)
{
	if (USART_GetITStatus(USART3, USART_IT_RXNE) != RESET)
	{
		uint8_t data = USART_ReceiveData(USART3); 
        ringbuffer_putchar(&esp01s_device.rx_rb, data); // 将数据放入环形缓冲区  esp01s_device.rx_rb 中
		USART_ClearITPendingBit(USART3, USART_IT_RXNE);
	}
}

esp01s_device 是一个管理 ESP01S 变量的结构体变量,rx_rb 就是里面定义的环形缓冲结构体。

然后在 主任务中,我的发送指令函数如下:

int esp_at_cmd(struct esp01s *esp, const char *cmd, const char *expect, uint16_t timeout_ms)
{
    // 清空缓存(很重要)
    int ret = 0;
    ringbuffer_reset(&esp->rx_rb);
    esp_send(cmd);
    esp_send("\r\n");
    ret = esp_wait_response(esp, expect, timeout_ms);
    esp->resp_len = 0;
    return ret;
}

int esp_wait_response(struct esp01s *dev, const char *expect, uint32_t timeout)
{  
    uint16_t delay_ms_counter = 0; 
	dev->resp_len = 0;
    while (delay_ms_counter < timeout)
    {
        uint8_t ch;
        if (ringbuffer_getchar(&dev->rx_rb, &ch) == 1)
        {
            // 保存数据
            if (dev->resp_len < ESP_RESP_BUF_SIZE - 1)
            {
                dev->resp_buf[dev->resp_len++] = ch;
                dev->resp_buf[dev->resp_len] = '\0';
            }
            // 判断成功
            if (strstr((char *)dev->resp_buf, expect))
            {
                return 0;
            }
            // 判断错误
            if (strstr((char *)dev->resp_buf, "ERROR"))
            {
                return -1;
            }
        } else {
            // 没有数据,等待一段时间
            delay_ms_counter++;
            esp_delay_ms(1);
        } 
    }
    return -2; // 超时
}

发送数据完成后,就等待响应,响应就是把串口中断中环形缓冲区得到的值,一个个取出来存到数组中,然后再进行判断,比如判断是否接收到响应字符,是否接收到错误字符。

OLED驱动

OLED 就是使用的中景园电子的,只不过使用的我自己的 IIC 函数。

应用设计

应用设计首先是初始化。然后是while任务轮询。

初始化

在初始化中,值得注意的就是 ESP01S 的初始化,其他驱动初始化比较一般。

我们的 ESP 01s 使用了 MQTT ,所以我们需要连接一些 MQTT 的配置。

首先是 ESP 复位,没什么好说的,只是需要切换到station模式,也就是使用 "AT+CWMODE=1" 步骤如下:

ret = esp_at_cmd(&esp01s_device, "AT+RST", "OK", 2000);
printf("AT+RESET resp: %s\n", esp01s_device.resp_buf);
if(ret != 0) while(1); 
delay_ms(500);

ret = esp_at_cmd(&esp01s_device, "AT", "OK", 2000);
printf("AT resp: %s\n", esp01s_device.resp_buf); 
if(ret != 0) while(1);

ret = esp_at_cmd(&esp01s_device, "AT+CWMODE=1", "OK", 2000);
printf("AT+CWMODE resp: %s\n", esp01s_device.resp_buf); 
if(ret != 0) while(1);

然后是连接 WIFI。这是我的 WIFI 名 和 密码 。你需要替换成自己的。

ret = esp_at_cmd(&esp01s_device, "AT+CWJAP=\"CMCC-XJmL\",\"sR62HiPv\"", "OK", 5000);
printf("AT+CWJAP resp: %s\n", esp01s_device.resp_buf); 
if(ret != 0) while(1);

然后是 MQTT 的处理,下面是常用指令:

AT+MQTTUSERCFG=0,1,"用户ID","账号","密码",0,0,""
# 设置MQTT连接所需要的的参数,包括用户ID(不为空)、
# 账号(admin)以及密码(public)
AT+MQTTCONN=0,"broker.emqx.io",1883,0
AT+MQTTPUB=0,"ESP8266/online","1",0,0
#发布一条topic为“ESP8266/online”,message为“1”的数据, #QOS设置为0
AT+MQTTSUB=0,"ESP8266/EMQX",0
#订阅一条topic为“ESP8266/EMQX”,QOS为0的数据

这里需要注意,需要先配置用户 ID ,不要使用 test 这种 ID 最好,因为我使用的公共测试服务器 broker.emqx.io,用 test 这个名字很可能连不上。

ret = esp_at_cmd(&esp01s_device, "AT+MQTTUSERCFG=0,1,\"user\",\"user\",\"123\",0,0,\"\"", "OK", 2000);
printf("AT+MQTTUSERCFG resp: %s\n", esp01s_device.resp_buf); 
if(ret != 0) while(1);

ret = esp_at_cmd(&esp01s_device, "AT+MQTTCONN=0,\"broker.emqx.io\",1883,0", "OK", 5000);
printf("AT+MQTTCONN resp: %s\n", esp01s_device.resp_buf); 
if(ret != 0) while(1);

我的代码还订阅了 /user/mqtttest/led 做调试。

ret = esp_at_cmd(&esp01s_device, "AT+MQTTSUB=0,\"/user/mqtttest/led\",0", "OK", 2000);  
printf("AT+MQTTSUB resp: %s\n", esp01s_device.resp_buf); 
if(ret != 0) while(1);
while 循环

在任务循环中,做了一个简单的计数器,每隔10ms加1,以此计数器达到计时作用,这个其实并不准确,需要准确最好使用定时器来做这个计数。但是我们这个项目不需要太高的实时性,这样也可以用。

首先是是否有人经过的检测,前面我们说过,有人的时候,中断中hc_sr50_state会置1,我们得到这个标识后,可以把有人的状态保持久一点,有人,我就做一个计数器加加,在加到200这个时间内,都是表示有人状态 hc_sr50x_delay_state 为 1 。因为一个while循环延时10ms,所以循环200次,也就是2S,但是这个2S其实没那么准,因为你一轮WHILE循环不使用定时器也不是一定的10ms。

// 如果有人 hc_sr50_state 这个状态在中断中置 1
if(hc_sr50_state == 1)
{
	// 有人就处于状态延迟状态
	hc_sr50x_delay_state       = 1;
	hc_sr50x_state_delay_count = 0;
	hc_sr50_state = 0; 
	printf("Someone has entered the area\r\n");
}
// 如果处于状态延迟状态
if(hc_sr50x_delay_state == 1)
{
	hc_sr50x_state_delay_count++;
	if(hc_sr50x_state_delay_count > 200) // 将每次的有人进入状态至少保持 2S
	{
		// 2S后清空这个 状态延迟状态
		hc_sr50x_state_delay_count = 0;
		hc_sr50x_delay_state = 0;
	}
} 

然后是 1S 的时候进行光照强度读取以及数据上报,这个就比较简单了,就是一个读取后比较。

if(count % 100 == 0) // 1S 读取一次光照强度
{
	if (bh1750_read_lux(BH1750_ADDR_L, BH1750_CONT_H_RES, &lux) == 0)
	{
		printf("continuous Light = %.2f lux hc_sr50x_delay_state:%d\r\n", lux, hc_sr50x_delay_state);
	}
	// 光照小于一定值,且有人处于状态延迟状态时,打开灯
	if(lux < 500 && hc_sr50x_delay_state == 1){
		led_on();
		led_state = 1;
	}else{
		// 其他情况关灯
		led_off();
		led_state = 0;
	} 
	// 上报状态信息
	sprintf(mqtt_put_message,"AT+MQTTPUB=0,\"/user/mqtttest/led_state\",\"%d\",0,0",led_state);
	esp_at_cmd(&esp01s_device, mqtt_put_message , "OK", 2000);  
	printf("AT+MQTTSUB resp: %s\n", esp01s_device.resp_buf);
	sprintf(mqtt_put_message,"AT+MQTTPUB=0,\"/user/mqtttest/lux\",\"%.2f\",0,0",lux);
	esp_at_cmd(&esp01s_device, mqtt_put_message , "OK", 2000);  
	printf("AT+MQTTSUB resp: %s\n", esp01s_device.resp_buf);
	sprintf(mqtt_put_message,"AT+MQTTPUB=0,\"/user/mqtttest/hc_sr50x_delay_state\",\"%d\",0,0",hc_sr50x_delay_state);
	esp_at_cmd(&esp01s_device, mqtt_put_message , "OK", 2000);  
	printf("AT+MQTTSUB resp: %s\n", esp01s_device.resp_buf);
	
	// 更新一次界面显示
	OLED_Clear_Buffer(); //清除之前的缓存
	sprintf((char*)oled_show_str,"Smart LED");
	OLED_ShowString(24,0,oled_show_str,16,1);
	sprintf((char*)oled_show_str,"LED: %s",led_state==1?"ON":"OFF");
	OLED_ShowString(0,16,oled_show_str,16,1);
	sprintf((char*)oled_show_str,"LUX: %.2f",lux);
	OLED_ShowString(0,32,oled_show_str,16,1);
	sprintf((char*)oled_show_str,"PERSON: %s",hc_sr50x_delay_state == 1?"Y":"N");
	OLED_ShowString(0,48,oled_show_str,16,1);
	OLED_Refresh(); //更新
} 

最后还做了一个 WIFI 接收信息解析,因为我们前面不是订阅了一个消息吗。当我们读取到一行 WIFI 发送的数据时,对其进行解析。

// 读取 ESP 接收
int ret = esp_read_line(&esp01s_device);
if(ret == 0)
{
	printf("Received line: %s", esp01s_device.resp_buf);
	esp_dispatch_line(&esp01s_device, (char *)esp01s_device.resp_buf); 
} else {
	// 没有完整行数据,继续等待
} 

mqttfx 连接测试

我们上报的信息,想要快速查看是否真的上报成功,可以使用 mqttfx 工具进行查看。工具在我的开源文件中包含。

1、点击设置,准备创建一个连接。

在这里插入图片描述

2、点击此处新建一个连接。

在这里插入图片描述

我把这个连接创建为 NewConnect 并且配置访问服务器。

在这里插入图片描述

主页面点击连接即可。

在这里插入图片描述

然后我们订阅一个主题,主题名自己输入,需要和代码中发布的主题一致。

在这里插入图片描述

便可以看到发布的消息了。

在这里插入图片描述

APP

本项目还使用 Uni-APP 搭建了一个简易的手机 APP 。

开发过程中遇到的主要问题是 mqtt 库 的选取,在下载mqtt库的时候,一定要下载3.0.0,其他的4.1.0和更高版本,我用着总是有莫名其妙的问题。

安装流程如下,前提是你的UNI-APP开发环境已经搭建完成。

安装

首先安装mqtt.js,建议使用较为稳定的3.0.0版本,在项目工程下打开终端。

npm install mqtt@3.0.0

请注意一定要在main.js中增加如下代码。

在这里插入图片描述

// #ifndef MP
// 处理 wx.connectSocket promisify 兼容问题,强制返回 SocketTask
uni.connectSocket = (function(connectSocket) {
	return function(options) {
		console.log(options)
		options.success = options.success || function() {}
		return connectSocket.call(this, options)
	}
})(uni.connectSocket)
// #endif

软件编写

直接放代码了,由于是示例程序,代码结构比较简单。

<template>
	<!-- 页面主容器:负责整体页面的留白和布局容器 -->
	<view class="page-wrapper">
		<!-- 卡片容器:使用 flex 布局,并让子元素自动换行 -->
		<view class="card-grid">
			<!-- 卡片1:路灯亮度 -->
			<view class="card">
				<!-- 卡片图标:显示灯的图标或 logo -->
				<image class="card-icon" src="/static/light.png" mode="aspectFit"></image>
				<!-- 卡片内容区域:标题和亮度信息的容器 -->
				<view class="card-content">
					<!-- 卡片标题:显示项目名称 -->
					<text class="card-title">路灯亮度</text>
					<!-- 卡片信息行:显示 lux 单位 -->
					<view class="card-info">
						<!-- 显示路灯亮度,单位为 lux -->
						<text class="info-text">{{ streetLightLux }} lux</text>
					</view>
				</view>
			</view>
			<!-- 卡片2:是否有人 -->
			<view class="card">
				<!-- 卡片图标 -->
				<image class="card-icon" src="/static/person.png" mode="aspectFit"></image>
				<!-- 卡片内容区域 -->
				<view class="card-content">
					<!-- 卡片标题 -->
					<text class="card-title">是否有人</text>
					<!-- 卡片信息行 -->
					<view class="card-info">
						<!-- 1 代表有人,0 代表无人 -->
						<text class="info-text">{{ hasPerson ? '1' : '0' }}</text>
					</view>
				</view>
			</view>
			<!-- 卡片3:LED 状态 -->
			<view class="card">
				<!-- 卡片图标 -->
				<image class="card-icon" src="/static/led.png" mode="aspectFit"></image>
				<!-- 卡片内容区域 -->
				<view class="card-content">
					<!-- 卡片标题 -->
					<text class="card-title">LED 状态</text>
					<!-- 卡片信息行 -->
					<view class="card-info">
						<!-- 显示 LED 是否点亮 -->
						<text class="info-text">{{ ledOn ? '已点亮' : '未点亮' }}</text>
					</view>
				</view>
			</view>
		</view>
	</view>
</template>

<script> 

const MQTT_TOPICS = {
	streetLightLux: '/user/mqtttest/lux',
	hasPerson: '/user/mqtttest/hc_sr50x_delay_state',
	ledOn: '/user/mqtttest/led_state'
}


import mqtt from 'mqtt/dist/mqtt.js'

export default { 
	data() {
		return {
			streetLightLux: 0,
			hasPerson: false,
			ledOn: false,
			mqttStatus: '未连接',
			client: null
		}
	},
	onLoad() {
		this.initMqttClient()
	},
	onUnload() {
		this.disconnectMqttClient()
	},
	methods: { 
		// 初始化 MQTT 客户端
		initMqttClient() {  
			const MQTT_GATWAY = 'broker.emqx.io:8083/mqtt'
			const MQTT_USERNAME = 'username'
			const MQTT_PASSWORD = '123456'

			const MQTT_OPTIONS = {
				connectTimeout: 5000,
				clientId: 'uniapp_' + Math.random().toString(36).substr(2, 9),
				username: MQTT_USERNAME,
				password: MQTT_PASSWORD,
				clean: true
			}

			console.log('starting init mqtt')

			let client = null 
			// #ifdef H5
			client = mqtt.connect('ws://' + MQTT_GATWAY, MQTT_OPTIONS)
			// #endif
			// #ifdef APP-PLUS
			console.log('applus')
			client = mqtt.connect('wx://' + MQTT_GATWAY, MQTT_OPTIONS)   
			// #endif	
 
			if (!client) {
				console.warn('当前平台不支持 MQTT')
				this.mqttStatus = '当前平台不支持'
				return
			}

			this.client = client

			client.on('connect', () => {
				console.log('连接成功!')
				this.mqttStatus = '已连接'
				client.subscribe(
					Object.values(MQTT_TOPICS),
					{ qos: 0 },
					(err) => {
						if (!err) {
							console.log('订阅成功!')
						} else {
							console.error('订阅失败:', err)
						}
					}
				)
			})

			client.on('reconnect', () => {
				console.log('正在重连...')
				this.mqttStatus = '正在重连'
			})

			client.on('end', () => {
				console.log('连接断开!')
				this.mqttStatus = '已断开'
			})

			client.on('error', (err) => {
				console.error('MQTT 错误:', err)
				this.mqttStatus = '连接异常'
			})

			client.on('message', (topic, message) => {
				const msg = message.toString()
				console.log('Received Message:', msg, 'On topic:', topic)
				this.handleMessage(topic, msg)
			})
		},

		// 处理收到的消息,更新页面数据
		handleMessage(topic, message) {
			switch (topic) {
				case MQTT_TOPICS.streetLightLux:
					this.streetLightLux = Number(message) || 0
					break
				case MQTT_TOPICS.hasPerson:
					this.hasPerson = message === '1' || message === 'true'
					break
				case MQTT_TOPICS.ledOn:
					this.ledOn = message === '1' || message === 'true'
					break
				default:
					console.log('未知主题:', topic)
			}
		},

		// 发布消息(保留方法,方便后续调用)
		mqttPublish(topic, data) {
			if (this.client && this.client.connected) {
				this.client.publish(
					topic,
					JSON.stringify(data),
					{ qos: 1 }
				)
			} else {
				console.warn('MQTT 未连接,无法发布')
			}
		},

		// 断开连接
		disconnectMqttClient() {
			if (this.client) {
				this.client.end()
				this.client = null
			}
		}
	}
}
</script>

<style scoped>
	.page-wrapper {
		padding: 20rpx;
		box-sizing: border-box;
	}

	.card-grid {
		display: grid;
		grid-template-columns: repeat(2, 1fr);
		gap: 20rpx;
	}

	.card {
		background-color: #fff;
		border-radius: 10rpx;
		padding: 20rpx;
		box-sizing: border-box;
		box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
		display: flex;
		align-items: center;
	}

	/* 卡片图标样式:固定尺寸并与文字保持间距 */
	.card-icon {
		width: 80rpx;
		height: 80rpx;
		margin-right: 20rpx;
	}

	/* 卡片内容区域样式:让文字区域占据剩余水平空间 */
	.card-content {
		flex: 1;
	}

	/* 卡片标题样式:独占一行、大号字体、加粗 */
	.card-title {
		display: block;
		font-size: 32rpx;
		font-weight: bold;
		margin-bottom: 10rpx;
	}

	/* 卡片信息行样式:横向排列信息内容,居中对齐 */
	.card-info {
		display: flex;
		align-items: center;
	}

	/* 信息文字样式:设置字号和右侧间距 */
	.info-text {
		font-size: 28rpx;
		margin-right: 20rpx;
	}
</style>

然后就可以正常使用了,建议安卓模拟器使用 MuMu模拟器,这样就不用去下载安卓开发软件了。最后云打包即可。

在这里插入图片描述

在这里插入图片描述

打包完成后,下载应用,就可以看到

在这里插入图片描述

但是我感觉 uniapp 的响应总是很慢,不知道为什么。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐