基于ESP8266和心知天气的小电视

上周末无意中在某个短视频中看到一个基于 ESP8266 开发的小电视,萌萌的外观,勾起了我的兴趣。大概半年多以前我也买了一个esp芯片,还有一块 0.96 寸 oled 屏,也有同样的想法要做一个,忘了什么原因一直闲置在一边。想着资源利用,又把这个事情提了上来。坑比想象中要多,从周六早上搞到现在,每天晚上弄到一点钟,终于弄完了,动手能力太差了。把过程记录一下,供以后参考。

VSCODE ESP8266 开发环境搭建

网上有很多资料,给 VSCODE 安装一些插件、添加相关配置即可。

WiFi连接

在代码里写死 SSID 和 密码的话,WiFi 连接比较简单,没几行代码。在 ESP8266自动配网 – WiFiManager库使用说明 中看到可以利用 WiFiManager 来进行配置,但是不知道是不是我安装的库不对,按照教程写完代码编译会报错,没有继续尝试。

#include <ESP8266WiFi.h>

// WiFi设置
const char *ssid = "My-WiFi-Name";
const char *password = "password";

void setup()
{
    Serial.begin(9600);
    connectWifi();
    delay(2000);
}

void loop() {
}

void connectWifi()
{
    // WiFiManager wifiManager;
    // wifiManager.autoConnect("AutoConnectAP", "12345678");
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED)
    {
        delay(500);
        Serial.print(".");
    }
    Serial.print("IP address:    ");
    Serial.println(WiFi.localIP());
}

oled 屏幕连接

这里选择了 SSD1306 这个库,所有的输出要在屏幕初始化完成之后进行。前两天优化代码的时候,把顺序弄反了,结果屏幕就是通电的时候亮一下然后黑屏,还以为屏幕不小心烧坏了。

库的例子程序里 SCREEN_ADDRESS 这个值默认为 0x3D,如果例子程序都跑不起来的,可以改成 0x3C 试一下。还有检测这个地址值的程序,参考 ESP8266驱动I2C-初始化oled显示屏

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64

#define OLED_RESET -1       // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3C ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

void setup()
{
    Serial.begin(9600);

    // 显示器连接
    if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS))
    {
        Serial.println(F("SSD1306 allocation failed"));
        for (;;)
            ;
    }
    delay(2000);
    printMessage("Display initialed!");
    delay(2000);
}

在屏幕上输出

SSD1306 库的使用方法可以参考 ESP8266驱动I2C OLED显示屏 这篇博客的内容,写得非常详细且实用。常用的输出文字、描点、画线以及各种图形的函数都有例子。

一个输出初始化信息的例子:

void printMessage(char *message)
{
    display.clearDisplay();
    display.setTextSize(1);
    display.setTextColor(WHITE);
    display.setCursor(0, 25);
    display.println(message);
    display.display();
    delay(2000);
}

字体及文字大小的控制

用了好久才发现原来 SSD1306 库不支持中文输出,要输出中文必须用 drawBitmap(x, y, bitmap, size, size, color) 这个函数,考虑到取字模的话会非常费时费力,于是又尝试使用 U8g2 库,U8g2 支持常见的一小部分中文,遗憾的是,不支持调整字体大小,没办法最后只能继续使用 SSD1306,然后取字模。

字体颜色必须要设置,精简代码的时候以为默认字体颜色就是白色,于是将设置颜色的代码去掉,结果背景色也是黑色,字体也是黑色,导致屏幕上看不到任何显示。

生成字模有很多软件可以使用,但是都需要下载安装,比较麻烦。花了一些时间找到一个不错的字母网站:单片机-LCD-LED-OLED中文点阵生成软件 ,可以在线生成,复制 C 代码,不需要下载安装,免费使用,使用起来比较方便。

⚠ 如果输出字体比较小,比如 16x16 的话,最好不要选择“黑体”这类线条较粗的字体,不然会糊。

关于天气 API 的选择

天气 API 的选择也是一个大坑。

心知天气

因为在“太极创客”的文章例子看到使用的是心知天气,本着代码复用的原则,也选择了心知天气。调用之后发现返回字段和例子里有差异,免费版只返回了“温度”之类的3个字段,太少不够用。有收费版可以试用15天,对于我的需求,每天调用48次就足够了,不太值得付费太多。

和风天气

和风天气免费版返回的字段和心知天气的收费版一样,调用频率虽然有限制但也是1分钟几十次,足有用了。但是有一个问题,就是在浏览器里 API 可以正常调用,但是在 ESP8266 里一直显示连接失败,在这个问题上卡了好久,最后选择放弃。

API 接口的端口以及 SSL

80 端口和 443 端口

在调用和风天气的接口的时候,发现一只连接失败。卡了好长时间才去对比浏览器请求和代码请求的区别,发现浏览器请求的端口是443,而我一直在 ESP8266 中请求连接 80 端口,所以一直连接失败。用 Telnet 连接 443 端口之后,发现可以正常连接上。

和风天气 API 域名的问题

在浏览器请求中发现两边不仅除了端口不一样,连域名也不一样,和风天气 API 文档中给的域名是 devapi.qweather.com ,而我在浏览器调试窗口中看到的是 devapi.hweather.net ,而且这两个域名在 telnet 中都能连上、在浏览器中都能正常返回,就很谜。。

接口加密问题

改了域名以及端口换成 443 之后,HTTP 还是显示连接失败。ESP8266 调用和风天气相关的资料不太多,在这个问题上卡了好久。

去查了 443 端口相关的资料,意识到 HTTPS SSL请求的问题,最后在和风天气的官网上看到一篇公告:和风天气官方动态-不再支持非SSL接口连接

尝试使用 WiFiClientSecure 去调用,也没成功。

最后放弃使用和风天气。

ArduinoJson的使用

调用了心知天气API后返回的数据和浏览器请求相比少了几个字段,以为是接口返回少了,所以感到很迷惑。将接口返回信息完整的打印出来发现,接口返回的数据没少。

void requestWeather()
{
    WiFiClient	client;
    String		reqRes = "/v3/weather/now.json?key=" + reqUserKey +
                 +"&location=" + reqLocation +
                 "&language=en&unit=" + reqUnit;
    /* 建立http请求信息 */
    String httpRequest = String( "GET " ) + reqRes + " HTTP/1.1\r\n" +
                 "Host: " + host + "\r\n" +
                 "Connection: close\r\n\r\n";
    Serial.println( "" );
    Serial.print( "Connecting to " ); Serial.print( host );

    /* 尝试连接服务器 */
    if ( client.connect( host, 80 ) )
    {
        Serial.println( " Success!" );

        /* 向服务器发送http请求信息 */
        client.print( httpRequest );
        Serial.println( "Sending request: " );
        Serial.println( httpRequest );

        /* 获取并显示服务器响应状态行 */
        String status_response = client.readStringUntil( '\n' );
        Serial.print( "status_response: " );
        Serial.println( status_response );

        /* 打印出完整的接口返回信息 */
        String responsePayload = wifiClient.readString();
        Serial.println( "Server Response Payload: " );
        Serial.println( responsePayload );

        /* 使用find跳过HTTP响应头 */
        if ( client.find( "\r\n\r\n" ) )
        {
            Serial.println( "Found Header End. Start Parsing." );
        }

        /* 利用ArduinoJson库解析心知天气响应信息 */
        parseInfo( client );
    } else {
        Serial.println( " connection failed!" );
    }
    /* 断开客户端与服务器连接工作 */
    client.stop();
}

就是下面这几行:

/* 打印出完整的接口返回信息 */
String responsePayload = wifiClient.readString();
Serial.println( "Server Response Payload: " );
Serial.println( responsePayload );

尝试将 ArduinoJson 的空间调大之后,解析出了全部字段:

const size_t capacity = JSON_ARRAY_SIZE(1) + JSON_OBJECT_SIZE(1) + 2*JSON_OBJECT_SIZE(3) + JSON_OBJECT_SIZE(6) + 230;
/* 将 capacity 改为 1024 */
DynamicJsonDocument doc(capacity);
deserializeJson(doc, client);

ArduinoJson Assistant

今天晚上回来的打算好好了解一下 ArduinoJson,看到 太极创客 有专门提到这个库,Arduino 官网竟然还有一个专门根据 JSON 数据反向生成解析代码的页面:ArduinoJson Assistant,变量自动命名,很好用。

总结

代码地址:weather-clock

花了一星期的时间,完成了本该半年之前就要完成的事情,途中遇到了许多想象不到的问题,每天晚上八九点回来一直弄到一两点,有点睡眠不足,终于明天是周末,可以多睡一会儿。

夜深人静,头脑更加清醒;

指尖跳动,键盘清脆作响;

用代码谱写最华美的乐章!