本帖最后由 岑剑伟 于 2025-5-19 13:17 编辑
小智实现B站粉丝数量实时监测--基于ESP32-S3- AI CAM
本期主要功能是通过调用 B 站 API 获取指定 B 站用户的粉丝数量,并在 Wi-Fi 连接状态变化时自动更新粉丝数量,同时支持定时更新和手动刷新粉丝数量。
闲聊
Iot 才是小智的精髓所在,因为一般人可以发力的点只有Iot了,试问我们需要自立门户,再造一个小智吗?我们缺聊天工具吗,我们没有人工智能助手吗?我们有很多可选项,小智不同的地方是在嵌入式系统引入了语音大模型,为冰冷的开发板注入了美丽的灵魂,它是语言模型和嵌入式系统功能开发利用的胶水。利用小智智能副驾才是是我们的目标,因此基于Iot 组件可以实现很多堪称奇幻的项目。今天我们一起来以“监测B站粉丝数量”功能为例,一起探索小智的无限可能。 (即使可能你对B站粉丝数量不关心,我们学习是解题思路而不是光要答案)
本次作业是 在上一项目的基础上继续推进
ESP32-S3 AI CAM移植小智手把手教程
把本次作业的 产生的 文件放在 Ai_cam 文件夹下,板卡主文件(dfrobot_esp32_s3_ai_cam.cc),补充修改已有的iot初始化函数,即可开始享受。
- void InitializeIot()
- {
- auto &thing_manager = iot::ThingManager::GetInstance();
- thing_manager.AddThing(iot::CreateThing("Speaker"));
- thing_manager.AddThing(iot::CreateThing("BilibiliFans"));
- }
复制代码
(一)刷新粉丝数量的方法
RefreshFollowerCount 方法:
该方法是获取粉丝数量的核心方法。
B站为查询粉丝数量提供了一个 API 接口
- https://api.bilibili.com/x/relation/stat?vmid=396355825
-
- {
- "code": 0,
- "message": "0",
- "ttl": 1,
- "data": {
- "mid": 396355825,
- "following": 399,
- "whisper": 0,
- "black": 0,
- "follower": 109
- }
- }
复制代码
我们首要任务是要获取这个简单的Json数据,数据到手后再慢慢提取数据。 在这里根据我们的经验和常识,我们知道需要用httpget的方法去获取网页数据,之前做过天气获取的项目的同学都知道。 那么在这里我们需要像天气获取项目那样新开一个httpget的接口吗!有个比较迷惑的问题,小智一开机不是已经联网了吗,我们还需要重来一次吗,重新输入wifi密码吗?我们开一个httpget的请求会不会破坏掉小智系统的工作流程呀?我们可以有什么优雅的解决办法呢。
通过前期的探索,我们知道小智板卡功能支持里面暴露了很多工具资源,我们是可以直接拿来用的。板卡模板中提供了16 个公共功能函数按功能划分为 5 类。这些是现成,当然我们也可以自己继续拓展板卡的功能,例如大家都很想加的摄像头功能。从下表中我们看到“网络功能”中有Http、WebSocket、Mqtt、Udp。只要是基于这些连接方式的应用我们都可以大胆畅想,发挥你的聪明才智。这里我们顺理成章地使用“CreateHttp()” 函数,免去很多构建的麻烦和冲突处理。
功能分类
|
函数名
|
功能描述
|
实例获取
|
GetInstance()
|
获取 Board 单例实例(通过create_board()创建)
|
板卡信息查询
|
GetBoardType()
|
获取板卡类型(纯虚函数,需子类实现)
|
GetUuid()
|
获取设备唯一标识(默认实现)
|
GetJson()
|
获取板卡 JSON 信息(默认实现)
|
硬件控制接口
|
GetBacklight()
|
获取背光控制接口(默认返回nullptr)
|
GetLed()
|
获取 LED 控制接口(默认实现)
|
|
GetAudioCodec()
|
获取音频编解码器接口(纯虚函数)
|
|
GetDisplay()
|
获取显示器接口(默认实现)
|
网络功能
|
CreateHttp()
|
创建 HTTP 客户端(纯虚函数)
|
CreateWebSocket()
|
创建 WebSocket 客户端(纯虚函数)
|
CreateMqtt()
|
创建 MQTT 客户端(纯虚函数)
|
CreateUdp()
|
创建 UDP 客户端(纯虚函数)
|
StartNetwork()
|
启动网络连接(纯虚函数)
|
GetNetworkStateIcon()
|
获取网络状态图标(纯虚函数)
|
电源与状态管理
|
GetBatteryLevel()
|
获取电池电量、充电状态(默认实现)
|
SetPowerSaveMode()
|
设置电源节能模式(纯虚函数)
|
1. CreateHttp()函数:它的作用是创建HTTP客户端,用于实现与HTTP服务器之间的通信。借助这个函数,设备能够发起HTTP请求,例如获取网页内容、向服务器提交数据等。这在许多需要与网络服务器进行数据交互的场景中非常有用,比如从服务器下载配置文件、上传设备运行状态数据等。
2. CreateWebSocket()函数:它用于创建WebSocket客户端,WebSocket协议使得客户端和服务器之间能够建立全双工通信通道。通过该函数创建的WebSocket客户端,设备可以实时接收服务器推送的消息,同时也能向服务器发送消息,实现双向实时通信。在实时数据传输场景,如实时监控、即时通讯等方面具有广泛应用。
3. CreateMqtt()函数:MQTT是一种轻量级的消息发布/订阅协议,适用于低带宽、不稳定网络环境下的物联网设备通信。使用此函数创建的MQTT客户端,设备能够方便地与MQTT服务器进行连接,订阅感兴趣的主题并接收相关消息,也可以向特定主题发布消息。常用于智能家居、工业物联网等领域中设备与服务器之间的消息交互。
4. CreateUdp()函数:它负责创建UDP客户端,UDP协议具有低延迟的特点,适合一些对实时性要求较高但对数据准确性要求相对较低的应用场景。通过该函数创建的UDP客户端,设备能够在网络中以UDP协议进行数据传输,例如在一些实时视频流、音频流传输或者简单的网络探测等应用中发挥作用。
函数具体实现:
bool RefreshFollowerCount()
{
if (uid_.empty())
{
ESP_LOGE(TAG, "UID is not set");
return false;
}
if (refresh_in_progress_)
{
ESP_LOGI(TAG, "Refresh already in progress");
return false;
}
refresh_in_progress_ = true;
auto &board = Board::GetInstance();
auto http = board.CreateHttp();
std::string url = "https://api.bilibili.com/x/relation/stat?vmid=" + uid_;
http->SetHeader("User-Agent", "Mozilla/5.0");
if (!http->Open("GET", url))
{
ESP_LOGE(TAG, "Failed to open HTTP connection");
delete http;
refresh_in_progress_ = false;
return false;
}
std::string response = http->GetBody();
delete http;
cJSON *root = cJSON_Parse(response.c_str());
if (root == NULL)
{
ESP_LOGE(TAG, "Failed to parse JSON response");
refresh_in_progress_ = false;
return false;
}
cJSON *code = cJSON_GetObjectItem(root, "code");
if (code == NULL || code->valueint != 0)
{
ESP_LOGE(TAG, "API returned error code");
cJSON_Delete(root);
refresh_in_progress_ = false;
return false;
}
cJSON *data = cJSON_GetObjectItem(root, "data");
if (data == NULL)
{
ESP_LOGE(TAG, "No data field in response");
cJSON_Delete(root);
refresh_in_progress_ = false;
return false;
}
cJSON *follower = cJSON_GetObjectItem(data, "follower");
if (follower == NULL)
{
ESP_LOGE(TAG, "No follower field in data");
cJSON_Delete(root);
refresh_in_progress_ = false;
return false;
}
follower_count_ = follower->valueint;
ESP_LOGI(TAG, "B站用户 %s 的粉丝数:%d", uid_.c_str(), follower_count_);
cJSON_Delete(root);
refresh_in_progress_ = false;
return true;
}
|
首先检查用户 UID 是否为空以及是否正在进行刷新操作,避免重复请求。然后创建 HTTP 请求对象,设置请求头和请求 URL,发送 GET 请求。获取响应后,使用 cJSON 库解析返回的 JSON 数据,将粉丝数量存储在 follower_count_ 成员变量中。
(二)Iot项目构造
我们按照套路来构建iot项目,难度也不大,因为这期内容都是务虚的,没有牵涉到硬件驱动,传感器数据采集的功能。
我们给Iot添加了 方法定义Refresh(刷新粉丝数量)、Report(汇报粉丝数量)两个方法函数,一个无关紧要的uid属性函数。
public:
BilibiliFans() : Thing("BilibiliFans", "B站粉丝数量")
{
// 定义属性
properties_.AddStringProperty("uid", "B站用户ID", [this]() -> std::string
{ return uid_; });
properties_.AddNumberProperty("follower_count", "粉丝数量", [this]() -> int
{ return follower_count_; });
// 定义方法
methods_.AddMethod("Refresh", "刷新粉丝数量", ParameterList(), [this](const ParameterList ¶meters)
{
ESP_LOGI(TAG, "刷新B站粉丝数量");
RefreshFollowerCount();
});
methods_.AddMethod("Report", "汇报粉丝数量", ParameterList(), [this](const ParameterList ¶meters)
{
ESP_LOGI(TAG, "汇报B站粉丝数量");
return follower_count_;
// 注意:实际的汇报效果是通过LLM理解粉丝数属性值来实现的
});
}
~BilibiliFans()
{
}
|
本来以为故事到处就结束了,但实际运行有个现象,让故事不够完美。 就是我们第一次让小智刷新B站粉丝数量的时候,它会播报我的粉丝数量为零。 通过串口查看,其他查询已经执行并已经获取粉丝数据,这我们也可以理解和接受,原因就是播报的音频数据在本轮对话较早之前已经生成了,需要在下一轮次对话,才能生效,不明真相的网友对此也很不解。其实这个现象虾哥也做个说明的,大家可以去翻看小智源码readme里面的iot教程。 为了完美解决这个问题,我们可以直观地想到,开机的时候就执行查询更新一次数据,或者定时查询数据就可以解决这个问题了。实际测试用过直接执行一次查询、多任务执行和定时器执行更新数据都会导致系统崩溃,无限重启状态。 分析其中原因是 因为系统启动之初 ,网络还没准备好就开始执行查询任务了,才会死机。这样这个问题就比较’tricky’了。

其实我们可以根据idf原生提供网络事件库来跟踪网络的问题,待到网络已经准备好了,我再执行查询任务,就有了下面这些网络事件的内容。
class BilibiliFans : public iot::Thing {
private:
// 成员变量
std::string uid_ = "396355825"; // 用户UID(硬编码)
int follower_count_ = -1; // 粉丝数缓存
bool refresh_in_progress_ = false; // 刷新状态锁
esp_timer_handle_t clock_timer_handle_; // 定时器句柄
// 核心方法
bool RefreshFollowerCount(); // 执行API请求
void InitializeEvent(); // 初始化事件监听
static void wifi_event_handler(...); // Wi-Fi事件回调
void OnWifiConnected(); // 连接成功处理
void OnWifiDisconnected(); // 断开连接处理
public:
BilibiliFans(); // 构造函数
~BilibiliFans(); // 析构函数
};
|
(三)Wi-Fi 事件处理函数
wifi_event_handler 函数:当 Wi-Fi 连接成功(获取到 IP 地址)时,调用 OnWifiConnected 方法;当 Wi-Fi 断开连接时,调用 OnWifiDisconnected 方法。
static void wifi_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
{
BilibiliFans *instance = static_cast(arg);
if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP)
{
ESP_LOGI("BiliFans", "Wi-Fi connected, updating fans count...");
instance->OnWifiConnected();
}
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED)
{
ESP_LOGW("BiliFans", "Wi-Fi disconnected, pausing update timer...");
instance->OnWifiDisconnected();
}
}
|
•
(1)Wi-Fi 连接和断开处理方法
void OnWifiConnected()
{
wifi_connected_ = true;
UpdateFansCount();
if (clock_timer_handle_)
{
esp_timer_start_periodic(clock_timer_handle_, 1 * 60 * 1000000);
}
}
void OnWifiDisconnected()
{
wifi_connected_ = false;
if (clock_timer_handle_)
{
esp_timer_stop(clock_timer_handle_);
}
}
|
•OnWifiConnected:当 Wi-Fi 连接成功时,标记 Wi-Fi 已连接,调用 UpdateFansCount 方法更新粉丝数量,并启动定时器。
•OnWifiDisconnected:当 Wi-Fi 断开连接时,标记 Wi-Fi 已断开,停止定时器。
•
(2)初始化事件处理
通过 esp_timer 创建 1 分钟间隔的定时器,定时器的回调函数为 UpdateFansCount 方法。仅在 Wi-Fi 连接时激活定时器,确保在网络可用时定时更新粉丝数量。
void InitializeEvent()
{
esp_timer_create_args_t clock_timer_args = {
.callback = [](void *arg)
{
BilibiliFans *instance = (BilibiliFans *)(arg);
instance->UpdateFansCount();
},
.arg = this,
.dispatch_method = ESP_TIMER_TASK,
.name = "BiliFansUpdateTimer",
.skip_unhandled_events = true};
esp_timer_create(&clock_timer_args, &clock_timer_handle_);
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &BilibiliFans::wifi_event_handler, this));
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &BilibiliFans::wifi_event_handler, this));
}
|

看到开机就可以自动刷新粉丝数量了!
(四)项目总体评价
实时性:通过定时刷新和手动刷新机制,确保粉丝数量数据的实时性。
稳定性:在 Wi-Fi 连接状态变化时自动处理,避免在网络不可用时进行无效请求,提高系统稳定性。
本项目基于ESP32-S3 AI CAM 板卡实现但具有通用性,可以广泛移植。

(五) 学习与成长
在物联网开发领域,ESP32 芯片与 ESP-IDF 框架的组合已成为硬件开发的黄金搭档。然而对于初学者而言,复杂的底层驱动、异步事件处理机制以及硬件抽象层的构建往往令人望而却步。幸运的是,开源社区的力量正在大幅降低这一技术门槛。小智开源代码的出现,为idf编程学习提供很好的参考,因为小智源码收录了来自厂商和创客大佬的板卡支持代码,是数以万人的知识精华总结,我们可以站在巨人的肩膀上,模仿和参考他们的案例就足以让自己快速地成长起来,现在介入idf编程的绝佳的机会,比以往任何时候都适合,小智运动其实是 idf编程知识的推广运动,让我们尽情享受这场难得盛宴。
如遇到问题可以留言,也可在b站问询。
|