6浏览
查看: 6|回复: 0

[项目] 基于FireBeetle 2 ESP32-C5的DFrobot粉丝数提示器

[复制链接]
本帖最后由 腿毛利小五郎 于 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 不用管,我也不清楚为啥
  1. [auth]
  2. phone = 18666666666
  3. password = 社区的密码
  4. app_id = 432809143856280
  5. sign_md5 = your_sign_here
  6. login_success_sign = your_login_sign
  7. [target]
  8. uid = 841942
  9. [timing]
  10. login_interval_minutes = 120
  11. refresh_interval_seconds = 10
  12. [mqtt]
  13. host = 127.0.0.1
  14. port = 1883
  15. topic = dfrobot/fans
  16. username = mqttuser
  17. password = 123456
复制代码
       以下是获取DF粉丝,转发MQTT的程序
        模拟登录请求,然后拿到token到另外一个地址去拿html响应,解析其中的粉丝。所有的配置都是从config拿的。
  1. import re, time, json, traceback, configparser
  2. import requests
  3. import paho.mqtt.client as mqtt
  4. from urllib.parse import urlparse, parse_qs
  5. CONFIG_FILE = "config.ini"
  6. HTML_DUMP_FILE = "last_response.html"
  7. REQUEST_TIMEOUT = 15
  8. DEFAULT_HEADERS = {
  9.     "User-Agent": "Mozilla/5.0",
  10.     "Accept": "*/*",
  11. }
  12. def debug_print(*a):
  13.     print(*a, flush=True)
  14. def save_html(path, html):
  15.     try:
  16.         with open(path, "w", encoding="utf-8") as f:
  17.             f.write(html)
  18.     except Exception as e:
  19.         debug_print("save_html failed:", e)
  20. def load_config():
  21.     cfg = configparser.ConfigParser()
  22.     cfg.read(CONFIG_FILE, encoding="utf-8")
  23.     return cfg
  24. def login_api(session, phone, password, app_id, sign_md5):
  25.     timestamp = str(int(time.time() * 1000))
  26.     biz_content = json.dumps({"password": password, "phone": phone}, separators=(",", ":"))
  27.     data = {
  28.         "app_auth_token": "",
  29.         "app_id": app_id,
  30.         "biz_content": biz_content,
  31.         "sign_type": "md5",
  32.         "sign": sign_md5,
  33.         "timestamp": timestamp,
  34.         "version": "1"
  35.     }
  36.     r = session.post("https://api.dfrobot.com.cn/user/login", headers={
  37.         "Content-Type":"application/x-www-form-urlencoded; charset=UTF-8",
  38.         "Origin":"https://auth.dfrobot.com.cn",
  39.         "Referer":"https://auth.dfrobot.com.cn/",
  40.         **DEFAULT_HEADERS
  41.     }, data=data, timeout=REQUEST_TIMEOUT)
  42.     try:
  43.         return r.json().get("data", {}).get("app_auth_token")
  44.     except Exception:
  45.         return None
  46. def request_login_success(session, sign, app_token):
  47.     url = (
  48.         "https://api.dfrobot.com.cn/user/login/success?"
  49.         "back_url=https://mc.dfrobot.com.cn/ucenter.php?returnUrl=https://mc.dfrobot.com.cn/portal.php"
  50.         f"&sign={sign}"
  51.     )
  52.     headers = {
  53.         "Referer":"https://auth.dfrobot.com.cn/",
  54.         "Host":"api.dfrobot.com.cn",
  55.         **DEFAULT_HEADERS
  56.     }
  57.     if app_token:
  58.         headers["Authorization"] = f"Bearer {app_token}"
  59.         session.cookies.set("app_auth_token", app_token, domain="mc.dfrobot.com.cn", path="/")
  60.     r = session.get(url, headers=headers, timeout=REQUEST_TIMEOUT, allow_redirects=True)
  61.     save_html(HTML_DUMP_FILE, r.text or "")
  62.     return r
  63. def extract_p_param_from_html(path):
  64.     try:
  65.         html = open(path, "r", encoding="utf-8").read()
  66.     except Exception:
  67.         return None
  68.     m = re.search(r"[?&]p=([A-Za-z0-9_\-%.]+)", html)
  69.     return m.group(1) if m else None
  70. def call_site_connect(session, p_value):
  71.     if not p_value:
  72.         return None
  73.     url = f"https://mc.dfrobot.com.cn/member.php?mod=logging&action=connect&p={p_value}"
  74.     r = session.get(url, headers={"Referer":"https://auth.dfrobot.com.cn", **DEFAULT_HEADERS}, timeout=REQUEST_TIMEOUT)
  75.     save_html(HTML_DUMP_FILE, r.text or "")
  76.     return r
  77. def fetch_follower_page(session, uid):
  78.     url = f"https://mc.dfrobot.com.cn/home.php?mod=follow&do=follower&uid={uid}"
  79.     r = session.get(url, headers={"Referer":f"https://mc.dfrobot.com.cn/home.php?mod=space&uid={uid}", **DEFAULT_HEADERS}, timeout=REQUEST_TIMEOUT)
  80.     save_html(HTML_DUMP_FILE, r.text or "")
  81.     return r.text or ""
  82. def parse_fans(html):
  83.     m = re.search(r"<b>\s*(\d+)\s*</b>\s*粉丝", html, re.S)
  84.     if m:
  85.         return int(m.group(1))
  86.     m2 = re.search(r"(\d+)\s*粉丝", html)
  87.     return int(m2.group(1)) if m2 else None
  88. def publish_fans_count(mqtt_client, topic, fans):
  89.     payload = json.dumps({"fans": fans, "timestamp": int(time.time())})
  90.     mqtt_client.publish(topic, payload)
  91. def login_and_prepare_session(cfg):
  92.     session = requests.Session()
  93.     session.headers.update(DEFAULT_HEADERS)
  94.     token = login_api(session, cfg["auth"]["phone"], cfg["auth"]["password"], cfg["auth"]["app_id"], cfg["auth"]["sign_md5"])
  95.     r_success = request_login_success(session, cfg["auth"]["login_success_sign"], token)
  96.     p_value = extract_p_param_from_html(HTML_DUMP_FILE)
  97.     if not p_value:
  98.         try:
  99.             final_url = r_success.url
  100.             parsed = parse_qs(urlparse(final_url).query)
  101.             if "p" in parsed:
  102.                 p_value = parsed["p"][0]
  103.         except Exception:
  104.             pass
  105.     if p_value:
  106.         call_site_connect(session, p_value)
  107.     return session
  108. def main():
  109.     cfg = load_config()
  110.     login_interval = int(cfg["timing"]["login_interval_minutes"]) * 60
  111.     refresh_interval = int(cfg["timing"]["refresh_interval_seconds"])
  112.     mqtt_client = mqtt.Client(protocol=mqtt.MQTTv311)
  113.     mqtt_user = cfg["mqtt"].get("username", "")
  114.     mqtt_pass = cfg["mqtt"].get("password", "")
  115.     if mqtt_user:
  116.         mqtt_client.username_pw_set(mqtt_user, mqtt_pass)
  117.     mqtt_client.connect(cfg["mqtt"]["host"], int(cfg["mqtt"]["port"]))
  118.     mqtt_topic = cfg["mqtt"]["topic"]
  119.     uid = cfg["target"]["uid"]
  120.     session = login_and_prepare_session(cfg)
  121.     last_login_time = time.time()
  122.     while True:
  123.         try:
  124.             now = time.time()
  125.             if now - last_login_time > login_interval:
  126.                 session = login_and_prepare_session(cfg)
  127.                 last_login_time = now
  128.             html = fetch_follower_page(session, uid)
  129.             if "请登录" in html or "未登录" in html:
  130.                 debug_print("未登录,尝试重新登录")
  131.                 session = login_and_prepare_session(cfg)
  132.                 last_login_time = time.time()
  133.                 continue
  134.             fans = parse_fans(html)
  135.             if fans is not None:
  136.                 debug_print("粉丝数:", fans)
  137.                 publish_fans_count(mqtt_client, mqtt_topic, fans)
  138.             else:
  139.                 debug_print("未能解析粉丝数")
  140.         except Exception:
  141.             traceback.print_exc()
  142.         time.sleep(refresh_interval)
  143. if __name__ == "__main__":
  144.     main()
复制代码
设备端代码
       MQTT订阅,解析显示:
  1. #include <Arduino.h>
  2. #include <Wire.h>
  3. #include <WiFi.h>
  4. #include <PubSubClient.h>
  5. #include <Preferences.h>
  6. #include "ESP32_WS2812_Lib.h"
  7. #include <U8g2lib.h>
  8. #include <ArduinoJson.h>
  9. // 硬件
  10. #define LEDS_COUNT    7
  11. #define LEDS_PIN      2
  12. #define CHANNEL       0
  13. #define SDA_PIN       9
  14. #define SCL_PIN       10
  15. // Wi‑Fi
  16. const char* WIFI_SSID     = "您的wifi";
  17. const char* WIFI_PASSWORD = "您的wifi密码";
  18. // MQTT
  19. const char* MQTT_SERVER   = "MQTT地址";
  20. const int   MQTT_PORT     = MQTT端口;
  21. const char* MQTT_USER     = "mqtt用户名";
  22. const char* MQTT_PASS     = "mqtt密码";
  23. const char* MQTT_TOPIC_FANS   = "dfrobot/fans";
  24. const char* MQTT_TOPIC_CONFIG = "devices/config";
  25. // 行为参数
  26. const unsigned long LIGHT_DURATION_MS = 5000UL; // 灯持续时间
  27. const unsigned long BLINK_INTERVAL_MS  = 500UL;  // 红灯闪烁间隔
  28. const unsigned long RAINBOW_STEP_MS    = 80UL;   // 彩虹步进间隔
  29. // 全局对象
  30. ESP32_WS2812 strip(LEDS_COUNT, LEDS_PIN, CHANNEL, TYPE_GRB);
  31. U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
  32. Preferences prefs;
  33. WiFiClient  netClient;
  34. PubSubClient mqtt(netClient);
  35. // 粉丝业务
  36. volatile int currentFans = -1;
  37. volatile int lastFans = -1;
  38. volatile unsigned long lightUntil = 0;
  39. volatile int lightMode = 0; // 0=off,1=colorful,2=red blink
  40. // 彩虹位置
  41. volatile uint8_t rainbowPos = 0;
  42. // 任务句柄
  43. TaskHandle_t ledTaskHandle;
  44. TaskHandle_t oledTaskHandle;
  45. TaskHandle_t wifiTaskHandle;
  46. TaskHandle_t mqttTaskHandle;
  47. // 灯带控制
  48. void clearStrip() {
  49.   for (int i = 0; i < strip.getLedCount(); i++) strip.setLedColorData(i, 0, 0, 0);
  50.   strip.show();
  51. }
  52. // 带偏移的彩虹显示
  53. void showColorful(uint8_t offset) {
  54.   for (int i = 0; i < strip.getLedCount(); i++) {
  55.     int pos = ((i * 256 / strip.getLedCount()) + offset) & 0xFF;
  56.     strip.setLedColorData(i, strip.Wheel(pos));
  57.   }
  58.   strip.show();
  59. }
  60. void showRed(bool on) {
  61.   for (int i = 0; i < strip.getLedCount(); i++) strip.setLedColorData(i, on ? 255 : 0, 0, 0);
  62.   strip.show();
  63. }
  64. // Wi‑Fi 连接
  65. void connectWiFi(const String& ssid, const String& pass) {
  66.   if (ssid.length() == 0) return;
  67.   WiFi.disconnect(true);
  68.   WiFi.mode(WIFI_MODE_STA);
  69.   Serial.printf("→ WiFi connecting: %s\n", ssid.c_str());
  70.   WiFi.begin(ssid.c_str(), pass.c_str());
  71.   unsigned long start = millis();
  72.   while (WiFi.status() != WL_CONNECTED && millis() - start < 15000) {
  73.     delay(200);
  74.     Serial.print(".");
  75.   }
  76.   if (WiFi.status() == WL_CONNECTED) {
  77.     Serial.printf("\n→ WiFi IP: %s\n", WiFi.localIP().toString().c_str());
  78.   } else {
  79.     Serial.println("\n→ WiFi failed");
  80.   }
  81. }
  82. void mqttCallback(char* topic, byte* payload, unsigned int length) {
  83.   String t = String(topic);
  84.   if (t == String(MQTT_TOPIC_CONFIG)) {
  85.     String msg; msg.reserve(length);
  86.     for (unsigned int i = 0; i < length; i++) msg += (char)payload[i];
  87.     Serial.printf("← MQTT [%s]: %s\n", topic, msg.c_str());
  88.     StaticJsonDocument<256> doc;
  89.     auto err = deserializeJson(doc, msg);
  90.     if (err) {
  91.       Serial.println("devices/config JSON parse error");
  92.       return;
  93.     }
  94.     if (doc.containsKey("ssid") && doc.containsKey("pass")) {
  95.       String ssid = doc["ssid"].as<String>();
  96.       String pass = doc["pass"].as<String>();
  97.       prefs.putString("wifi_ssid", ssid);
  98.       prefs.putString("wifi_pass", pass);
  99.       Serial.printf("→ Saved WiFi: %s\n", ssid.c_str());
  100.       connectWiFi(ssid, pass);
  101.     }
  102.     return;
  103.   }
  104.   if (t == String(MQTT_TOPIC_FANS)) {
  105.     StaticJsonDocument<128> doc;
  106.     DeserializationError err = deserializeJson(doc, payload, length);
  107.     if (err) {
  108.       Serial.println("fans JSON parse error");
  109.       return;
  110.     }
  111.     if (!doc.containsKey("fans")) return;
  112.     int fans = doc["fans"].as<int>();
  113.     lastFans = currentFans;
  114.     currentFans = fans;
  115.     if (lastFans >= 0) {
  116.       if (fans > lastFans) {
  117.         lightMode = 1;
  118.         lightUntil = millis() + LIGHT_DURATION_MS;
  119.         Serial.printf("粉丝增加: %d -> %d\n", lastFans, currentFans);
  120.       } else if (fans < lastFans) {
  121.         lightMode = 2;
  122.         lightUntil = millis() + LIGHT_DURATION_MS;
  123.         Serial.printf("粉丝减少: %d -> %d\n", lastFans, currentFans);
  124.       }
  125.     } else {
  126.       Serial.printf("首次粉丝数: %d\n", currentFans);
  127.     }
  128.     return;
  129.   }
  130. }
  131. void mqttTask(void* pv) {
  132.   mqtt.setServer(MQTT_SERVER, MQTT_PORT);
  133.   mqtt.setCallback(mqttCallback);
  134.   while (true) {
  135.     if (WiFi.status() == WL_CONNECTED) {
  136.       while (!mqtt.connected()) {
  137.         Serial.print("→ MQTT connect... ");
  138.         String clientId = "ESP32Client-";
  139.         clientId += String((uint32_t)ESP.getEfuseMac(), HEX);
  140.         if (mqtt.connect(clientId.c_str(), MQTT_USER, MQTT_PASS)) {
  141.           Serial.println("OK");
  142.           mqtt.subscribe(MQTT_TOPIC_CONFIG);
  143.           mqtt.subscribe(MQTT_TOPIC_FANS);
  144.           Serial.printf("→ Subscribed to %s and %s\n", MQTT_TOPIC_CONFIG, MQTT_TOPIC_FANS);
  145.         } else {
  146.           int rc = mqtt.state();
  147.           Serial.printf("Failed rc=%d\n", rc);
  148.           vTaskDelay(pdMS_TO_TICKS(5000));
  149.         }
  150.       }
  151.       mqtt.loop();
  152.     }
  153.     vTaskDelay(pdMS_TO_TICKS(100));
  154.   }
  155. }
  156. // OLED 显示
  157. void oledTask(void* pv) {
  158.   const TickType_t interval = pdMS_TO_TICKS(1000);
  159.   TickType_t nextWake = xTaskGetTickCount();
  160.   struct tm timeinfo;
  161.   while (true) {
  162.     bool hasTime = getLocalTime(&timeinfo);
  163.     char timeBuf[24]; // "YYYY-MM-DD HH:MM:SS"
  164.     if (hasTime) {
  165.       snprintf(timeBuf, sizeof(timeBuf), "%04d-%02d-%02d %02d:%02d:%02d",
  166.                timeinfo.tm_year + 1900,
  167.                timeinfo.tm_mon + 1,
  168.                timeinfo.tm_mday,
  169.                timeinfo.tm_hour,
  170.                timeinfo.tm_min,
  171.                timeinfo.tm_sec);
  172.     } else {
  173.       strncpy(timeBuf, "Time: --:--:--", sizeof(timeBuf));
  174.       timeBuf[sizeof(timeBuf)-1] = '\0';
  175.     }
  176.     u8g2.firstPage();
  177.     do {
  178.       u8g2.setFont(u8g2_font_7x14_tf);
  179.       u8g2.setCursor(5, 14);
  180.       u8g2.print("Fans Monitor");
  181.       u8g2.setFont(u8g2_font_6x12_tf);
  182.       u8g2.setCursor(0, 34);
  183.       if (currentFans < 0) u8g2.print("Current fans: --");
  184.       else {
  185.         char fansBuf[32];
  186.         snprintf(fansBuf, sizeof(fansBuf), "Current fans: %d", currentFans);
  187.         u8g2.print(fansBuf);
  188.       }
  189.       u8g2.setCursor(0, 50);
  190.       u8g2.printf("WiFi: %s", WiFi.SSID().c_str());
  191.       u8g2.setCursor(0, 62);
  192.       u8g2.print(timeBuf);
  193.     } while (u8g2.nextPage());
  194.     vTaskDelayUntil(&nextWake, interval);
  195.   }
  196. }
  197. // LED 控制任务
  198. void ledTask(void* pv) {
  199.   const TickType_t interval = pdMS_TO_TICKS(50);
  200.   TickType_t nextWake = xTaskGetTickCount();
  201.   unsigned long lastBlink = 0;
  202.   unsigned long lastRainbowStep = 0;
  203.   bool blinkState = false;
  204.   while (true) {
  205.     unsigned long now = millis();
  206.     if (now < lightUntil) {
  207.       if (lightMode == 1) {
  208.         if (now - lastRainbowStep >= RAINBOW_STEP_MS) {
  209.           rainbowPos++; // 自动溢出
  210.           lastRainbowStep = now;
  211.         }
  212.         showColorful(rainbowPos);
  213.       } else if (lightMode == 2) {
  214.         if (now - lastBlink >= BLINK_INTERVAL_MS) {
  215.           blinkState = !blinkState;
  216.           lastBlink = now;
  217.         }
  218.         showRed(blinkState);
  219.       } else {
  220.         clearStrip();
  221.       }
  222.     } else {
  223.       if (lightMode != 0) {
  224.         lightMode = 0;
  225.         clearStrip();
  226.       }
  227.     }
  228.     vTaskDelayUntil(&nextWake, interval);
  229.   }
  230. }
  231. // Wi‑Fi 任务(保持连接与 NTP)
  232. void wifiTask(void* pv) {
  233.   const TickType_t retry = pdMS_TO_TICKS(5000);
  234.   bool ntpDone = false;
  235.   String ssid = prefs.getString("wifi_ssid", String(WIFI_SSID));
  236.   String pass = prefs.getString("wifi_pass", String(WIFI_PASSWORD));
  237.   connectWiFi(ssid, pass);
  238.   while (true) {
  239.     if (WiFi.status() != WL_CONNECTED) {
  240.       Serial.println("→ Wi‑Fi lost, retry...");
  241.       connectWiFi(ssid, pass);
  242.       vTaskDelay(retry);
  243.     } else {
  244.       if (!ntpDone) {
  245.         configTime(8*3600, 0, "pool.ntp.org", "ntp.aliyun.com");
  246.         Serial.println("→ NTP sync");
  247.         ntpDone = true;
  248.       }
  249.       vTaskDelay(pdMS_TO_TICKS(10000));
  250.     }
  251.   }
  252. }
  253. // setup
  254. void setup() {
  255.   Serial.begin(115200);
  256.   prefs.begin("cfg", false);
  257.   Wire.begin(SDA_PIN, SCL_PIN, 100000);
  258.   delay(20);
  259.   strip.begin();
  260.   strip.setBrightness(30);
  261.   clearStrip();
  262.   u8g2.begin();
  263.   String ssid = prefs.getString("wifi_ssid", String(WIFI_SSID));
  264.   String pass = prefs.getString("wifi_pass", String(WIFI_PASSWORD));
  265.   connectWiFi(ssid, pass);
  266.   mqtt.setServer(MQTT_SERVER, MQTT_PORT);
  267.   mqtt.setCallback(mqttCallback);
  268.   xTaskCreate(ledTask,  "LED",   3072, NULL, 1, &ledTaskHandle);
  269.   xTaskCreate(oledTask, "OLED",  3072, NULL, 1, &oledTaskHandle);
  270.   xTaskCreate(wifiTask, "WiFi",  4096, NULL, 1, &wifiTaskHandle);
  271.   xTaskCreate(mqttTask, "MQTT",  4096, NULL, 1, &mqttTaskHandle);
  272.   Serial.println("→ Setup complete");
  273. }
  274. void loop() {
  275.   delay(1000);
  276. }
复制代码






您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

为本项目制作心愿单
购买心愿单
心愿单 编辑
[[wsData.name]]

硬件清单

  • [[d.name]]
btnicon
我也要做!
点击进入购买页面
上海智位机器人股份有限公司 沪ICP备09038501号-4 备案 沪公网安备31011502402448

© 2013-2025 Comsenz Inc. Powered by Discuz! X3.4 Licensed

mail