/*
* ============================================================
* 项目:桌宠风格桌面环境监测站 (本地版,无WiFi)
* 功能:采集温湿度、气压、空气质量、光照,OLED显示表情/数据,
* 根据环境数据驱动舵机、灯带、蜂鸣器进行情绪反馈。
* 按键切换显示模式。
* 硬件:Arduino Uno + DHT22 + BMP280 + MQ-135 + 光敏电阻
* + OLED 0.96寸(I2C) + SG90舵机 + WS2812灯带 + 蜂鸣器
* 作者:组员A/B/C
* 日期:2026-06-22
* ============================================================
*/
// -------------------- 包含库 --------------------
#include <Wire.h> // I2C通信库
#include <Adafruit_GFX.h> // OLED图形库
#include <Adafruit_SSD1306.h> // OLED驱动库 (0.96寸, 128x64)
#include <Adafruit_BMP280.h> // BMP280气压传感器库
#include <DHT.h> // DHT温湿度传感器库
#include <Servo.h> // 舵机控制库
#include <Adafruit_NeoPixel.h> // WS2812灯带库
// ============================================================
// 一、引脚定义与全局配置
// ============================================================
// ---------- 传感器引脚 ----------
#define DHTPIN 2 // DHT22数据引脚
#define DHTTYPE DHT22 // 传感器型号
#define BMP_SDA A4 // I2C SDA (默认)
#define BMP_SCL A5 // I2C SCL (默认)
#define MQ135_PIN A0 // MQ-135模拟输出
#define LIGHT_PIN A1 // 光敏电阻模拟输出
// ---------- 显示与交互引脚 ----------
#define OLED_ADDR 0x3C // OLED I2C地址 (常见0x3C或0x3D)
#define BUTTON1 3 // 按键1 (切换显示模式)
// ---------- 反馈模块引脚 (可选) ----------
#define SERVO_PIN 5 // 舵机信号线
#define BUZZER_PIN 6 // 蜂鸣器
#define LED_PIN 7 // WS2812灯带数据线
// ---------- 定时器间隔 (毫秒) ----------
#define INTERVAL_SENSOR 2000 // 传感器采集间隔 2秒
#define INTERVAL_DISPLAY 100 // OLED刷新间隔 100ms
#define INTERVAL_BUTTON 50 // 按键扫描间隔 50ms
// ============================================================
// 二、全局对象与变量
// ============================================================
// 传感器对象
DHT dht(DHTPIN, DHTTYPE);
Adafruit_BMP280 bmp; // BMP280默认I2C
Adafruit_SSD1306 display(128, 64, &Wire, -1); // OLED (复位引脚 -1 表示不用)
// 舵机对象
Servo neckServo;
// 灯带对象 (8颗LED)
Adafruit_NeoPixel strip = Adafruit_NeoPixel(8, LED_PIN, NEO_GRB + NEO_KHZ800);
// ---------- 全局状态变量 ----------
float temperature = 0, humidity = 0, pressure = 0, airQuality = 0, lightLevel = 0;
int emotionState = 0; // 0:开心, 1:难受, 2:瞌睡, 3:惊讶
int displayMode = 0; // 0:表情模式, 1:数据模式 (按键切换)
// millis() 时间戳
unsigned long lastSensorTime = 0;
unsigned long lastDisplayTime = 0;
unsigned long lastButtonTime = 0;
// ============================================================
// 三、函数声明 (模块分组)
// ============================================================
// 传感器组
void initSensors();
void readAllSensors();
// 显示组
void initDisplay();
void updateDisplay();
void drawEmotion(int state);
void drawDataPage();
// 辅助绘制弧线 (用于表情)
void drawArc(int x0, int y0, int r, int startAngle, int endAngle, uint16_t color);
// 交互反馈组
void initActuators();
void updateEmotionFeedback(int state);
void setLEDColor(int state);
void playTone(int state);
void moveServo(int state);
// 按键处理
void handleButtons();
// 主控逻辑
void evaluateEmotion();
// ============================================================
// 四、setup() 初始化
// ============================================================
void setup() {
Serial.begin(115200); // 硬件串口用于调试
// 初始化各模块
initSensors();
initDisplay();
initActuators();
// 显示启动画面
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(10, 20);
display.println("Pet!");
display.display();
delay(2000);
// 提示:MQ-135 需要预热 (建议12-24小时,此处仅演示)
Serial.println("System ready. MQ-135 warming up...");
}
// ============================================================
// 五、loop() 主循环 (非阻塞多任务)
// ============================================================
void loop() {
unsigned long currentMillis = millis();
// ----- 任务1: 传感器采集 (2秒间隔) -----
if (currentMillis - lastSensorTime >= INTERVAL_SENSOR) {
lastSensorTime = currentMillis;
readAllSensors(); // 读取所有传感器
evaluateEmotion(); // 根据新数据评估情绪
updateEmotionFeedback(emotionState); // 更新舵机/灯带/蜂鸣器
}
// ----- 任务2: OLED刷新 (100ms间隔) -----
if (currentMillis - lastDisplayTime >= INTERVAL_DISPLAY) {
lastDisplayTime = currentMillis;
updateDisplay(); // 根据当前模式刷新屏幕
}
// ----- 任务3: 按键扫描 (50ms间隔) -----
if (currentMillis - lastButtonTime >= INTERVAL_BUTTON) {
lastButtonTime = currentMillis;
handleButtons();
}
// 其他任务可在此添加,均采用非阻塞方式
}
// ============================================================
// 六、传感器组 实现
// ============================================================
/**
* 初始化所有传感器
*/
void initSensors() {
dht.begin();
// BMP280初始化,地址0x76或0x77,默认0x76
if (!bmp.begin(0x76)) {
Serial.println("BMP280 not found! Check I2C.");
}
// 设置BMP280采样模式
bmp.setSampling(Adafruit_BMP280::MODE_NORMAL, // 正常模式
Adafruit_BMP280::SAMPLING_X2, // 温度过采样
Adafruit_BMP280::SAMPLING_X16, // 压力过采样
Adafruit_BMP280::FILTER_X16, // 滤波
Adafruit_BMP280::STANDBY_MS_500); // 待机时间
pinMode(MQ135_PIN, INPUT);
pinMode(LIGHT_PIN, INPUT);
}
/**
* 读取所有传感器数据并存入全局变量
*/
void readAllSensors() {
// 读取温湿度 (可能失败返回NAN)
float t = dht.readTemperature();
float h = dht.readHumidity();
if (!isnan(t) && !isnan(h)) {
temperature = t;
humidity = h;
} else {
Serial.println("DHT read error!");
}
// 读取气压 (单位hPa)
pressure = bmp.readPressure() / 100.0F; // 转换为百帕
// 读取空气质量 (模拟值,需校准,这里仅示例)
int raw = analogRead(MQ135_PIN);
// 简易转换: 将0-1023映射到0-100的指数 (仅供参考,需实际校准)
airQuality = map(raw, 0, 1023, 0, 100);
// 实际可参照MQ-135数据手册进行电压-浓度换算
// 读取光照 (模拟值)
lightLevel = analogRead(LIGHT_PIN); // 0-1023
// 串口调试输出
Serial.print("T: "); Serial.print(temperature);
Serial.print(" H: "); Serial.print(humidity);
Serial.print(" P: "); Serial.print(pressure);
Serial.print(" AQ: "); Serial.print(airQuality);
Serial.print(" Light: "); Serial.println(lightLevel);
}
// ============================================================
// 七、显示组 实现
// ============================================================
/**
* 初始化OLED显示屏
*/
void initDisplay() {
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
Serial.println("OLED initialization failed!");
for (;;); // 死循环
}
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.display();
}
/**
* 根据displayMode刷新屏幕
*/
void updateDisplay() {
display.clearDisplay();
if (displayMode == 0) {
drawEmotion(emotionState); // 表情模式
} else {
drawDataPage(); // 数据模式
}
display.display();
}
/**
* 绘制表情 (模拟桌宠脸)
* 使用简单图形组合,可自行扩展位图
*/
void drawEmotion(int state) {
// 绘制一个圆形脸
display.drawCircle(64, 32, 28, SSD1306_WHITE);
// 根据情绪画眼睛和嘴
switch (state) {
case 0: // 开心
// 眼睛 (实心圆)
display.fillCircle(50, 28, 4, SSD1306_WHITE);
display.fillCircle(78, 28, 4, SSD1306_WHITE);
// 嘴巴 (微笑弧线,使用辅助函数)
drawArc(64, 40, 12, 0, 180, SSD1306_WHITE);
break;
case 1: // 难受 (生气)
display.fillCircle(50, 30, 4, SSD1306_WHITE);
display.fillCircle(78, 30, 4, SSD1306_WHITE);
// 嘴巴 (下弧线,撇嘴)
drawArc(64, 50, 12, 180, 360, SSD1306_WHITE);
break;
case 2: // 瞌睡
display.fillCircle(50, 30, 4, SSD1306_WHITE);
display.fillCircle(78, 30, 4, SSD1306_WHITE);
// 眯眼 (画水平线)
display.drawLine(46, 30, 54, 30, SSD1306_WHITE);
display.drawLine(74, 30, 82, 30, SSD1306_WHITE);
// 嘴巴 (小圆点)
display.fillCircle(64, 46, 2, SSD1306_WHITE);
break;
case 3: // 惊讶
display.fillCircle(50, 28, 5, SSD1306_WHITE);
display.fillCircle(78, 28, 5, SSD1306_WHITE);
// 嘴巴 (大圆)
display.fillCircle(64, 44, 8, SSD1306_WHITE);
break;
default:
break;
}
// 在底部显示温湿度小标签
display.setTextSize(1);
display.setCursor(0, 56);
display.print("T:");
display.print(temperature, 1);
display.print("C H:");
display.print(humidity, 1);
display.print("%");
}
/**
* 绘制数据页面 (显示详细数值)
*/
void drawDataPage() {
display.setTextSize(1);
display.setCursor(0, 0);
display.print("Temp: ");
display.print(temperature, 1);
display.println(" C");
display.print("Hum : ");
display.print(humidity, 1);
display.println(" %");
display.print("Pres: ");
display.print(pressure, 1);
display.println(" hPa");
display.print("AQI : ");
display.print(airQuality, 0);
display.println("/100");
display.print("Light: ");
display.print(lightLevel);
display.println("/1023");
}
/**
* 辅助函数:画弧线 (用于表情)
* 因Adafruit_GFX无原生弧线,这里采用逐点绘制近似
*/
void drawArc(int x0, int y0, int r, int startAngle, int endAngle, uint16_t color) {
// 将角度限制在0-360,步长5度,可调整精度
for (int i = startAngle; i <= endAngle; i += 5) {
float rad = radians(i);
int x = x0 + r * cos(rad);
int y = y0 + r * sin(rad);
display.drawPixel(x, y, color);
}
}
// ============================================================
// 八、交互反馈组 实现 (舵机、灯带、蜂鸣器)
// ============================================================
/**
* 初始化舵机、灯带、蜂鸣器
*/
void initActuators() {
// 舵机
neckServo.attach(SERVO_PIN);
neckServo.write(90); // 中间角度
// 灯带
strip.begin();
strip.show(); // 全部熄灭
// 蜂鸣器
pinMode(BUZZER_PIN, OUTPUT);
}
/**
* 根据情绪状态更新反馈 (舵机、灯带、蜂鸣器)
*/
void updateEmotionFeedback(int state) {
// 控制灯带颜色
setLEDColor(state);
// 控制舵机转动角度
moveServo(state);
// 播放不同音调
playTone(state);
}
/**
* 设置WS2812灯带颜色
*/
void setLEDColor(int state) {
uint32_t color;
switch (state) {
case 0: color = strip.Color(0, 255, 0); break; // 绿色 (开心)
case 1: color = strip.Color(255, 0, 0); break; // 红色 (难受)
case 2: color = strip.Color(0, 0, 255); break; // 蓝色 (瞌睡)
case 3: color = strip.Color(255, 255, 0); break; // 黄色 (惊讶)
default: color = strip.Color(255, 255, 255);
}
for (int i = 0; i < strip.numPixels(); i++) {
strip.setPixelColor(i, color);
}
strip.show();
}
/**
* 舵机动作 (根据情绪摆动)
*/
void moveServo(int state) {
int angle = 90;
switch (state) {
case 0: angle = 90; break; // 正中
case 1: angle = 45; break; // 左偏
case 2: angle = 135; break; // 右偏
case 3: angle = 60; break; // 偏左
}
neckServo.write(angle);
}
/**
* 蜂鸣器发声 (短促提示)
*/
void playTone(int state) {
// 限制发声频率,避免过于频繁
static unsigned long lastBeep = 0;
if (millis() - lastBeep < 500) return; // 至少间隔500ms
lastBeep = millis();
int freq = 0;
switch (state) {
case 0: freq = 1000; break; // 开心: 高音
case 1: freq = 200; break; // 难受: 低音
case 2: freq = 500; break; // 瞌睡: 中音
case 3: freq = 800; break; // 惊讶: 中高音
}
if (freq > 0) {
tone(BUZZER_PIN, freq, 100); // 发声100ms,自动停止
}
}
// ============================================================
// 九、按键处理
// ============================================================
/**
* 扫描按键并执行对应动作 (仅一个按键,切换显示模式)
*/
void handleButtons() {
// 按键按下为低电平 (假设使用上拉输入)
if (digitalRead(BUTTON1) == LOW) {
// 防抖处理
static unsigned long lastDebounce = 0;
if (millis() - lastDebounce > 200) { // 防抖200ms
displayMode = (displayMode + 1) % 2; // 切换 0<->1
lastDebounce = millis();
Serial.print("Display mode: ");
Serial.println(displayMode);
}
}
}
// ============================================================
// 十、主控逻辑 (情绪评估)
// ============================================================
/**
* 根据传感器数据评估当前情绪状态
* 阈值可根据实际环境调整
*/
void evaluateEmotion() {
// 规则示例:
// 温度 > 28 或 空气质量 > 70 -> 难受
// 温度 < 18 或 光照 < 100 -> 瞌睡
// 湿度 > 80 或 气压异常 (<980 或 >1030) -> 惊讶
// 其他 -> 开心
if (temperature > 28.0 || airQuality > 70) {
emotionState = 1; // 难受
} else if (temperature < 18.0 || lightLevel < 100) {
emotionState = 2; // 瞌睡
} else if (humidity > 80.0 || pressure < 980.0 || pressure > 1030.0) {
emotionState = 3; // 惊讶
} else {
emotionState = 0; // 开心
}
}
// ============================================================
// 十一、结尾
// ============================================================
// 说明:
// 1. 需要安装库:Adafruit GFX, Adafruit SSD1306, Adafruit BMP280,
// DHT sensor library, Servo, Adafruit NeoPixel。
// 2. 硬件连接请参照引脚定义。
// 3. MQ-135 使用前建议预热12-24小时并进行校准,本代码仅为演示。
// 4. 可自定义表情位图或增加更多情绪状态。
// 5. 该版本完全离线运行,无需网络。
// ============================================================