本帖最后由 腿毛利小五郎 于 2025-10-9 00:28 编辑
项目概览
看到很多都是显示B站粉丝的小摆件,一直没有找到有人去做显示本社区粉丝的产品,于是就做了一个基于 FireBeetle 2 ESP32-C5 的“DFrobot粉丝数提示器”。ESP32 通过 Wi‑Fi , MQTT Broker(原本是想在32上做的但是代码过于臃肿,还有bug,还是在服务器上跑吧)。订阅粉丝数主题。收到粉丝变化后,设备会在 OLED 上显示当前数值,同时用 WS2812 灯带做视觉提示:粉丝增加时播放可动的彩虹流动,粉丝减少时红灯闪烁。设备还能通过 MQTT 接收配置并把 Wi‑Fi 信息保存在本地,重启后继续用上次的设置。代码框架来自我的上一个工程,基于FireBeetle 2 ESP32-C5 智能环境监测灯光控制 DF创客社区
核心功能- 自动连 Wi‑Fi,并做 NTP 同步把时间显示到 OLED。
- 订阅 MQTT 主题,实时接收粉丝数(JSON 格式),对比新旧数值触发灯光提示。
- 粉丝增加:彩虹流动效果并持续一段时间。
- 粉丝减少:红色闪烁提示并持续一段时间。
- 支持通过 MQTT 下发配置(例如更新 Wi‑Fi SSID/密码),并持久化保存(同上一个帖子)。
- Wi‑Fi 和 MQTT 都有更稳健的重连与退避策略,减少短时网络抖动带来的问题(同上一个帖子)。
硬件与软件清单运行流程
开机后设备先加载本地保存的 Wi‑Fi 信息,尝试连接路由器。连上网并同步好 NTP 时间后,开始连接 MQTT Broker。只在确认有有效 IP 和 Wi‑Fi 联通的情况下再去连 MQTT,可以避免很多无谓的重试。连上 Broker 之后订阅两个主题:一个接粉丝数据,一个接远程配置。
当收到粉丝数据时,先把消息解析成数字,和之前的数值比较:如果变多,开启“彩虹流动”并记下何时结束;如果变少,开启“红灯闪烁”。LED 动画在独立任务里跑,OLED 也在独立任务里每秒刷新屏幕并显示当前粉丝数、Wi‑Fi 名称和 NTP 时间。远程下发新 Wi‑Fi 信息时会写入本地存储并触发重连。
服务端代码和配置说明
如下是config.ini文件 其中仅需要配置自己的
查询的uid,和mqtt服务器地址端口账号密码。
app_id sign_md5 login_success_sign 不用管,我也不清楚为啥
- [auth]
- phone = 18666666666
- password = 社区的密码
- app_id = 432809143856280
- sign_md5 = your_sign_here
- login_success_sign = your_login_sign
-
- [target]
- uid = 841942
-
- [timing]
- login_interval_minutes = 120
- refresh_interval_seconds = 10
-
- [mqtt]
- host = 127.0.0.1
- port = 1883
- topic = dfrobot/fans
- username = mqttuser
- password = 123456
复制代码
以下是获取DF粉丝,转发MQTT的程序
模拟登录请求,然后拿到token到另外一个地址去拿html响应,解析其中的粉丝。所有的配置都是从config拿的。
- import re, time, json, traceback, configparser
- import requests
- import paho.mqtt.client as mqtt
- from urllib.parse import urlparse, parse_qs
-
- CONFIG_FILE = "config.ini"
- HTML_DUMP_FILE = "last_response.html"
- REQUEST_TIMEOUT = 15
-
- DEFAULT_HEADERS = {
- "User-Agent": "Mozilla/5.0",
- "Accept": "*/*",
- }
-
- def debug_print(*a):
- print(*a, flush=True)
-
- def save_html(path, html):
- try:
- with open(path, "w", encoding="utf-8") as f:
- f.write(html)
- except Exception as e:
- debug_print("save_html failed:", e)
-
- def load_config():
- cfg = configparser.ConfigParser()
- cfg.read(CONFIG_FILE, encoding="utf-8")
- return cfg
-
- def login_api(session, phone, password, app_id, sign_md5):
- timestamp = str(int(time.time() * 1000))
- biz_content = json.dumps({"password": password, "phone": phone}, separators=(",", ":"))
- data = {
- "app_auth_token": "",
- "app_id": app_id,
- "biz_content": biz_content,
- "sign_type": "md5",
- "sign": sign_md5,
- "timestamp": timestamp,
- "version": "1"
- }
- r = session.post("https://api.dfrobot.com.cn/user/login", headers={
- "Content-Type":"application/x-www-form-urlencoded; charset=UTF-8",
- "Origin":"https://auth.dfrobot.com.cn",
- "Referer":"https://auth.dfrobot.com.cn/",
- **DEFAULT_HEADERS
- }, data=data, timeout=REQUEST_TIMEOUT)
- try:
- return r.json().get("data", {}).get("app_auth_token")
- except Exception:
- return None
-
- def request_login_success(session, sign, app_token):
- url = (
- "https://api.dfrobot.com.cn/user/login/success?"
- "back_url=https://mc.dfrobot.com.cn/ucenter.php?returnUrl=https://mc.dfrobot.com.cn/portal.php"
- f"&sign={sign}"
- )
- headers = {
- "Referer":"https://auth.dfrobot.com.cn/",
- "Host":"api.dfrobot.com.cn",
- **DEFAULT_HEADERS
- }
- if app_token:
- headers["Authorization"] = f"Bearer {app_token}"
- session.cookies.set("app_auth_token", app_token, domain="mc.dfrobot.com.cn", path="/")
- r = session.get(url, headers=headers, timeout=REQUEST_TIMEOUT, allow_redirects=True)
- save_html(HTML_DUMP_FILE, r.text or "")
- return r
-
- def extract_p_param_from_html(path):
- try:
- html = open(path, "r", encoding="utf-8").read()
- except Exception:
- return None
- m = re.search(r"[?&]p=([A-Za-z0-9_\-%.]+)", html)
- return m.group(1) if m else None
-
- def call_site_connect(session, p_value):
- if not p_value:
- return None
- url = f"https://mc.dfrobot.com.cn/member.php?mod=logging&action=connect&p={p_value}"
- r = session.get(url, headers={"Referer":"https://auth.dfrobot.com.cn", **DEFAULT_HEADERS}, timeout=REQUEST_TIMEOUT)
- save_html(HTML_DUMP_FILE, r.text or "")
- return r
-
- def fetch_follower_page(session, uid):
- url = f"https://mc.dfrobot.com.cn/home.php?mod=follow&do=follower&uid={uid}"
- r = session.get(url, headers={"Referer":f"https://mc.dfrobot.com.cn/home.php?mod=space&uid={uid}", **DEFAULT_HEADERS}, timeout=REQUEST_TIMEOUT)
- save_html(HTML_DUMP_FILE, r.text or "")
- return r.text or ""
-
- def parse_fans(html):
- m = re.search(r"<b>\s*(\d+)\s*</b>\s*粉丝", html, re.S)
- if m:
- return int(m.group(1))
- m2 = re.search(r"(\d+)\s*粉丝", html)
- return int(m2.group(1)) if m2 else None
-
- def publish_fans_count(mqtt_client, topic, fans):
- payload = json.dumps({"fans": fans, "timestamp": int(time.time())})
- mqtt_client.publish(topic, payload)
-
- def login_and_prepare_session(cfg):
- session = requests.Session()
- session.headers.update(DEFAULT_HEADERS)
- token = login_api(session, cfg["auth"]["phone"], cfg["auth"]["password"], cfg["auth"]["app_id"], cfg["auth"]["sign_md5"])
- r_success = request_login_success(session, cfg["auth"]["login_success_sign"], token)
- p_value = extract_p_param_from_html(HTML_DUMP_FILE)
- if not p_value:
- try:
- final_url = r_success.url
- parsed = parse_qs(urlparse(final_url).query)
- if "p" in parsed:
- p_value = parsed["p"][0]
- except Exception:
- pass
- if p_value:
- call_site_connect(session, p_value)
- return session
-
- def main():
- cfg = load_config()
- login_interval = int(cfg["timing"]["login_interval_minutes"]) * 60
- refresh_interval = int(cfg["timing"]["refresh_interval_seconds"])
- mqtt_client = mqtt.Client(protocol=mqtt.MQTTv311)
-
- mqtt_user = cfg["mqtt"].get("username", "")
- mqtt_pass = cfg["mqtt"].get("password", "")
- if mqtt_user:
- mqtt_client.username_pw_set(mqtt_user, mqtt_pass)
-
- mqtt_client.connect(cfg["mqtt"]["host"], int(cfg["mqtt"]["port"]))
- mqtt_topic = cfg["mqtt"]["topic"]
- uid = cfg["target"]["uid"]
-
- session = login_and_prepare_session(cfg)
- last_login_time = time.time()
-
- while True:
- try:
- now = time.time()
- if now - last_login_time > login_interval:
- session = login_and_prepare_session(cfg)
- last_login_time = now
-
- html = fetch_follower_page(session, uid)
- if "请登录" in html or "未登录" in html:
- debug_print("未登录,尝试重新登录")
- session = login_and_prepare_session(cfg)
- last_login_time = time.time()
- continue
-
- fans = parse_fans(html)
- if fans is not None:
- debug_print("粉丝数:", fans)
- publish_fans_count(mqtt_client, mqtt_topic, fans)
- else:
- debug_print("未能解析粉丝数")
- except Exception:
- traceback.print_exc()
- time.sleep(refresh_interval)
-
- if __name__ == "__main__":
- main()
-
复制代码
设备端代码
MQTT订阅,解析显示:
- #include <Arduino.h>
- #include <Wire.h>
- #include <WiFi.h>
- #include <PubSubClient.h>
- #include <Preferences.h>
- #include "ESP32_WS2812_Lib.h"
- #include <U8g2lib.h>
- #include <ArduinoJson.h>
-
- // 硬件
- #define LEDS_COUNT 7
- #define LEDS_PIN 2
- #define CHANNEL 0
- #define SDA_PIN 9
- #define SCL_PIN 10
-
- // Wi‑Fi
- const char* WIFI_SSID = "您的wifi";
- const char* WIFI_PASSWORD = "您的wifi密码";
-
- // MQTT
- const char* MQTT_SERVER = "MQTT地址";
- const int MQTT_PORT = MQTT端口;
- const char* MQTT_USER = "mqtt用户名";
- const char* MQTT_PASS = "mqtt密码";
- const char* MQTT_TOPIC_FANS = "dfrobot/fans";
- const char* MQTT_TOPIC_CONFIG = "devices/config";
-
- // 行为参数
- const unsigned long LIGHT_DURATION_MS = 5000UL; // 灯持续时间
- const unsigned long BLINK_INTERVAL_MS = 500UL; // 红灯闪烁间隔
- const unsigned long RAINBOW_STEP_MS = 80UL; // 彩虹步进间隔
-
- // 全局对象
- ESP32_WS2812 strip(LEDS_COUNT, LEDS_PIN, CHANNEL, TYPE_GRB);
- U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
- Preferences prefs;
- WiFiClient netClient;
- PubSubClient mqtt(netClient);
-
- // 粉丝业务
- volatile int currentFans = -1;
- volatile int lastFans = -1;
- volatile unsigned long lightUntil = 0;
- volatile int lightMode = 0; // 0=off,1=colorful,2=red blink
-
- // 彩虹位置
- volatile uint8_t rainbowPos = 0;
-
- // 任务句柄
- TaskHandle_t ledTaskHandle;
- TaskHandle_t oledTaskHandle;
- TaskHandle_t wifiTaskHandle;
- TaskHandle_t mqttTaskHandle;
-
- // 灯带控制
- void clearStrip() {
- for (int i = 0; i < strip.getLedCount(); i++) strip.setLedColorData(i, 0, 0, 0);
- strip.show();
- }
-
- // 带偏移的彩虹显示
- void showColorful(uint8_t offset) {
- for (int i = 0; i < strip.getLedCount(); i++) {
- int pos = ((i * 256 / strip.getLedCount()) + offset) & 0xFF;
- strip.setLedColorData(i, strip.Wheel(pos));
- }
- strip.show();
- }
-
- void showRed(bool on) {
- for (int i = 0; i < strip.getLedCount(); i++) strip.setLedColorData(i, on ? 255 : 0, 0, 0);
- strip.show();
- }
-
- // Wi‑Fi 连接
- void connectWiFi(const String& ssid, const String& pass) {
- if (ssid.length() == 0) return;
- WiFi.disconnect(true);
- WiFi.mode(WIFI_MODE_STA);
- Serial.printf("→ WiFi connecting: %s\n", ssid.c_str());
- WiFi.begin(ssid.c_str(), pass.c_str());
- unsigned long start = millis();
- while (WiFi.status() != WL_CONNECTED && millis() - start < 15000) {
- delay(200);
- Serial.print(".");
- }
- if (WiFi.status() == WL_CONNECTED) {
- Serial.printf("\n→ WiFi IP: %s\n", WiFi.localIP().toString().c_str());
- } else {
- Serial.println("\n→ WiFi failed");
- }
- }
-
- void mqttCallback(char* topic, byte* payload, unsigned int length) {
- String t = String(topic);
-
- if (t == String(MQTT_TOPIC_CONFIG)) {
- String msg; msg.reserve(length);
- for (unsigned int i = 0; i < length; i++) msg += (char)payload[i];
- Serial.printf("← MQTT [%s]: %s\n", topic, msg.c_str());
-
- StaticJsonDocument<256> doc;
- auto err = deserializeJson(doc, msg);
- if (err) {
- Serial.println("devices/config JSON parse error");
- return;
- }
- if (doc.containsKey("ssid") && doc.containsKey("pass")) {
- String ssid = doc["ssid"].as<String>();
- String pass = doc["pass"].as<String>();
- prefs.putString("wifi_ssid", ssid);
- prefs.putString("wifi_pass", pass);
- Serial.printf("→ Saved WiFi: %s\n", ssid.c_str());
- connectWiFi(ssid, pass);
- }
- return;
- }
-
- if (t == String(MQTT_TOPIC_FANS)) {
- StaticJsonDocument<128> doc;
- DeserializationError err = deserializeJson(doc, payload, length);
- if (err) {
- Serial.println("fans JSON parse error");
- return;
- }
- if (!doc.containsKey("fans")) return;
- int fans = doc["fans"].as<int>();
-
- lastFans = currentFans;
- currentFans = fans;
-
- if (lastFans >= 0) {
- if (fans > lastFans) {
- lightMode = 1;
- lightUntil = millis() + LIGHT_DURATION_MS;
- Serial.printf("粉丝增加: %d -> %d\n", lastFans, currentFans);
- } else if (fans < lastFans) {
- lightMode = 2;
- lightUntil = millis() + LIGHT_DURATION_MS;
- Serial.printf("粉丝减少: %d -> %d\n", lastFans, currentFans);
- }
- } else {
- Serial.printf("首次粉丝数: %d\n", currentFans);
- }
- return;
- }
- }
-
- void mqttTask(void* pv) {
- mqtt.setServer(MQTT_SERVER, MQTT_PORT);
- mqtt.setCallback(mqttCallback);
-
- while (true) {
- if (WiFi.status() == WL_CONNECTED) {
- while (!mqtt.connected()) {
- Serial.print("→ MQTT connect... ");
- String clientId = "ESP32Client-";
- clientId += String((uint32_t)ESP.getEfuseMac(), HEX);
- if (mqtt.connect(clientId.c_str(), MQTT_USER, MQTT_PASS)) {
- Serial.println("OK");
- mqtt.subscribe(MQTT_TOPIC_CONFIG);
- mqtt.subscribe(MQTT_TOPIC_FANS);
- Serial.printf("→ Subscribed to %s and %s\n", MQTT_TOPIC_CONFIG, MQTT_TOPIC_FANS);
- } else {
- int rc = mqtt.state();
- Serial.printf("Failed rc=%d\n", rc);
- vTaskDelay(pdMS_TO_TICKS(5000));
- }
- }
- mqtt.loop();
- }
- vTaskDelay(pdMS_TO_TICKS(100));
- }
- }
-
- // OLED 显示
- void oledTask(void* pv) {
- const TickType_t interval = pdMS_TO_TICKS(1000);
- TickType_t nextWake = xTaskGetTickCount();
-
- struct tm timeinfo;
-
- while (true) {
- bool hasTime = getLocalTime(&timeinfo);
-
- char timeBuf[24]; // "YYYY-MM-DD HH:MM:SS"
- if (hasTime) {
- snprintf(timeBuf, sizeof(timeBuf), "%04d-%02d-%02d %02d:%02d:%02d",
- timeinfo.tm_year + 1900,
- timeinfo.tm_mon + 1,
- timeinfo.tm_mday,
- timeinfo.tm_hour,
- timeinfo.tm_min,
- timeinfo.tm_sec);
- } else {
- strncpy(timeBuf, "Time: --:--:--", sizeof(timeBuf));
- timeBuf[sizeof(timeBuf)-1] = '\0';
- }
-
- u8g2.firstPage();
- do {
- u8g2.setFont(u8g2_font_7x14_tf);
- u8g2.setCursor(5, 14);
- u8g2.print("Fans Monitor");
-
- u8g2.setFont(u8g2_font_6x12_tf);
- u8g2.setCursor(0, 34);
- if (currentFans < 0) u8g2.print("Current fans: --");
- else {
- char fansBuf[32];
- snprintf(fansBuf, sizeof(fansBuf), "Current fans: %d", currentFans);
- u8g2.print(fansBuf);
- }
-
- u8g2.setCursor(0, 50);
- u8g2.printf("WiFi: %s", WiFi.SSID().c_str());
-
- u8g2.setCursor(0, 62);
- u8g2.print(timeBuf);
- } while (u8g2.nextPage());
-
- vTaskDelayUntil(&nextWake, interval);
- }
- }
- // LED 控制任务
- void ledTask(void* pv) {
- const TickType_t interval = pdMS_TO_TICKS(50);
- TickType_t nextWake = xTaskGetTickCount();
- unsigned long lastBlink = 0;
- unsigned long lastRainbowStep = 0;
- bool blinkState = false;
-
- while (true) {
- unsigned long now = millis();
-
- if (now < lightUntil) {
- if (lightMode == 1) {
- if (now - lastRainbowStep >= RAINBOW_STEP_MS) {
- rainbowPos++; // 自动溢出
- lastRainbowStep = now;
- }
- showColorful(rainbowPos);
- } else if (lightMode == 2) {
- if (now - lastBlink >= BLINK_INTERVAL_MS) {
- blinkState = !blinkState;
- lastBlink = now;
- }
- showRed(blinkState);
- } else {
- clearStrip();
- }
- } else {
- if (lightMode != 0) {
- lightMode = 0;
- clearStrip();
- }
- }
-
- vTaskDelayUntil(&nextWake, interval);
- }
- }
-
- // Wi‑Fi 任务(保持连接与 NTP)
- void wifiTask(void* pv) {
- const TickType_t retry = pdMS_TO_TICKS(5000);
- bool ntpDone = false;
-
- String ssid = prefs.getString("wifi_ssid", String(WIFI_SSID));
- String pass = prefs.getString("wifi_pass", String(WIFI_PASSWORD));
- connectWiFi(ssid, pass);
-
- while (true) {
- if (WiFi.status() != WL_CONNECTED) {
- Serial.println("→ Wi‑Fi lost, retry...");
- connectWiFi(ssid, pass);
- vTaskDelay(retry);
- } else {
- if (!ntpDone) {
- configTime(8*3600, 0, "pool.ntp.org", "ntp.aliyun.com");
- Serial.println("→ NTP sync");
- ntpDone = true;
- }
- vTaskDelay(pdMS_TO_TICKS(10000));
- }
- }
- }
-
- // setup
- void setup() {
- Serial.begin(115200);
- prefs.begin("cfg", false);
-
- Wire.begin(SDA_PIN, SCL_PIN, 100000);
- delay(20);
-
- strip.begin();
- strip.setBrightness(30);
- clearStrip();
-
- u8g2.begin();
-
- String ssid = prefs.getString("wifi_ssid", String(WIFI_SSID));
- String pass = prefs.getString("wifi_pass", String(WIFI_PASSWORD));
- connectWiFi(ssid, pass);
-
- mqtt.setServer(MQTT_SERVER, MQTT_PORT);
- mqtt.setCallback(mqttCallback);
-
- xTaskCreate(ledTask, "LED", 3072, NULL, 1, &ledTaskHandle);
- xTaskCreate(oledTask, "OLED", 3072, NULL, 1, &oledTaskHandle);
- xTaskCreate(wifiTask, "WiFi", 4096, NULL, 1, &wifiTaskHandle);
- xTaskCreate(mqttTask, "MQTT", 4096, NULL, 1, &mqttTaskHandle);
-
- Serial.println("→ Setup complete");
- }
-
- void loop() {
- delay(1000);
- }
复制代码
|