天气感应彩蛋

利用 ESP8266 开发板驱动 WS2812B 灯珠,将其封装进蛋壳中制作成的智能天气灯。设备联网后能实时获取当地天气状况,通过灯光颜色直观反馈:晴天暖黄、雨天深蓝,并具备随温度变化的色彩渐变效果。
1
焊接RGB灯珠




12
请准备一个RGB灯珠、五根杜邦线和一个DC母头电源。接线时请注意区分灯珠的DIN和DOUT:DIN为信号输入端,是需要焊接信号线的位置。请将5V和GND各焊接两根线,DIN焊接一根线,用于连接ESP8266。
2
打印模型




12
egg-top.stl
1.13MB
egg-bottom.stl
1.12MB
3
接线图

将灯珠的 DIN 引脚连接至 ESP8266 的 D1 接口,同时确保灯珠的负极(GND)与 ESP8266 的 GND 共地连通。
4
编写代码




12
使用前需先在天气网站注册账号,获取 API 密钥后才能调用接口。
#include <Adafruit_NeoPixel.h>
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <ArduinoJson.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
// ============================================================
// 1. 用户配置
// ============================================================
const char* WIFI_SSID = "vivoX200";
const char* WIFI_PASSWORD = "12345678";
const char* OPENWEATHER_KEY = "你的apikey";
const char* AP_SSID = "ESP8266_LED";
const char* AP_PASSWORD = "12345678";
#define LED_PIN 5 // GPIO5 = NodeMCU D1
#define NUM_LEDS 1 // 当前只有一颗灯珠
#define DEFAULT_BRIGHTNESS 255 // 0~255
// ============================================================
// 2. 全局对象与状态
// ============================================================
Adafruit_NeoPixel strip(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);
ESP8266WebServer server(80);
uint8_t currentBrightness = DEFAULT_BRIGHTNESS;
// 0=关闭 1=纯色 2=RGB循环 3=彩虹 4=呼吸 5=闪烁 6=天气
uint8_t currentMode = 0;
uint32_t solidColor = 0;
unsigned long lastEffectUpdate = 0;
// 天气配置
String weatherCity = "Beijing";
String weatherCountry = "CN";
String weatherMain = "";
bool weatherAvailable = false;
const unsigned long WEATHER_INTERVAL = 30UL * 60UL * 1000UL;
unsigned long lastWeatherUpdate = 0;
// ============================================================
// 3. 灯光基础控制
// ============================================================
// 只在 setup() 中把 NeoPixel 内部亮度设为 255。
// 后续统一通过软件缩放 RGB,避免反复 setBrightness()
// 对 Adafruit_NeoPixel 内部像素缓存进行重复缩放。
uint32_t scaleColor(uint32_t color, uint8_t brightness) {
uint8_t r = (color >> 16) & 0xFF;
uint8_t g = (color >> 8) & 0xFF;
uint8_t b = color & 0xFF;
r = ((uint16_t)r * brightness) / 255;
g = ((uint16_t)g * brightness) / 255;
b = ((uint16_t)b * brightness) / 255;
return strip.Color(r, g, b);
}
void displayColor(uint32_t color, uint8_t brightness) {
uint32_t outputColor = scaleColor(color, brightness);
for (uint16_t i = 0; i < NUM_LEDS; i++) {
strip.setPixelColor(i, outputColor);
}
strip.show();
}
void turnOffLight() {
strip.clear();
strip.show();
}
// ============================================================
// 4. 天气颜色与天气灯光
// ============================================================
uint32_t getWeatherColor() {
String value = weatherMain;
value.toLowerCase();
if (value == "clear") {
return strip.Color(255, 190, 0); // 晴天:暖黄色
}
if (value == "clouds") {
return strip.Color(160, 170, 190); // 多云:灰蓝色
}
if (value == "rain" || value == "drizzle") {
return strip.Color(0, 100, 255); // 雨天:蓝色
}
if (value == "thunderstorm") {
return strip.Color(160, 40, 255); // 雷雨:紫色
}
if (value == "snow") {
return strip.Color(180, 220, 255); // 雪天:冰蓝色
}
if (value == "mist" || value == "fog" ||
value == "haze" || value == "smoke") {
return strip.Color(120, 130, 140); // 雾、霾:灰色
}
return strip.Color(100, 100, 100);
}
void applyWeatherLight() {
if (!weatherAvailable) {
Serial.println(F("天气数据尚未获取,暂时无法显示天气灯光"));
return;
}
displayColor(getWeatherColor(), currentBrightness);
Serial.print(F("天气灯光已更新:"));
Serial.println(weatherMain);
}
// ============================================================
// 5. URL 编码
// ============================================================
String urlEncode(const String& value) {
const char hex[] = "0123456789ABCDEF";
String encoded;
encoded.reserve(value.length() * 3);
for (size_t i = 0; i < value.length(); i++) {
uint8_t c = (uint8_t)value[i];
if ((c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
c == '-' || c == '_' || c == '.' || c == '~') {
encoded += (char)c;
} else {
encoded += '%';
encoded += hex[c >> 4];
encoded += hex[c & 0x0F];
}
}
return encoded;
}
// ============================================================
// 6. 获取天气:只解析 weather[0].main
// ============================================================
void fetchWeather() {
if (WiFi.status() != WL_CONNECTED) {
Serial.println(F("外部 WiFi 未连接,无法获取天气"));
return;
}
WiFiClient client;
HTTPClient http;
String url =
"http://api.openweathermap.org/data/2.5/weather?q=" +
urlEncode(weatherCity) + "," +
urlEncode(weatherCountry) +
"&appid=" + String(OPENWEATHER_KEY) +
"&units=metric";
Serial.println();
Serial.println(F("正在获取天气..."));
Serial.print(F("查询城市:"));
Serial.print(weatherCity);
Serial.print(',');
Serial.println(weatherCountry);
http.setTimeout(15000);
http.setReuse(false);
if (!http.begin(client, url)) {
Serial.println(F("HTTP 初始化失败"));
return;
}
int httpCode = http.GET();
Serial.print(F("HTTP 状态码:"));
Serial.println(httpCode);
if (httpCode == HTTP_CODE_OK) {
// 过滤器只保留 weather[0].main。
StaticJsonDocument<96> filter;
filter["weather"][0]["main"] = true;
// 只存储过滤后的天气字段。
StaticJsonDocument<128> doc;
DeserializationError error = deserializeJson(
doc,
http.getStream(),
DeserializationOption::Filter(filter)
);
if (error) {
Serial.print(F("天气 JSON 解析失败:"));
Serial.println(error.c_str());
} else if (doc["weather"][0]["main"].is<const char*>()) {
weatherMain = doc["weather"][0]["main"].as<String>();
weatherAvailable = weatherMain.length() > 0;
Serial.print(F("天气获取成功:"));
Serial.println(weatherMain);
if (currentMode == 6 && weatherAvailable) {
applyWeatherLight();
}
} else {
Serial.println(F("响应中没有 weather[0].main 字段"));
}
} else {
Serial.print(F("天气请求失败:"));
Serial.println(http.errorToString(httpCode));
if (httpCode > 0) {
Serial.println(http.getString());
}
}
http.end();
}
// ============================================================
// 7. 单颗灯珠效果
// ============================================================
// RGB 循环:单颗灯珠依次显示红、绿、蓝。
void rgbCycleStep() {
static uint8_t colorIndex = 0;
uint32_t colors[] = {
strip.Color(255, 0, 0),
strip.Color(0, 255, 0),
strip.Color(0, 0, 255)
};
displayColor(colors[colorIndex], currentBrightness);
colorIndex = (colorIndex + 1) % 3;
}
// 彩虹:单颗灯珠通过持续改变色相呈现彩虹变色。
void rainbowLoop() {
static uint16_t hue = 0;
if (millis() - lastEffectUpdate < 25) {
return;
}
lastEffectUpdate = millis();
uint32_t color = strip.gamma32(strip.ColorHSV(hue));
displayColor(color, currentBrightness);
hue += 384;
}
// 呼吸:单颗灯珠蓝色渐亮、渐暗。
void breathingLoop() {
static int breathLevel = 0;
static int direction = 1;
if (millis() - lastEffectUpdate < 15) {
return;
}
lastEffectUpdate = millis();
uint8_t actualBrightness =
((uint16_t)currentBrightness * breathLevel) / 255;
displayColor(strip.Color(0, 80, 255), actualBrightness);
breathLevel += direction * 3;
if (breathLevel >= 255) {
breathLevel = 255;
direction = -1;
} else if (breathLevel <= 0) {
breathLevel = 0;
direction = 1;
}
}
// 单颗灯珠没有空间位置可供流水,因此跑马灯改为橙色闪烁。
void marqueeLoop() {
static bool ledOn = false;
if (millis() - lastEffectUpdate < 300) {
return;
}
lastEffectUpdate = millis();
ledOn = !ledOn;
if (ledOn) {
displayColor(strip.Color(255, 80, 0), currentBrightness);
} else {
turnOffLight();
}
}
// ============================================================
// 8. 模式切换
// ============================================================
void setLightMode(uint8_t mode) {
currentMode = mode;
lastEffectUpdate = millis();
switch (currentMode) {
case 0:
turnOffLight();
break;
case 1:
displayColor(solidColor, currentBrightness);
break;
case 2:
rgbCycleStep();
break;
case 3:
// 立即显示彩虹效果,不等待下一次 loop。
lastEffectUpdate = millis() - 25;
rainbowLoop();
break;
case 4:
// 呼吸灯从暗到亮开始。
displayColor(strip.Color(0, 80, 255), 0);
break;
case 5:
// 立即点亮一次,之后进入闪烁。
displayColor(strip.Color(255, 80, 0), currentBrightness);
break;
case 6:
if (weatherAvailable) {
applyWeatherLight();
} else {
turnOffLight();
fetchWeather();
}
break;
}
}
// ============================================================
// 9. 网页
// ============================================================
void handleRoot() {
static const char PAGE[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="theme-color" content="#101426">
<title>ESP8266 智能灯光控制</title>
<style>
:root{--bg:#101426;--card:#1a2035;--light:#222a43;--primary:#7185ff;--text:#f4f6ff;--muted:#929bb5}
*{margin:0;padding:0;box-sizing:border-box}
body{min-height:100vh;padding:24px 16px 40px;background:radial-gradient(circle at 50% 0%,#252d55 0%,transparent 40%),var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei",sans-serif}
.container{width:100%;max-width:480px;margin:auto}
.header{padding:12px 4px 28px}
.header-top{display:flex;justify-content:space-between;align-items:center}
.logo{display:flex;align-items:center;gap:12px}
.logo-icon{width:46px;height:46px;display:flex;align-items:center;justify-content:center;border-radius:14px;background:linear-gradient(135deg,#667aff,#9b72ff);font-size:24px}
.logo h1{font-size:19px}.logo p{margin-top:5px;font-size:12px;color:var(--muted)}
.status{padding:7px 11px;border-radius:20px;background:rgba(64,210,151,.12);color:#55dba7;font-size:11px}
.card{padding:20px;margin-bottom:16px;border:1px solid rgba(255,255,255,.07);border-radius:18px;background:rgba(26,32,53,.92);box-shadow:0 8px 28px rgba(0,0,0,.12)}
.card-title{display:flex;align-items:center;gap:10px;margin-bottom:18px}
.card-icon{width:34px;height:34px;display:flex;align-items:center;justify-content:center;border-radius:10px;background:rgba(113,133,255,.14);font-size:17px}
.card-title h2{font-size:15px}.card-title p{margin-top:4px;font-size:11px;color:var(--muted)}
.mode-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}
.mode-btn{min-height:65px;display:flex;align-items:center;gap:12px;padding:12px;border:1px solid rgba(255,255,255,.07);border-radius:12px;background:var(--light);color:var(--text);font-size:13px;text-align:left;cursor:pointer}
.mode-btn:active{transform:scale(.97)}
.mode-btn.active{border-color:var(--primary);background:rgba(113,133,255,.18);color:#929fff}
.mode-icon{font-size:23px}
.color-content{display:flex;align-items:center;justify-content:space-between;gap:12px}
.color-description{font-size:13px;color:var(--muted)}
.color-control{display:flex;align-items:center;gap:12px}
input[type=color]{width:48px;height:42px;padding:3px;border:1px solid rgba(255,255,255,.07);border-radius:10px;background:var(--light)}
.action-btn{padding:11px 18px;border:0;border-radius:10px;background:linear-gradient(135deg,#667aff,#8c72ff);color:white;font-size:13px;font-weight:600;cursor:pointer}
.brightness-header{display:flex;justify-content:space-between;align-items:center}
.brightness-value{color:#929fff;font-size:20px;font-weight:650}
input[type=range]{width:100%;height:6px;margin:22px 0;appearance:none;border-radius:10px;background:#343d59}
input[type=range]::-webkit-slider-thumb{width:19px;height:19px;appearance:none;border-radius:50%;background:#929fff;border:3px solid white}
.city-inputs{display:grid;grid-template-columns:1fr 90px;gap:10px;margin-bottom:12px}
input[type=text]{width:100%;min-width:0;padding:12px;border:1px solid rgba(255,255,255,.07);border-radius:10px;outline:none;background:var(--light);color:white;font-size:13px}
.weather-actions{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.weather-actions button{width:100%}
.secondary-btn{padding:11px 10px;border:1px solid rgba(255,255,255,.07);border-radius:10px;background:var(--light);color:var(--text);font-size:13px;cursor:pointer}
.city-info{margin-top:14px;padding:11px;border-radius:9px;background:rgba(113,133,255,.08);color:var(--muted);font-size:12px;line-height:1.6;word-break:break-all}
.footer{padding:12px;text-align:center;color:#68728c;font-size:11px}
button{-webkit-tap-highlight-color:transparent}
@media(max-width:360px){.card{padding:16px}.mode-grid{gap:8px}.mode-btn{padding:10px;font-size:12px}}
</style>
</head>
<body>
<div class="container">
<header class="header"><div class="header-top">
<div class="logo"><div class="logo-icon">💡</div><div><h1>智能灯光控制</h1><p>ESP8266 · Single LED</p></div></div>
<div class="status">设备已连接</div>
</div></header>
<section class="card">
<div class="card-title"><div class="card-icon">✨</div><div><h2>灯光模式</h2><p>选择单颗灯珠的显示效果</p></div></div>
<div class="mode-grid">
<button class="mode-btn" onclick="setMode(0)"><span class="mode-icon">🌙</span><span>关闭灯光</span></button>
<button class="mode-btn" onclick="setMode(2)"><span class="mode-icon">🌈</span><span>RGB 循环</span></button>
<button class="mode-btn" onclick="setMode(3)"><span class="mode-icon">🎨</span><span>彩虹变色</span></button>
<button class="mode-btn" onclick="setMode(4)"><span class="mode-icon">💙</span><span>呼吸灯</span></button>
<button class="mode-btn" onclick="setMode(5)"><span class="mode-icon">⚡</span><span>闪烁效果</span></button>
<button class="mode-btn" onclick="setMode(6)"><span class="mode-icon">🌦️</span><span>天气灯光</span></button>
</div></section>
<section class="card">
<div class="card-title"><div class="card-icon">🎨</div><div><h2>自定义颜色</h2><p>选择颜色并应用到灯珠</p></div></div>
<div class="color-content"><div class="color-description">自定义 RGB 颜色</div>
<div class="color-control"><input type="color" id="colorPicker" value="#ff0000"><button class="action-btn" onclick="setColor()">应用颜色</button></div></div>
</section>
<section class="card">
<div class="card-title"><div class="card-icon">☀️</div><div><h2>亮度调节</h2><p>自由调整灯光亮度</p></div></div>
<div class="brightness-header"><span class="color-description">当前亮度</span><span class="brightness-value"><span id="brightnessValue">80</span><small style="font-size:12px"> / 255</small></span></div>
<input type="range" id="bright" min="0" max="255" value="80" oninput="updateBrightness()">
<button class="action-btn" onclick="setBright()">应用亮度</button>
</section>
<section class="card">
<div class="card-title"><div class="card-icon">🌤️</div><div><h2>天气设置</h2><p>配置天气查询城市</p></div></div>
<div class="city-inputs"><input type="text" id="city" placeholder="城市名称" value="Beijing"><input type="text" id="country" placeholder="国家代码" value="CN"></div>
<div class="weather-actions"><button class="action-btn" onclick="saveCity()">保存城市</button><button class="secondary-btn" onclick="refreshWeather()">立即刷新天气</button></div>
<div class="city-info" id="cityInfo">天气每 30 分钟自动更新一次</div>
</section>
<footer class="footer">ESP8266 Smart LED Controller</footer>
</div>
<script>
function api(params){return fetch('/api?'+params).then(r=>{if(!r.ok)throw new Error('HTTP '+r.status);return r.text();});}
function setMode(mode){
api('mode='+mode).then(()=>{
document.querySelectorAll('.mode-btn').forEach(b=>b.classList.remove('active'));
const index=[0,2,3,4,5,6].indexOf(mode);
if(index>=0)document.querySelectorAll('.mode-btn')[index].classList.add('active');
}).catch(()=>alert('控制请求失败,请检查设备连接'));
}
function setColor(){
const c=document.getElementById('colorPicker').value;
const r=parseInt(c.slice(1,3),16),g=parseInt(c.slice(3,5),16),b=parseInt(c.slice(5,7),16);
api('mode=1&r='+r+'&g='+g+'&b='+b).then(()=>{
document.querySelectorAll('.mode-btn').forEach(x=>x.classList.remove('active'));
}).catch(()=>alert('颜色设置失败'));
}
function updateBrightness(){document.getElementById('brightnessValue').innerText=document.getElementById('bright').value;}
function setBright(){api('bright='+document.getElementById('bright').value).catch(()=>alert('亮度设置失败'));}
function saveCity(){
const city=document.getElementById('city').value.trim();
const country=document.getElementById('country').value.trim();
if(!city){alert('请输入城市名称');return;}
api('setcity=1&city='+encodeURIComponent(city)+'&country='+encodeURIComponent(country))
.then(()=>document.getElementById('cityInfo').innerText='已设置查询城市:'+city+', '+country)
.catch(()=>alert('城市设置失败'));
}
function refreshWeather(){
document.getElementById('cityInfo').innerText='正在获取最新天气...';
api('refreshweather=1')
.then(()=>document.getElementById('cityInfo').innerText='天气请求已完成,请查看设备串口输出')
.catch(()=>document.getElementById('cityInfo').innerText='天气请求失败,请检查设备连接');
}
</script>
</body>
</html>
)rawliteral";
server.send_P(200, "text/html; charset=utf-8", PAGE);
}
// ============================================================
// 10. API 接口
// ============================================================
void handleApi() {
if (server.hasArg("mode")) {
int mode = server.arg("mode").toInt();
if (mode >= 0 && mode <= 6) {
if (mode == 1) {
uint8_t r = constrain(server.arg("r").toInt(), 0, 255);
uint8_t g = constrain(server.arg("g").toInt(), 0, 255);
uint8_t b = constrain(server.arg("b").toInt(), 0, 255);
solidColor = strip.Color(r, g, b);
}
setLightMode((uint8_t)mode);
}
}
if (server.hasArg("bright")) {
currentBrightness = constrain(server.arg("bright").toInt(), 0, 255);
// 静态模式立即按新亮度重绘。
if (currentMode == 0) {
turnOffLight();
} else if (currentMode == 1) {
displayColor(solidColor, currentBrightness);
} else if (currentMode == 6 && weatherAvailable) {
applyWeatherLight();
}
// 动态模式在下一帧自动使用新亮度。
}
if (server.hasArg("setcity") && server.arg("setcity") == "1") {
if (server.hasArg("city")) {
weatherCity = server.arg("city");
}
if (server.hasArg("country")) {
weatherCountry = server.arg("country");
}
weatherAvailable = false;
Serial.print(F("天气城市已修改:"));
Serial.print(weatherCity);
Serial.print(',');
Serial.println(weatherCountry);
}
if (server.hasArg("refreshweather") &&
server.arg("refreshweather") == "1") {
fetchWeather();
lastWeatherUpdate = millis();
}
server.send(200, "text/plain; charset=utf-8", "OK");
}
// ============================================================
// 11. 初始化
// ============================================================
void setup() {
Serial.begin(115200);
delay(100);
// 初始化灯珠。NeoPixel 内部亮度固定为 255。
strip.begin();
strip.setBrightness(255);
strip.clear();
strip.show();
// AP + STA 双模式。
WiFi.mode(WIFI_AP_STA);
bool apSuccess = WiFi.softAP(AP_SSID, AP_PASSWORD);
Serial.println();
if (apSuccess) {
Serial.println(F("AP 热点启动成功"));
Serial.print(F("热点名称:"));
Serial.println(AP_SSID);
Serial.print(F("热点 IP:"));
Serial.println(WiFi.softAPIP());
} else {
Serial.println(F("AP 热点启动失败"));
}
// 发起一次外部 WiFi 连接,不添加自动重连。
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print(F("正在连接外部 WiFi"));
unsigned long connectStart = millis();
while (WiFi.status() != WL_CONNECTED &&
millis() - connectStart < 15000) {
delay(250);
Serial.print('.');
yield();
}
Serial.println();
if (WiFi.status() == WL_CONNECTED) {
Serial.println(F("外部 WiFi 连接成功"));
Serial.print(F("STA IP:"));
Serial.println(WiFi.localIP());
} else {
Serial.println(F("外部 WiFi 未连接,AP 网页控制仍可使用"));
}
server.on("/", HTTP_GET, handleRoot);
server.on("/api", HTTP_GET, handleApi);
server.begin();
Serial.println(F("网页服务器已启动"));
Serial.print(F("手机连接热点后访问:http://"));
Serial.println(WiFi.softAPIP());
// 启动时尝试获取一次天气。
if (WiFi.status() == WL_CONNECTED) {
fetchWeather();
}
lastWeatherUpdate = millis();
}
// ============================================================
// 12. 主循环
// ==========================================================
5
组装与测试




123
将 WS2812B 灯珠固定在蛋壳底部,并使用热熔胶进行加固。设备通电联网后,只需在浏览器中输入 ESP8266 的 IP 地址,即可进入控制页面进行灯光设置。
0
0
0
qq空间
微博
复制链接
分享 更多相关项目
猜你喜欢
评论/提问(已发布 0 条)
0