一、硬件介绍
1、主控开发板
使用的是XIAO ESP32S3开发板,集成ESP32S3 32位双核处理器芯片,最高运行频率240MHz,板载8MB PSRAM和8MB FLASH

功能引脚示意图

2、扩展底板
一款支持XIAO系列的功能扩展板,具有丰富的外设:包括OLED、RTC、可扩展内存、无源蜂鸣器、RESET/用户按钮、5V舵机连接器等;

主要特性

功能引脚示意图

3、Grove Creator Kit 器件
在本次开发中,使用到的 `Grove Creator Kit 套件` 中的器件如下:
Grove - Temperature & Humidity Sensor (DHT11)
这款温湿度传感器提供预校准的数字输出,独特的电容式传感器元件测量相对湿度,温度通过负温度系数 (NTC) 热敏电阻测量;

主要特性

Grove - Light Sensor
Grove - 光传感器集成了一个光敏电阻来检测光强度,当光强度增加时,光敏电阻的阻值会减小,板载的双运放芯片LM358产生与光强度相对应的电压;
输出信号是模拟值,光线越亮,数值越大;

主要特性

Grove - Chainable RGB LED
基于P9813芯片,是一款全彩LED驱动器,提供了3个恒流驱动器以及256级灰度的调制输出;
通过2线传输(数据和时钟)与MCU通信;

主要特性
工作电压:5V
工作电流:20mA
通信协议:串行
Grove - Sound Sensor
Grove - 声音传感器可以检测环境的声音强度,该模块的主要组件是一个简单的麦克风,基于LM358放大器和驻极体麦克风;
> 该模块的输出是模拟信号;

主要特性

Grove - Buzzer
> Grove - 蜂鸣器模块的主要组件是一个压电蜂鸣器压;
> 压电蜂鸣器可以连接到数字输出,当输出为高电平时会发出音调;

主要特性

二、功能实现
1、硬件搭建
将光传感器、温湿度传感器、RGB LED、声音传感器、蜂鸣器等模块,通过Grove接口分别连接到扩展板和开发板对应的引脚上;

实物搭建效果:

2、功能效果
【小型环境监测站】:实现对室内环境温湿度、光照强度、环境响度等数据实时监测,并通过RGB LED的不同颜色变化反馈当前的环境状态;
以及当相关参数异常时触发蜂鸣器警报声,并且OLED屏幕 / WIFI网页端实时显示监测到的相关数据等信息;
温湿度检测:通过DHT11数字传感器,获取当前环境温度与相对湿度;
光照强度检测:通过光敏传感器输出模拟量,获取当前环境光强百分比(0~100%);
环境响度检测:通过声音传感器输出模拟量,获取当前环境响度大小(安静/有声音/声音很大);
RGB LED:通过不同RGB颜色的变化,反映当前环境状态等级(绿色/蓝色/黄色/红色);
警报触发条件
温度 > 33℃
湿度 > 80%
光照强度< 40%
声音振动幅度>150 (声音很大)
正常情况下:RGB LED为绿色;
当任意1项条件满足:RGB LED变为蓝色;
当任意2项条件满足:RGB LED变为黄色;
当3项及以上条件满足:RGB LED变为红色,同时触发蜂鸣器警报声音;
主要运行流程图

三、代码编写
主要相关代码
> OLED屏幕:显示温湿度、光照强度信息;
> 网页UI端:显示温湿度、光照强度、响度、警报项数等信息;
#include <DHT11.h>
#include <U8x8lib.h>
#include <Wire.h>
#include <ChainableLED.h>
#include <WiFi.h>
#include <WebServer.h>
// WiFi配置
#define WIFI_SSID "WIFI"
#define WIFI_PASSWORD "WIFI_PASS"
WebServer server(80);
// 引脚配置
#define LIGHT_PIN A3
#define DHT11_PIN D7
#define RGB_CIN D9
#define RGB_DIN D10
#define Sound_PIN A2
#define BUZZER_PIN D0
// OLED/DHT11/RGB LED配置
U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8(/* reset=*/ U8X8_PIN_NONE, /* clock=*/ SCL, /* data=*/ SDA );
DHT11 dht11(DHT11_PIN);
ChainableLED leds(RGB_CIN, RGB_DIN, 1);
// 相关传感器变量
int lightValue = 0;
int lightPercent = 0;
int temperature = 0;
int humidity = 0;
int dhtResult = -1;
int lastValidTemp = 0;
int lastValidHumi = 0;
bool hasValidData = false;
int adcAvg;
int amplitude;
const int baseLevel = 2020;
int alarmCount = 0;
String soundLevel;
unsigned long timerLT = 0;
const unsigned long REFRESH_RATE = 500; // 光强刷新时间ms
unsigned long timerHum = 0;
const unsigned long intervalHum = 2000; //DHT11 刷新时间
unsigned long buzzerTimer = 0;
bool buzzerState = false;
const unsigned long buzzerOnMs = 150; // 蜂鸣器持续时间
const unsigned long buzzerOffMs = 150;
void handleJsonData(){
String json = "{";
if(hasValidData){
json += "\"temp\":" + String(lastValidTemp);
json += ",\"humi\":" + String(lastValidHumi);
}else{
json += "\"temp\":null";
json += ",\"humi\":null";
}
json += ",\"light\":" + String(lightPercent);
json += ",\"sound\":\"" + soundLevel + "\"";
json += ",\"alarm\":" + String(alarmCount);
json += "}";
server.send(200, "application/json", json);
}
String getHtmlPage(){
String html = R"HTML(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>小型环境监测站</title>
<style>
*{box-sizing:border-box;margin:0;padding:0;font-family:"Microsoft Yahei",sans-serif;}
body{
background:#121422;
color:#e8ecff;
padding:40px 20px;
}
.wrap{
max-width:520px;
margin:0 auto;
}
h1{
text-align:center;
font-size:42px;
color:#62d0ff;
margin-bottom:40px;
font-weight:bold;
letter-spacing:3px;
}
.main-card{
background:#1e223b;
padding:36px 32px;
border-radius:20px;
box-shadow: 0 8px 30px #0b0d18;
border:1px solid #303558;
}
.row{
display:flex;
justify-content:space-between;
padding:18px 4px;
border-bottom:1px solid #34395c;
font-size:22px;
}
.row:last-child{
border-bottom:none;
}
.label-text{
color:#b1b8e0;
}
.val-text{
color:#ffffff;
font-weight:bold;
}
.warn{
color:#ff4444 !important;
}
</style>
</head>
<body>
<div>
<h1>小型环境监测站</h1>
<div>
<div>
<span>温度</span>
<span id="val_temp">-- ℃</span>
</div>
<div>
<span>湿度</span>
<span id="val_humi">-- %RH</span>
</div>
<div>
<span>光强</span>
<span id="val_light">-- %</span>
</div>
<div>
<span>响度</span>
<span id="val_sound">--</span>
</div>
<div>
<span>警报项数</span>
<span id="val_alarm">--</span>
</div>
</div>
</div>
<script>
async function fetchData(){
try{
const res = await fetch("/data");
const d = await res.json();
//温度
const tempDom = document.getElementById("val_temp");
if(d.temp!==null){
tempDom.innerText = d.temp+" ℃";
if(d.temp > 33) tempDom.classList.add("warn");
else tempDom.classList.remove("warn");
}else{
tempDom.innerText = "-- ℃";
tempDom.classList.remove("warn");
}
//湿度
const humiDom = document.getElementById("val_humi");
if(d.humi!==null){
humiDom.innerText = d.humi+" %RH";
if(d.humi > 80) humiDom.classList.add("warn");
else humiDom.classList.remove("warn");
}else{
humiDom.innerText = "-- %RH";
humiDom.classList.remove("warn");
}
//光强
const lightDom = document.getElementById("val_light");
lightDom.innerText = d.light + " %";
if(d.light < 40) lightDom.classList.add("warn");
else lightDom.classList.remove("warn");
//响度
const soundDom = document.getElementById("val_sound");
soundDom.innerText = d.sound;
if(d.sound === "声音很大") soundDom.classList.add("warn");
else soundDom.classList.remove("warn");
//警报项数
document.getElementById("val_alarm").innerText = d.alarm;
}catch(e){
}
}
setInterval(fetchData,500);
fetchData();
</script>
</body>
</html>
)HTML";
return html;
}
void handleRoot() {
server.send(200, "text/html", getHtmlPage());
}
void wifiInit(){
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
int cnt=0;
while(WiFi.status() != WL_CONNECTED && cnt<20){
delay(500);
Serial.print(".");
cnt++;
}
if(WiFi.status() == WL_CONNECTED){
Serial.println("\nWiFi连接成功!");
Serial.print("网页访问地址:http://");
Serial.println(WiFi.localIP());
}else{
Serial.println("\nWiFi连接失败,请检查账号密码");
}
server.on("/", handleRoot);
server.on("/data",handleJsonData);
server.begin();
Serial.println("Web服务已启动");
}
//OLED 数值更新
void refreshOled(){
u8x8.setCursor(3,1);
u8x8.print("Light:");
u8x8.print(lightPercent);
u8x8.print("% ");
u8x8.setCursor(3,3);
u8x8.print("Temp:");
if(hasValidData)
{
u8x8.print(lastValidTemp);
u8x8.print("C ");
}else{
u8x8.print("-- ");
}
u8x8.setCursor(3,5);
u8x8.print("Humi:");
if(hasValidData)
{
u8x8.print(lastValidHumi);
u8x8.print("% ");
}
else
{
u8x8.print("-- ");
}
}
void setup()
{
Serial.begin(115200);
u8x8.begin();
u8x8.setFlipMode(0);
u8x8.setFont(u8x8_font_7x14_1x2_r);
pinMode(DHT11_PIN, INPUT);
pinMode(LIGHT_PIN, INPUT);
pinMode(Sound_PIN, INPUT);
pinMode(BUZZER_PIN, OUTPUT);
digitalWrite(BUZZER_PIN, LOW);
u8x8.clear();
leds.setColorRGB(0,0,255,0);
delay(800);
wifiInit();
}
void loop()
{
unsigned long now = millis();
server.handleClient();
//DHT11温湿度 2s读取一次
if(now - timerHum >= intervalHum)
{
timerHum = now;
dhtResult = dht11.readTemperatureHumidity(temperature, humidity);
if(dhtResult == 0){
lastValidTemp = temperature;
lastValidHumi = humidity;
hasValidData = true;
}
}
bool dhtValid = (dhtResult == 0);
//声音采样
long sum = 0;
const int sampleCnt = 32;
for(int i = 0; i < sampleCnt; i++)
{
sum += analogRead(Sound_PIN);
}
adcAvg = sum / sampleCnt;
amplitude = abs(adcAvg - baseLevel);
//响度等级
if(amplitude < 30){
soundLevel = "安静";
}else if(amplitude < 150){
soundLevel = "有声音";
}else{
soundLevel = "声音很大";
}
//警报条件统计
alarmCount = 0;
bool cond1 = hasValidData && (lastValidTemp > 33);
bool cond2 = hasValidData && (lastValidHumi > 80);
bool cond3 = (lightPercent < 40);
bool cond4 = (amplitude > 150);
if(cond1) alarmCount++;
if(cond2) alarmCount++;
if(cond3) alarmCount++;
if(cond4) alarmCount++;
//RGB LED颜色反馈
if(alarmCount == 0)
{
leds.setColorRGB(0, 0, 255, 0); // 绿
}
else if(alarmCount == 1)
{
leds.setColorRGB(0, 0, 0, 255);
}
else if(alarmCount == 2)
{
leds.setColorRGB(0, 255, 255, 0);
}
else if(alarmCount >=3)
{
leds.setColorRGB(0, 255, 0, 0);
}
//蜂鸣器 警报
if(alarmCount >=3)
{
if(buzzerState == false && (now - buzzerTimer >= buzzerOffMs))
{
digitalWrite(BUZZER_PIN, HIGH);
buzzerState = true;
buzzerTimer = now;
}
if(buzzerState == true && (now - buzzerTimer >= buzzerOnMs))
{
digitalWrite(BUZZER_PIN, LOW);
buzzerState = false;
buzzerTimer = now;
}
}
else
{
digitalWrite(BUZZER_PIN, LOW);
buzzerState = false;
}
// 光强 500ms更新
if(now - timerLT >= REFRESH_RATE){
timerLT = now;
lightValue = analogRead(LIGHT_PIN);
lightPercent = map(lightValue, 0, 4095, 0, 100);
lightPercent = constrain(lightPercent,0,100); // 0~100%
refreshOled();
}
}四、程序烧录
1、连接USB数据线至开发板;
2、选择端口号对应的开发板;
3、点击 `上传` 烧录程序到开发板上;

五、效果演示
OLED屏幕上实时显示当前环境的部分相关数据,同时通过串口打印当前网页端的访问地址,可查看更多当前环境的相关信息;

网页端:当相关参数超过设定范围时,将会变为红色字体显示;


六、总结
在Grove Creator Kit - α套件中,大多数模块都是采用的5V供电,以及部分是3V3供电,因此在实际使用过程中,需要单独引出5V供电引脚用来给这些模块供电,由于这些模块都是采用的Grove接口,因此在搭建过程中相比于杜邦线连接也比较方便;特别是套件中的摇杆模块、温湿度传感器、LED等模块,非常适合在不同的项目中进行快速原型验证和进行开发使用!
我要赚赏金
