纯测试AI能力,不要在评论里开喷拉屎
先看效果图:
你需要的材料:
- ESP32 - S3
- 一个TFT屏幕(st7789)
- 一个2.4G的WIFI
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>
#include <esp_freertos_hooks.h>
#include "ChineseFont16.h"
constexpr int LCD_CS = 13;
constexpr int LCD_DC = 12;
constexpr int LCD_RST = 11;
constexpr int LCD_MOSI = 10;
constexpr int LCD_SCK = 9;
SPIClass lcdSpi(FSPI);
Adafruit_ST7789 lcd(&lcdSpi, LCD_CS, LCD_DC, LCD_RST);
// 修改成你的WiFi名称和密码
const char* ssid = "你的WiFi名称";//需要2.4G WiFi,ESP32不支持5G WiFi
const char* password = "你的WiFi密码";
const char* userApiUrl = "https://api.v2.rainyun.com/user/";
const char* rainyunApiKey = "你的雨云API Key";
constexpr unsigned long RESELL_REFRESH_INTERVAL = 30000;
constexpr unsigned long SERIAL_HEARTBEAT_INTERVAL = 1000;
constexpr unsigned long WIFI_CHECK_INTERVAL = 1000;
constexpr unsigned long WIFI_RECONNECT_INTERVAL = 5000;
constexpr unsigned long WIFI_HARD_RECONNECT_AFTER = 20000;
constexpr unsigned long WIFI_CONNECT_TIMEOUT = 20000;
constexpr unsigned long CPU_SAMPLE_INTERVAL = 1000;
constexpr unsigned long SYSTEM_RESTART_INTERVAL = 5UL * 60UL * 1000UL;
constexpr uint8_t CPU_CALIBRATION_SAMPLES = 5;
constexpr uint16_t HTTP_TIMEOUT = 8000;
unsigned long lastResellRequest = 0;
unsigned long resellRequestCount = 0;
unsigned long lastSerialHeartbeat = 0;
unsigned long lastWiFiCheck = 0;
unsigned long lastWiFiReconnectAttempt = 0;
unsigned long wifiDisconnectedSince = 0;
unsigned long lastCpuSample = 0;
volatile uint32_t cpuIdleCounters[portNUM_PROCESSORS] = {};
uint32_t previousCpuIdleCounters[portNUM_PROCESSORS] = {};
uint32_t maximumIdleRates[portNUM_PROCESSORS] = {};
uint8_t cpuUsagePercent = 0;
uint8_t cpuCalibrationSamples = 0;
bool wasWiFiConnected = false;
bool cpuMonitorReady = false;
bool cpuMonitorCalibrated = false;
bool dashboardVisible = false;
struct DashboardData
{
String name;
double stockDaily;
double stockMonthly;
double monthlyIncome;
uint8_t vipLevel;
bool isAgent;
};
void drawChineseText(int16_t x, int16_t y, const char* text, uint16_t color, uint8_t scale = 1);
const uint8_t* getStatusAsciiGlyph(char character)
{
static const uint8_t glyphC[] = {0x00,0x00,0x78,0x44,0x42,0x02,0x02,0x02,0x02,0x44,0x38,0x00,0x00,0x00,0x00,0x00};
static const uint8_t glyphP[] = {0x00,0x00,0x3E,0x44,0x44,0x44,0x3C,0x04,0x04,0x04,0x0E,0x00,0x00,0x00,0x00,0x00};
static const uint8_t glyphU[] = {0x00,0x00,0xEE,0x44,0x44,0x44,0x44,0x44,0x44,0x44,0x38,0x00,0x00,0x00,0x00,0x00};
static const uint8_t glyph0[] = {0x00,0x18,0x24,0x42,0x42,0x42,0x42,0x42,0x42,0x24,0x18,0x00,0x00,0x00,0x00,0x00};
static const uint8_t glyph1[] = {0x00,0x10,0x1C,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x7C,0x00,0x00,0x00,0x00,0x00};
static const uint8_t glyph2[] = {0x00,0x38,0x44,0x44,0x40,0x20,0x20,0x10,0x08,0x44,0x7C,0x00,0x00,0x00,0x00,0x00};
static const uint8_t glyph3[] = {0x00,0x38,0x44,0x44,0x40,0x30,0x40,0x40,0x44,0x44,0x38,0x00,0x00,0x00,0x00,0x00};
static const uint8_t glyph4[] = {0x00,0x20,0x20,0x30,0x28,0x28,0x24,0x7C,0x20,0x20,0x70,0x00,0x00,0x00,0x00,0x00};
static const uint8_t glyph5[] = {0x00,0x7C,0x04,0x04,0x3C,0x44,0x40,0x40,0x44,0x44,0x38,0x00,0x00,0x00,0x00,0x00};
static const uint8_t glyph6[] = {0x00,0x18,0x24,0x02,0x3A,0x46,0x42,0x42,0x42,0x44,0x38,0x00,0x00,0x00,0x00,0x00};
static const uint8_t glyph7[] = {0x00,0x7C,0x44,0x20,0x20,0x20,0x10,0x10,0x10,0x10,0x10,0x00,0x00,0x00,0x00,0x00};
static const uint8_t glyph8[] = {0x00,0x3C,0x42,0x42,0x42,0x3C,0x24,0x42,0x42,0x42,0x3C,0x00,0x00,0x00,0x00,0x00};
static const uint8_t glyph9[] = {0x00,0x1C,0x22,0x42,0x42,0x62,0x5C,0x40,0x40,0x24,0x18,0x00,0x00,0x00,0x00,0x00};
static const uint8_t glyphColon[] = {0x00,0x00,0x00,0x00,0x30,0x30,0x00,0x00,0x00,0x30,0x30,0x00,0x00,0x00,0x00,0x00};
static const uint8_t glyphPercent[] = {0x00,0x24,0x2A,0x1A,0x1A,0x2A,0x54,0x58,0x58,0x54,0x24,0x00,0x00,0x00,0x00,0x00};
static const uint8_t glyphDash[] = {0x00,0x00,0x00,0x00,0x00,0x00,0xFE,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00};
switch (character)
{
case '0': return glyph0;
case '1': return glyph1;
case '2': return glyph2;
case '3': return glyph3;
case '4': return glyph4;
case '5': return glyph5;
case '6': return glyph6;
case '7': return glyph7;
case '8': return glyph8;
case '9': return glyph9;
case 'C': return glyphC;
case 'P': return glyphP;
case 'U': return glyphU;
case ':': return glyphColon;
case '%': return glyphPercent;
case '-': return glyphDash;
default: return nullptr;
}
}
void drawStatusAscii(int16_t x, int16_t y, const char* text, uint16_t color)
{
while (*text != '\0')
{
const uint8_t* glyph = getStatusAsciiGlyph(*text++);
if (glyph != nullptr)
{
for (uint8_t row = 0; row < 16; ++row)
{
for (uint8_t column = 0; column < 8; ++column)
{
if (glyph[row] & (1U << column))
{
lcd.drawPixel(x + column, y + row, color);
}
}
}
}
x += 9;
}
}
bool countCore0Idle()
{
++cpuIdleCounters[0];
return false;
}
#if portNUM_PROCESSORS > 1
bool countCore1Idle()
{
++cpuIdleCounters[1];
return false;
}
#endif
void drawCpuUsage()
{
if (!dashboardVisible)
{
return;
}
lcd.fillRect(16, 188, 90, 22, ST77XX_WHITE);
char cpuText[12];
if (cpuMonitorCalibrated)
{
snprintf(cpuText, sizeof(cpuText), "CPU:%u%%", cpuUsagePercent);
}
else
{
snprintf(cpuText, sizeof(cpuText), "CPU:--");
}
drawStatusAscii(20, 190, cpuText, ST77XX_BLACK);
}
void drawStatusBar()
{
lcd.fillRect(14, 184, 292, 34, ST77XX_WHITE);
lcd.drawFastVLine(111, 188, 22, 0xC618);
lcd.drawFastVLine(209, 188, 22, 0xC618);
drawCpuUsage();
const uint32_t heapSize = ESP.getHeapSize();
const uint8_t memoryUsed = heapSize == 0 ? 0 : 100 - (ESP.getFreeHeap() * 100UL / heapSize);
drawChineseText(119, 190, "内存", ST77XX_BLACK);
char memoryText[8];
snprintf(memoryText, sizeof(memoryText), ":%u%%", memoryUsed);
drawStatusAscii(153, 190, memoryText, ST77XX_BLACK);
drawChineseText(216, 190, "状态", ST77XX_BLACK);
drawChineseText(252, 190, WiFi.status() == WL_CONNECTED ? "正常" : "断开",
WiFi.status() == WL_CONNECTED ? 0x0400 : ST77XX_RED);
}
void initializeCpuMonitor()
{
bool hooksRegistered = esp_register_freertos_idle_hook_for_cpu(countCore0Idle, 0) == ESP_OK;
#if portNUM_PROCESSORS > 1
hooksRegistered = hooksRegistered &&
esp_register_freertos_idle_hook_for_cpu(countCore1Idle, 1) == ESP_OK;
#endif
for (uint8_t core = 0; core < portNUM_PROCESSORS; ++core)
{
previousCpuIdleCounters[core] = cpuIdleCounters[core];
}
lastCpuSample = millis();
cpuMonitorReady = hooksRegistered;
Serial.println(cpuMonitorReady ? "[CPU] 双核占用监测已启动" : "[CPU] 空闲任务回调注册失败");
}
void updateCpuUsage(unsigned long now)
{
if (!cpuMonitorReady || now - lastCpuSample < CPU_SAMPLE_INTERVAL)
{
return;
}
const unsigned long elapsed = now - lastCpuSample;
uint8_t busiestCoreUsage = 0;
for (uint8_t core = 0; core < portNUM_PROCESSORS; ++core)
{
const uint32_t currentCounter = cpuIdleCounters[core];
const uint32_t idleDelta = currentCounter - previousCpuIdleCounters[core];
previousCpuIdleCounters[core] = currentCounter;
const uint32_t idleRate = static_cast<uint32_t>(
static_cast<uint64_t>(idleDelta) * 1000ULL / elapsed);
if (!cpuMonitorCalibrated && idleRate > maximumIdleRates[core])
{
maximumIdleRates[core] = idleRate;
}
if (cpuMonitorCalibrated && maximumIdleRates[core] > 0)
{
const uint32_t cappedIdleRate = min(idleRate, maximumIdleRates[core]);
const uint32_t busyRate = maximumIdleRates[core] - cappedIdleRate;
const uint8_t coreUsage = min<uint32_t>(
100, (static_cast<uint64_t>(busyRate) * 100ULL + maximumIdleRates[core] - 1) /
maximumIdleRates[core]);
busiestCoreUsage = max(busiestCoreUsage, coreUsage);
}
}
if (!cpuMonitorCalibrated)
{
++cpuCalibrationSamples;
if (cpuCalibrationSamples >= CPU_CALIBRATION_SAMPLES)
{
cpuMonitorCalibrated = true;
Serial.println("[CPU] 空闲基准校准完成");
}
}
else
{
cpuUsagePercent = (cpuUsagePercent * 3 + busiestCoreUsage + 2) / 4;
}
lastCpuSample = now;
drawCpuUsage();
}
const ChineseGlyph16* findChineseGlyph(uint16_t codepoint)
{
for (size_t index = 0; index < CHINESE_FONT_16_COUNT; ++index)
{
if (pgm_read_word(&CHINESE_FONT_16[index].codepoint) == codepoint)
{
return &CHINESE_FONT_16[index];
}
}
return nullptr;
}
void drawChineseGlyph(int16_t x, int16_t y, uint16_t codepoint, uint16_t color, uint8_t scale = 1)
{
const ChineseGlyph16* glyph = findChineseGlyph(codepoint);
if (glyph == nullptr)
{
lcd.drawRect(x, y, 16 * scale, 16 * scale, color);
return;
}
for (uint8_t row = 0; row < 16; ++row)
{
const uint16_t rowBits = pgm_read_byte(&glyph->bitmap[row * 2]) |
(pgm_read_byte(&glyph->bitmap[row * 2 + 1]) << 8);
for (uint8_t column = 0; column < 16; ++column)
{
if (rowBits & (1U << column))
{
lcd.fillRect(x + column * scale, y + row * scale, scale, scale, color);
}
}
}
}
uint16_t readUtf8Codepoint(const char*& text)
{
const uint8_t first = static_cast<uint8_t>(*text++);
if (first < 0x80)
{
return first;
}
if ((first & 0xE0) == 0xC0)
{
const uint8_t second = static_cast<uint8_t>(*text++);
return ((first & 0x1F) << 6) | (second & 0x3F);
}
const uint8_t second = static_cast<uint8_t>(*text++);
const uint8_t third = static_cast<uint8_t>(*text++);
return ((first & 0x0F) << 12) | ((second & 0x3F) << 6) | (third & 0x3F);
}
void drawChineseText(int16_t x, int16_t y, const char* text, uint16_t color, uint8_t scale)
{
while (*text != '\0')
{
const uint16_t codepoint = readUtf8Codepoint(text);
if (codepoint < 0x80)
{
lcd.setTextColor(color);
lcd.setTextSize(scale * 2);
lcd.setCursor(x, y);
lcd.write(static_cast<uint8_t>(codepoint));
}
else
{
drawChineseGlyph(x, y, codepoint, color, scale);
}
x += 16 * scale;
}
}
void drawCenteredValue(int16_t centerX, int16_t y, double value)
{
char valueText[18];
snprintf(valueText, sizeof(valueText), "%.2f", value);
const int16_t width = strlen(valueText) * 12;
lcd.setTextColor(ST77XX_BLACK);
lcd.setTextSize(2);
lcd.setCursor(centerX - width / 2, y);
lcd.print(valueText);
}
void initializeLcd()
{
Serial.println("[LCD] 配置控制引脚");
pinMode(LCD_CS, OUTPUT);
pinMode(LCD_DC, OUTPUT);
pinMode(LCD_RST, OUTPUT);
digitalWrite(LCD_CS, HIGH);
digitalWrite(LCD_DC, HIGH);
Serial.println("[LCD] 执行硬件复位");
digitalWrite(LCD_RST, HIGH);
delay(20);
digitalWrite(LCD_RST, LOW);
delay(100);
digitalWrite(LCD_RST, HIGH);
delay(150);
Serial.print("[LCD] 启动 SPI,SCK=");
Serial.print(LCD_SCK);
Serial.print(", MOSI=");
Serial.print(LCD_MOSI);
Serial.print(", CS=");
Serial.print(LCD_CS);
Serial.print(", DC=");
Serial.print(LCD_DC);
Serial.print(", RST=");
Serial.println(LCD_RST);
lcdSpi.begin(LCD_SCK, -1, LCD_MOSI, LCD_CS);
Serial.println("[LCD] 初始化 ST7789 240x320");
lcd.init(240, 320);
lcd.setRotation(1);
lcd.invertDisplay(false);
lcd.fillScreen(ST77XX_BLACK);
Serial.println("[LCD] 初始化完成");
}
void showWiFiInfo()
{
dashboardVisible = false;
lcd.fillScreen(ST77XX_BLACK);
lcd.setCursor(12, 16);
lcd.setTextColor(ST77XX_GREEN);
lcd.setTextSize(2);
lcd.println("WiFi connected");
lcd.setTextColor(ST77XX_WHITE);
lcd.setCursor(12, 56);
lcd.print("IP: ");
lcd.println(WiFi.localIP());
lcd.setCursor(12, 88);
lcd.print("RSSI: ");
lcd.print(WiFi.RSSI());
lcd.println(" dBm");
}
void showDashboard(const DashboardData& data)
{
dashboardVisible = true;
lcd.fillScreen(ST77XX_WHITE);
lcd.setTextWrap(false);
lcd.drawRect(4, 4, 312, 232, ST77XX_BLACK);
lcd.drawRect(5, 5, 310, 230, ST77XX_BLACK);
lcd.fillCircle(48, 50, 34, 0x9CF3);
lcd.setTextColor(ST77XX_BLACK);
lcd.setTextSize(4);
lcd.setCursor(36, 37);
lcd.write(data.name.length() > 0 ? data.name.charAt(0) : '?');
lcd.setTextSize(3);
lcd.setCursor(92, 15);
lcd.print(data.name.substring(0, 11));
if (data.isAgent)
{
lcd.fillTriangle(94, 48, 101, 59, 108, 48, ST77XX_BLUE);
lcd.fillCircle(101, 63, 9, 0xFD20);
lcd.drawCircle(101, 63, 6, 0xFBE0);
drawChineseText(116, 48, "高级代理", ST77XX_RED, 2);
}
else
{
lcd.setTextColor(0xFD20);
lcd.setTextSize(3);
lcd.setCursor(112, 52);
lcd.print(data.vipLevel);
drawChineseText(132, 52, "级会员", 0xFD20, 2);
}
drawChineseText(24, 105, "今日进货", ST77XX_BLACK);
drawChineseText(128, 105, "本月收益", ST77XX_BLACK);
drawChineseText(232, 105, "本月进货", ST77XX_BLACK);
drawCenteredValue(56, 136, data.stockDaily);
drawCenteredValue(160, 136, data.monthlyIncome);
drawCenteredValue(264, 136, data.stockMonthly);
lcd.drawFastHLine(14, 174, 292, 0xC618);
drawStatusBar();
}
bool readDashboardData(DashboardData& dashboard)
{
const unsigned long requestNumber = ++resellRequestCount;
const unsigned long requestStartedAt = millis();
Serial.println();
Serial.println("========== 分销数据请求开始 ==========");
Serial.print("请求编号:");
Serial.println(requestNumber);
Serial.print("请求时间:");
Serial.print(requestStartedAt / 1000);
Serial.println(" 秒");
Serial.print("请求地址:");
Serial.println(userApiUrl);
Serial.print("WiFi 状态:");
Serial.println(WiFi.status() == WL_CONNECTED ? "已连接" : "未连接");
Serial.print("RSSI:");
Serial.print(WiFi.RSSI());
Serial.println(" dBm");
WiFiClientSecure client;
client.setInsecure();
client.setTimeout(HTTP_TIMEOUT);
HTTPClient http;
if (!http.begin(client, userApiUrl))
{
Serial.println("无法创建 HTTPS 请求");
Serial.print("本次请求耗时:");
Serial.print(millis() - requestStartedAt);
Serial.println(" ms");
return false;
}
http.setConnectTimeout(HTTP_TIMEOUT);
http.setTimeout(HTTP_TIMEOUT);
http.setReuse(false);
http.addHeader("x-api-key", rainyunApiKey);
const int httpCode = http.GET();
if (httpCode <= 0)
{
Serial.print("请求失败:");
Serial.println(http.errorToString(httpCode));
Serial.print("本次请求耗时:");
Serial.print(millis() - requestStartedAt);
Serial.println(" ms");
http.end();
return false;
}
const String response = http.getString();
Serial.print("HTTP 状态码:");
Serial.println(httpCode);
Serial.print("响应长度:");
Serial.print(response.length());
Serial.println(" 字节");
http.end();
if (httpCode != HTTP_CODE_OK)
{
Serial.println("HTTP 状态不是 200,跳过 JSON 数据读取");
Serial.println("========== 分销数据请求结束(失败) ==========");
return false;
}
DynamicJsonDocument document(4096);
const DeserializationError error = deserializeJson(document, response);
if (error)
{
Serial.print("JSON 解析失败:");
Serial.println(error.c_str());
Serial.print("本次请求耗时:");
Serial.print(millis() - requestStartedAt);
Serial.println(" ms");
Serial.println("========== 分销数据请求结束(失败) ==========");
return false;
}
JsonObject data = document.as<JsonObject>();
if (document["data"].is<JsonObject>())
{
data = document["data"].as<JsonObject>();
}
if (data["Name"].isNull() ||
data["StockDaily"].isNull() ||
data["StockMonthly"].isNull() ||
data["ResellPointsMonthly"].isNull())
{
Serial.println("响应中缺少仪表盘字段");
Serial.println("需要字段:Name、StockDaily、StockMonthly、ResellPointsMonthly");
Serial.print("本次请求耗时:");
Serial.print(millis() - requestStartedAt);
Serial.println(" ms");
Serial.println("========== 分销数据请求结束(失败) ==========");
return false;
}
dashboard.name = data["Name"].as<String>();
dashboard.stockDaily = data["StockDaily"].as<double>();
dashboard.stockMonthly = data["StockMonthly"].as<double>();
dashboard.monthlyIncome = data["ResellPointsMonthly"].as<double>() / 2000.0;
dashboard.vipLevel = data["VipLevel"] | 1;
dashboard.isAgent = data["IsAgent"] | false;
Serial.println("解析后的仪表盘数据:");
Serial.print(" 名称:");
Serial.println(dashboard.name);
Serial.print(" 今日进货:");
Serial.println(dashboard.stockDaily, 4);
Serial.print(" 本月收益:");
Serial.println(dashboard.monthlyIncome, 4);
Serial.print(" 本月进货:");
Serial.println(dashboard.stockMonthly, 4);
Serial.print("本次请求耗时:");
Serial.print(millis() - requestStartedAt);
Serial.println(" ms");
Serial.println("========== 分销数据请求结束(成功) ==========");
return true;
}
void updateResellData()
{
if (WiFi.status() != WL_CONNECTED)
{
Serial.println("[API] WiFi 未连接,跳过本次数据请求");
return;
}
Serial.println("开始更新 LCD 分销数据...");
DashboardData dashboard;
if (readDashboardData(dashboard))
{
showDashboard(dashboard);
Serial.println("LCD 仪表盘更新完成");
}
else
{
Serial.println("[API] 本次更新失败,保留 LCD 上一次成功数据");
}
}
void maintainWiFi(unsigned long now)
{
if (now - lastWiFiCheck < WIFI_CHECK_INTERVAL)
{
return;
}
lastWiFiCheck = now;
const bool isConnected = WiFi.status() == WL_CONNECTED;
if (isConnected)
{
wifiDisconnectedSince = 0;
if (!wasWiFiConnected)
{
Serial.println("[WiFi] 已恢复连接");
Serial.print("[WiFi] IP 地址:");
Serial.println(WiFi.localIP());
Serial.print("[WiFi] RSSI:");
Serial.print(WiFi.RSSI());
Serial.println(" dBm");
lastResellRequest = now - RESELL_REFRESH_INTERVAL;
drawStatusBar();
}
wasWiFiConnected = true;
return;
}
if (wasWiFiConnected)
{
Serial.println("[WiFi] 检测到连接已断开");
drawStatusBar();
}
wasWiFiConnected = false;
if (wifiDisconnectedSince == 0)
{
wifiDisconnectedSince = now;
}
if (now - lastWiFiReconnectAttempt >= WIFI_RECONNECT_INTERVAL)
{
lastWiFiReconnectAttempt = now;
if (now - wifiDisconnectedSince < WIFI_HARD_RECONNECT_AFTER)
{
Serial.println("[WiFi] 正在尝试软重连...");
WiFi.reconnect();
}
else
{
Serial.println("[WiFi] 持续断线,重新启动 STA 连接...");
WiFi.disconnect(false, false);
delay(50);
WiFi.begin(ssid, password);
wifiDisconnectedSince = now;
}
}
}
void setup()
{
Serial.begin(115200);
const unsigned long serialWaitStartedAt = millis();
while (!Serial && millis() - serialWaitStartedAt < 3000)
{
delay(10);
}
delay(200);
Serial.println();
Serial.println("[BOOT] Serial monitor started at 115200 baud");
Serial.flush();
initializeLcd();
lcd.fillScreen(ST77XX_BLACK);
lcd.setCursor(12, 16);
lcd.setTextColor(ST77XX_YELLOW);
lcd.setTextSize(2);
lcd.println("Connecting WiFi...");
Serial.println();
Serial.println("========== ESP32 启动 ==========");
Serial.println("ESP32开始连接WiFi...");
WiFi.mode(WIFI_STA);
WiFi.setSleep(false);
WiFi.setAutoReconnect(true);
WiFi.persistent(false);
Serial.println("[WiFi] 省电模式已关闭,自动重连已启用,凭据不重复写入 Flash");
// No WiFi.config() call: obtain IP address from the router via DHCP.
WiFi.begin(ssid, password);
const unsigned long connectStartedAt = millis();
while (WiFi.status() != WL_CONNECTED && millis() - connectStartedAt < WIFI_CONNECT_TIMEOUT)
{
delay(500);
Serial.print(".");
}
Serial.println();
Serial.print("分销数据刷新间隔:");
Serial.print(RESELL_REFRESH_INTERVAL / 1000);
Serial.println(" 秒");
wasWiFiConnected = WiFi.status() == WL_CONNECTED;
if (wasWiFiConnected)
{
Serial.println("连接成功!");
Serial.print("IP地址:");
Serial.println(WiFi.localIP());
Serial.print("信号强度RSSI:");
Serial.print(WiFi.RSSI());
Serial.println(" dBm");
showWiFiInfo();
updateResellData();
}
else
{
Serial.println("[WiFi] 首次连接超时,将在后台继续重连");
}
lastResellRequest = millis();
lastWiFiReconnectAttempt = millis();
initializeCpuMonitor();
}
void loop()
{
const unsigned long now = millis();
if (now >= SYSTEM_RESTART_INTERVAL)
{
Serial.println("[SYSTEM] 已运行 5 分钟,正在执行软件复位...");
Serial.flush();
delay(50);
ESP.restart();
}
maintainWiFi(now);
updateCpuUsage(now);
if (now - lastSerialHeartbeat >= SERIAL_HEARTBEAT_INTERVAL)
{
lastSerialHeartbeat = now;
Serial.print("[HEARTBEAT] uptime=");
Serial.print(now / 1000);
Serial.print("s, WiFi=");
Serial.print(WiFi.status() == WL_CONNECTED ? "connected" : "disconnected");
Serial.print(", CPU=");
if (cpuMonitorCalibrated)
{
Serial.print(cpuUsagePercent);
Serial.println('%');
}
else
{
Serial.println("calibrating");
}
}
if (WiFi.status() == WL_CONNECTED && now - lastResellRequest >= RESELL_REFRESH_INTERVAL)
{
Serial.println();
Serial.println("达到刷新间隔,准备请求最新数据");
updateResellData();
lastResellRequest = millis();
Serial.print("下一次刷新倒计时:");
Serial.print(RESELL_REFRESH_INTERVAL / 1000);
Serial.println(" 秒");
}
}
//命名为ChineseFont16.h,这个是字模
#pragma once
#include <Arduino.h>
struct ChineseGlyph16
{
uint16_t codepoint;
uint8_t bitmap[32];
};
const ChineseGlyph16 CHINESE_FONT_16[] PROGMEM = {
{0x4ECA, {0x80,0x00,0x80,0x00,0x40,0x01,0x20,0x02,0x10,0x04,0x48,0x08,0x84,0x10,0x83,0x60,0x00,0x00,0xf8,0x0f,0x00,0x08,0x00,0x04,0x00,0x04,0x00,0x02,0x00,0x01,0x80,0x00}},
{0x65E5, {0x00,0x00,0xf8,0x0f,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0xf8,0x0f,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0xf8,0x0f,0x08,0x08}},
{0x8FDB, {0x00,0x09,0x04,0x09,0x08,0x09,0xc8,0x3f,0x00,0x09,0x00,0x09,0x0f,0x09,0xe8,0x7f,0x08,0x09,0x08,0x09,0x88,0x08,0x88,0x08,0x48,0x08,0x14,0x00,0xe2,0x7f,0x00,0x00}},
{0x8D27, {0x10,0x01,0x10,0x01,0x08,0x19,0x0c,0x07,0xea,0x21,0x09,0x21,0x08,0x3e,0x00,0x00,0xf8,0x0f,0x08,0x08,0x88,0x08,0x88,0x08,0x88,0x08,0x40,0x06,0x30,0x18,0x0e,0x20}},
{0x672C, {0x80,0x00,0x80,0x00,0x80,0x00,0x80,0x00,0xfe,0x3f,0xc0,0x01,0xa0,0x02,0xa0,0x02,0x90,0x04,0x88,0x08,0x84,0x10,0xf2,0x27,0x81,0x40,0x80,0x00,0x80,0x00,0x80,0x00}},
{0x6708, {0xe0,0x3f,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0xe0,0x3f,0x20,0x20,0x20,0x20,0x20,0x20,0xe0,0x3f,0x20,0x20,0x20,0x20,0x10,0x20,0x10,0x20,0x08,0x28,0x04,0x10}},
{0x6536, {0x10,0x02,0x10,0x02,0x12,0x02,0x12,0x01,0x12,0x7f,0x92,0x10,0x52,0x11,0x12,0x11,0x12,0x11,0x1a,0x0a,0x16,0x0a,0x12,0x04,0x10,0x0a,0x10,0x11,0x90,0x20,0x50,0x40}},
{0x76CA, {0x08,0x08,0x10,0x04,0x00,0x00,0xff,0x7f,0x00,0x00,0x10,0x04,0x08,0x08,0x04,0x10,0x02,0x20,0xfc,0x1f,0x24,0x12,0x24,0x12,0x24,0x12,0x24,0x12,0xff,0x7f,0x00,0x00}},
{0x5185, {0x80,0x00,0x80,0x00,0x80,0x00,0xfe,0x3f,0x82,0x20,0x82,0x20,0x82,0x20,0x42,0x21,0x42,0x22,0x22,0x24,0x12,0x28,0x0a,0x28,0x02,0x20,0x02,0x20,0x02,0x28,0x02,0x10}},
{0x5B58, {0x20,0x00,0x20,0x00,0xff,0x7f,0x10,0x00,0x10,0x00,0xc8,0x1f,0x08,0x08,0x0c,0x04,0x0a,0x02,0xe9,0x7f,0x08,0x02,0x08,0x02,0x08,0x02,0x08,0x02,0x88,0x02,0x08,0x01}},
{0x72B6, {0x10,0x02,0x10,0x12,0x10,0x22,0x12,0x22,0x14,0x02,0xf4,0x7f,0x10,0x02,0x10,0x02,0x18,0x02,0x14,0x05,0x13,0x05,0x10,0x09,0x90,0x08,0x90,0x10,0x50,0x20,0x30,0x40}},
{0x6001, {0x80,0x00,0x80,0x00,0xfe,0x3f,0x80,0x00,0x40,0x01,0x20,0x02,0x50,0x04,0x8c,0x18,0x03,0x60,0x80,0x00,0x10,0x11,0x12,0x21,0x12,0x48,0x12,0x48,0xe1,0x0f,0x00,0x00}},
{0x6B63, {0x00,0x00,0xfe,0x3f,0x80,0x00,0x80,0x00,0x80,0x00,0x80,0x00,0x88,0x00,0x88,0x1f,0x88,0x00,0x88,0x00,0x88,0x00,0x88,0x00,0x88,0x00,0x88,0x00,0xff,0x7f,0x00,0x00}},
{0x5E38, {0x80,0x00,0x88,0x08,0x90,0x04,0xfe,0x7f,0x02,0x40,0xf9,0x2f,0x08,0x08,0xf8,0x0f,0x80,0x00,0xfc,0x1f,0x84,0x10,0x84,0x10,0x84,0x14,0x84,0x08,0x80,0x00,0x80,0x00}},
{0x65AD, {0x20,0x00,0x20,0x20,0xaa,0x1e,0x72,0x02,0x22,0x02,0xfe,0x02,0x22,0x7e,0x72,0x12,0xaa,0x12,0xa6,0x12,0x22,0x12,0x22,0x12,0x02,0x12,0xfe,0x11,0x00,0x11,0x80,0x10}},
{0x5F00, {0x00,0x00,0xfe,0x3f,0x10,0x04,0x10,0x04,0x10,0x04,0x10,0x04,0x10,0x04,0xff,0x7f,0x10,0x04,0x10,0x04,0x10,0x04,0x10,0x04,0x08,0x04,0x08,0x04,0x04,0x04,0x02,0x04}},
{0x9AD8, {0x40,0x00,0x80,0x00,0xff,0x7f,0x00,0x00,0xf0,0x07,0x10,0x04,0x10,0x04,0xf0,0x07,0x00,0x00,0xfe,0x3f,0x02,0x20,0xf2,0x27,0x12,0x24,0x12,0x24,0xf2,0x27,0x02,0x30}},
{0x7EA7, {0x08,0x00,0xc8,0x3f,0x04,0x21,0x04,0x11,0x12,0x11,0x1f,0x09,0x08,0x39,0x04,0x21,0x82,0x22,0x9f,0x22,0x82,0x14,0x80,0x14,0x58,0x08,0x47,0x14,0x22,0x22,0x80,0x41}},
{0x4EE3, {0x10,0x09,0x10,0x11,0x10,0x11,0x08,0x01,0x08,0x7d,0xec,0x03,0x0c,0x01,0x0a,0x02,0x09,0x02,0x08,0x02,0x08,0x04,0x08,0x44,0x08,0x48,0x08,0x50,0x08,0x60,0x08,0x40}},
{0x7406, {0x00,0x00,0x80,0x3f,0xbf,0x24,0x88,0x24,0x88,0x3f,0x88,0x24,0x88,0x24,0xbe,0x3f,0x08,0x04,0x08,0x04,0x88,0x3f,0x08,0x04,0x38,0x04,0x07,0x04,0xc2,0x7f,0x00,0x00}},
{0x4F1A, {0x80,0x00,0x80,0x00,0x40,0x01,0x20,0x02,0x10,0x04,0x0c,0x18,0xf3,0x67,0x00,0x00,0x00,0x00,0xfe,0x3f,0x40,0x00,0x20,0x00,0x10,0x04,0x08,0x08,0xfc,0x1f,0x08,0x10}},
{0x5458, {0x00,0x00,0xf8,0x0f,0x08,0x08,0x08,0x08,0xf8,0x0f,0x00,0x00,0xfc,0x1f,0x04,0x10,0x84,0x10,0x84,0x10,0x84,0x10,0x84,0x10,0x44,0x13,0x20,0x0c,0x18,0x10,0x06,0x20}}
};
constexpr size_t CHINESE_FONT_16_COUNT = sizeof(CHINESE_FONT_16) / sizeof(CHINESE_FONT_16[0]);
主要通过雨云API接口实现,有能力可自行二改开板
