本帖最后由 腿毛利小五郎 于 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);
 - }
 
  复制代码
  
 
 
 
 
 
 |