这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » DIY与开源设计 » 电子DIY » 用SeeeduinoWioTerminal做一只独立联网天气屏

共1条 1/1 1 跳转至页

用SeeeduinoWioTerminal做一只独立联网天气屏

高工
2026-10-11 19:03:00     打赏
简介

之前一直想做一块放在桌上的小屏,不用电脑、不依赖任何客户端,就能看到当前位置的天气和空气质量。Wio Terminal 自带屏幕、自带 RTL8720 Wi-Fi、Arduino IDE 直接烧,正好适合这个需求。

最终的样子是:通电就联网,大约每 10 分钟自己跑 HTTPS 去 Open-Meteo 拉一次天气和空气质量,再画到自带的 320×240 TFT 上。A 键手动刷新(最短 60 秒一次,防止被 API 限流),B 键或者摇杆按下切换总览 / 详情。整个过程没有任何 PC 端程序,USB 只用来供电和调试。

整体架构

代码全部放在 weather_wifi.ino 这一个工程里,分成四块:

文件职责
weather_wifi.ino入口、setup()/loop()、Wi-Fi 与 HTTP 流程、按键扫描
weather_data.hWeatherData / AirData 结构、JSON 解析、HTTP Date 头转时间戳
weather_ui.h颜色常量、所有绘图函数、drawUI()
secrets.hWi-Fi SSID/密码、ALLOW_PUBLIC_HTTP_FALLBACK 开关、城市坐标
root_ca.hISRG Root X1 根证书字符串

主循环 loop() 同时干三件事:按键扫描 + 联网状态判断 + 决定要不要再拉一次数据。屏幕只在 dirty 标记置位或超过 1 秒没刷时才重绘,避免每帧都画。

数据流:

Wi-Fi 连上 → fetchStage=1
   → fetchData(weather) → fetchStage=2
   → fetchData(air)     → fetchStage=0
之后每 10 分钟循环:失败时改为 60 秒重试

UI 只有两个页面,details 标志切换;不管哪一页都共用同一个 320×240 的 TFT_eSprite 离屏缓冲,最后 pushSprite(0,0) 一次性送出。背景用 4×4 Bayer 抖动画一个深蓝渐变,RGB332 也能看出层次。

硬件与依赖

  • 板子:Seeeduino Wio Terminal(ATSAMD51 + RTL8720)

  • Arduino IDE + Seeed SAMD Boards 1.8.6

  • 库:TFT_eSPI、rpcWiFi 1.1.0 + rpcUnified、ArduinoJson 6.21.5

  • RTL8720 固件要支持 rpcWiFi,Seeed 官方建议 ≥ 2.0.2

  • 串口调试可选 115200

固件里只用了 Wio Terminal 的三个按键 WIO_KEY_A、WIO_KEY_B、WIO_5S_PRESS,分别对应刷新、切详情、切详情。所有按键都配 INPUT_PULLUP,按下读 LOW。

关键实现

1. HTTPS + 公共 HTTP 回退

Open-Meteo 的接口本身支持 HTTPS,也推荐用 HTTPS。但是 RTL8720 固件跑 TLS 的时候实测会在握手阶段拿到 EOF,所以代码里做了一个软降级:

client.setCACert(ROOT_CA);
http.setConnectTimeout(12000);
http.setTimeout(12000);
http.setReuse(false);
http.useHTTP10(true); // HTTP/1.0,避免 chunked 让 ArduinoJson 解码失败

if (http.begin(client, url)) {
  const char* headers[] = { "Date" };
  http.collectHeaders(headers, 1);
  int response = http.GET();

  if (response < 0 && ALLOW_PUBLIC_HTTP_FALLBACK) {
    http.end();
    client.stop();
    url.replace("https://", "http://");
    http.setConnectTimeout(8000);
    http.setTimeout(8000);
    http.begin(publicClient, url);
    http.collectHeaders(headers, 1);
    response = http.GET();
  }

降级走的是 http://api.open-meteo.com,不带证书校验。secrets.h 里 ALLOW_PUBLIC_HTTP_FALLBACK 可以关掉,关掉之后 TLS 失败就直接报错。需要明白的是:回退路径下没有 HTTPS 身份验证,只发了坐标和字段名,没有发送 Wi-Fi 密码或账号。

2. JSON 过滤解析

直接喂 deserializeJson(doc, *stream) 给 4 KB 的 DynamicJsonDocument 会爆 RAM,所以加了一个 384 字节的 StaticJsonDocument filter 只放白名单字段:

StaticJsonDocument<384> filter;
filter["current"] = true;
if (!airRequest) {
  filter["hourly"]["time"] = true;
  filter["hourly"]["precipitation_probability"] = true;
}
DynamicJsonDocument doc(4096);
WiFiClient* stream = http.getStreamPtr();
stream->Stream::setTimeout(12000);
DeserializationError error = deserializeJson(doc, *stream,
    DeserializationOption::Filter(filter));

http.useHTTP10(true) 是关键一步,HTTP/1.1 默认 chunked,对流式 JSON 解析不友好;切到 HTTP/1.0 之后服务端会按长度一次性返回。

3. 不依赖 RTC 的本地时钟

Wio Terminal 没有电池 RTC,要显示时间就只能从网络拿,又不能真的去查 NTP。代码里用 Open-Meteo 响应里的 Date 头:

const char* headers[] = { "Date" };
http.collectHeaders(headers, 1);
int response = http.GET();
if (response == 200) {
  uint32_t serverTime = parseHttpDate(http.header("Date"));
  if (serverTime) setClock(serverTime);
}

parseHttpDate() 自己写了一个 RFC 1123 解析,把 "Sat, 11 Oct 2025 03:14:12 GMT" 转成 epoch。tickClock() 用 millis() 增量累加,clockRemainder 保留余数,保证长时间运行误差不会累积成几秒。屏幕右下角显示的 timeText() 就是把 UTC 加上 8 小时转成 HH:MM。

吉隆坡是 UTC+8,所以显示的是当地时间,不需要再算时区。

4. 数据新鲜度判断

Open-Meteo 的天气和空气质量更新频率不一样,所以代码里对两者分别设置过期阈值:

字段过期阈值
天气温度/湿度/降水1 小时
US AQI / PM2.53 小时

过期后屏幕上相应数字会用 SOFT 灰色而不是白色,底部状态条会显示 STALE DATA,告诉用户这是缓存值。

数据缺失一律显示 --,不显示 0,避免把"接口没给"误读成"零毫米降水"或"零污染"。

5. 概率降水的有效区间

Open-Meteo 的 precipitation_probability 是 hourly 的,utcClock 没拿到的时候强行匹配会取到很久以前的小时。

if (utcClock && times.size() == probabilities.size()) {
  for (size_t i = 0; i < times.size(); i++) {
    uint32_t end; float probability;
    if (timestampIn(times[i], end) && end >= utcClock
        && end - utcClock <= 3600
        && numberIn(probabilities[i], 0, 100, probability)) {
      next.probability = probability;
      next.probabilityEnd = end;
      break;
    }
  }
}

只匹配 end >= utcClock && end - utcClock <= 3600 的那一小时,匹配上的就是"接下来 60 分钟内会结束的那个时段"。过期后 UI 直接不显示 xx% chance。

6. 自绘天气图标

8 KB 内存下塞不进大位图,所以 weatherIcon() 全部用 fillCircle、drawLine、fillTriangle 现画。weather_code==0 的时候画太阳或月亮,叠加云层;code>=95 加闪电三角形;(code>=71 && code<=77) || code==85 || code==86 画雪花线;code>=51 画雨丝。

0           → Clear sky / Clear night
1~2         → 多云 + 太阳
3           → 阴天
45 / 48     → 阴天 + 三条横线(雾)
51~55       → 多云 + 雨丝(毛毛雨)
56~57, 66~67→ 冻雨
61~65       → 雨
71~77, 85~86→ 雪
80~82       → 阵雨
95+         → 雷暴

7. US AQI 配色条

uint16_t pollutionColor(float aqi) {
  if (aqi < 0)       return SOFT;
  if (aqi <= 50)     return rgb(67, 220, 151);   // Good
  if (aqi <= 100)    return rgb(241, 204, 90);   // Moderate
  if (aqi <= 150)    return rgb(251, 155, 75);   // Sensitive
  if (aqi <= 200)    return rgb(250, 95, 108);   // Unhealthy
  if (aqi <= 300)    return rgb(181, 115, 234);  // Very unhealthy
  return rgb(209, 73, 136);                      // Hazardous
}

底部还有一条 6 段的色带,对应 25 / 75 / 125 / 175 / 250 / 350 六个档位,让用户看一眼就知道当前值落在哪一档。

测试过程与结果

烧录之后,第一次启动会先看到下面这张图(还没有拉到数据时的"等待"状态):

9cf0253714b801c51f64fed6a2eb6561.jpg

城市名右上角的绿点亮代表 Wi-Fi 已连,左下角 Data age W:-- A:-- 因为 utcClock 还没设。等到第一次 fetchData() 拿到 Date 头,时间就开始走了。

串口 115200 打开能拿到这些关键日志:

WIFI_OK IP=192.168.x.x
WEATHER_HTTP 200
WEATHER_OK TEMP=28.4 HUMIDITY=78
AIR_HTTP 200
AIR_OK US_AQI=42 PM25=12.6

如果 TLS 握手失败并触发回退,会先看到一行 PUBLIC_HTTP_FALLBACK,后面再跟 WEATHER_HTTP 200,时间戳和数值一样正常。

屏幕截图脚本

固件里专门留了一个 S 命令做 RGB565 帧导出,方便不带 SD 卡的情况下截屏:

void captureFrame() {
  drawUI();
  Serial.println("FRAME 320 240");
  for (int y = 0; y < 240; y++) {
    for (int x = 0; x < 320; x++) {
      uint16_t p = screen.readPixel(x, y);
      // 写 4 个 hex 字符进 row
    }
    Serial.write((const uint8_t*)row, sizeof(row));
    while (!Serial.available() && millis() - start < 3000) delay(1);
    if (Serial.read() != 'K') return;
  }
}

每发一行要等 PC 回 K 才继续,简单的手抖协议但够用。PC 端 capture-screen.ps1 默认端口 COM11,把 4 字符十六进制还原成 RGB565,再展开成 PNG 写到 screen-preview.png。

param([string]$Port='COM11',[string]$OutputPath=(Join-Path $PSScriptRoot 'screen-preview.png'))
$serial=[IO.Ports.SerialPort]::new($Port,115200)
$serial.ReadBufferSize=262144
# ...
do {$line=$serial.ReadLine().Trim()}while($line -ne 'FRAME 320 240')
for($y=0;$y -lt 240;$y++){
  $rows[$y]=$serial.ReadLine().Trim()
  if($rows[$y].Length -ne 1280){throw "Incomplete row $y"}
  $serial.Write('K')
}

跑 powershell -ExecutionPolicy Bypass -File capture-screen.ps1 -Port COM11 就能拿到当前屏幕的快照。

image.png

踩坑记录

  1. TLS EOF:RTL8720 固件跟 Open-Meteo 的 HTTPS 握手有时会拿到 EOF,没有更多错误细节。开了 ALLOW_PUBLIC_HTTP_FALLBACK 之后体验顺滑很多,但是代价是 HTTP 那条路没有证书校验。生产环境可以关掉回退,等 RTL8720 固件修。

  2. Chunked + ArduinoJson:HTTP/1.1 默认 chunked 传输,ArduinoJson 流式解析会失败。useHTTP10(true) 是必须的。

  3. 读屏的 readPixel 慢:每像素调一次函数会很慢,截图大概要十几秒。一次性截图够用,做视频就别想了。

  4. SPIFFS/SD 都没用:要留调试插桩、要看现场画面,最终选了"导出 RGB565 + PC 还原"。比 SD 卡少一个 BOM,比 SPIFFS 少一次擦写。

  5. Date 头解析时区:必须按 GMT 处理。parseHttpDate() 里专门拦了 strcmp(zone,"GMT"),否则有些 CDN 会返回 +0000 之类的写法直接判失败。

总结

目前这个项目已经基本实现了最初设想的功能:设备可以独立运行,只需通过 USB 或充电器供电,就能自动联网、定时更新天气和 AQI 数据,并通过按键进行操作。对于缺失的数据,屏幕会显示 --;如果天气或 AQI 信息过期,也会通过颜色变化和提示提醒用户。即使没有 RTC 时钟模块,设备也能根据服务器返回的时间戳显示当地时间,同时还支持通过串口导出屏幕截图,方便后续调试和分享。接下来,可以考虑增加多城市切换、过去 24 小时 AQI 折线图,以及进一步优化 TLS 连接的稳定性。如果以后需要接入 WPA2-Enterprise 等更复杂的网络,也需要进一步测试现有 Wi-Fi 方案是否支持。


题外话:要不是印尼整天烧森林,谁闲的没事干去关系AQI指数





关键词: Wio-terminal     联网天气监控     AQI    

共1条 1/1 1 跳转至页

回复

匿名不能发帖!请先 [ 登陆 注册 ]