天气时钟:天气模块开发、主函数编写
- I2C协议和SPI协议
-
-
- [I2C(Inter-Integrated Circuit)](#I2C(Inter-Integrated Circuit))
- [SPI(Serial Peripheral Interface)](#SPI(Serial Peripheral Interface))
-
- 天气模块
- 主函数
上一篇文章,我们已经完成了WiFi模块、OLED模块、NTP模块的编写,本文将先对I2C协议进行相关补充,并且对天气时钟进行收尾。
I2C协议和SPI协议
I2C(Inter-Integrated Circuit)和SPI(Serial Peripheral Interface)是两种常用的串行通信协议,主要用于微控制器与各种外设(如传感器、显示器、存储器等)之间的数据传输。
I2C(Inter-Integrated Circuit)
特点:
- 双线制:I2C使用两根线进行通信:数据线(SDA)和时钟线(SCL)。通过这两根线就可以实现多个设备之间的通信。
- 多主机和多从机:I2C支持多个主设备和多个从设备,主设备可以主动发起通信。
- 地址识别:每个I2C设备都有一个唯一的地址,主设备通过地址来选择要与之通信的从设备。
- 速度:常见的I2C速度为100 kHz(标准模式)和400 kHz(快速模式),某些设备支持更高的速度(如高速模式,最高可达3.4 MHz)。
- 应用场景:适合传感器、EEPROM等低速设备的连接,尤其是在引脚资源有限的情况下使用。
优点:
- 线缆少,连接简单(只需两根线)。
- 支持多个从设备,节省引脚资源。
- 适用于短距离通信。
缺点:
- 速度相对较慢,适合对速度要求不高的应用。
- 由于使用了共享总线,可能会存在总线冲突的问题。
SPI(Serial Peripheral Interface)
特点:
- 多线制:SPI通常使用四根线进行通信:主设备输出(MOSI)、主设备输入(MISO)、时钟线(SCK)和从设备选择(SS)。每个从设备通常需要一个独立的SS线。
- 全双工通信:SPI支持全双工通信,主设备和从设备可以同时发送和接收数据。
- 速度:SPI的速度通常比I2C快,可以达到几MHz到几十MHz,具体取决于设备和连接方式。
- 应用场景:适合需要高速数据传输的设备,如SD卡、显示屏、ADC/DAC等。
优点:
- 速度快,适合对速度要求较高的应用。
- 简单的协议,易于实现。
- 全双工通信,提高了数据传输效率。
缺点:
- 需要更多的引脚,特别是在有多个从设备的情况下。
- 不支持多主机,通常由一个主设备控制多个从设备。
天气模块
心知天气预报使用
使用心知天气预报可以通过自行创建项目的方式,获取私人API和城市等的专属ID,以获取准确的信息,以下是操作流程:
- 点击控制台
- 点击申请添加免费产品
- 产品创建完成后,复制API私钥(私人API)
- 查看产品文档,此处我们功能较简单,使用v3版本即可,找到查看天气预报的网址
可以看到在预报网址上,我们只需要将变量key替换为自己的API_KEY、将变量location修改为对应的城市ID即可;同时我们只查询当天一天的天气,因此变量start应修改为0,变量days应修改为1;由于我们的数据从网站返回,并且返回结果是JSON格式的,因此我们需要对HTTPClient和JSON相关信息进行介绍
HTTPClient类介绍
HTTPClient
是一个用于在嵌入式系统(如Arduino、ESP8266、ESP32等)中进行HTTP通信的库。它提供了简单的接口来发送HTTP请求和接收响应,支持常见的HTTP方法,如GET、POST、PUT、DELETE等。以下是对 HTTPClient
的详细介绍:
主要功能
-
HTTP请求:
- 支持发送GET、POST、PUT、DELETE等HTTP请求。
- 可以与HTTP和HTTPS(SSL/TLS)服务器通信。
-
请求头和参数:
- 可以设置自定义HTTP请求头。
- 支持发送带有参数的请求。
-
响应处理:
- 能够接收和处理HTTP响应,包括响应码和响应体。
- 提供方法来获取响应的内容长度、类型等信息。
-
连接管理:
- 支持持久连接(Keep-Alive)以提高效率。
- 提供方法来管理连接的生命周期。
常用函数
-
begin()
:- 初始化HTTP请求,指定目标URL和WiFiClient。
- 可以重载以支持HTTPS和自定义端口。
-
addHeader()
:- 添加自定义HTTP请求头。
-
GET()
:- 发送HTTP GET请求,返回HTTP响应码。
-
POST()
:- 发送HTTP POST请求,带有可选的请求体。
-
PUT()
:- 发送HTTP PUT请求,带有可选的请求体。
-
DELETE()
:- 发送HTTP DELETE请求。
-
getString()
:- 获取HTTP响应体的内容作为字符串。
-
getStream()
:- 获取HTTP响应体的内容作为流,适合处理大数据。
-
end()
:- 结束HTTP请求,释放资源。
注意事项
- 在使用
HTTPClient
时,确保设备已连接到网络。 - 对于HTTPS请求,可能需要处理SSL证书验证。
HTTPClient
适合在资源受限的嵌入式环境中使用,但在处理大量数据时要注意内存管理。
JSON介绍
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,易于人类阅读和编写,同时也易于机器解析和生成。以下是JSON的主要特点和用途:
-
简单易读:
- JSON使用键值对的形式来表示数据,结构清晰。这使得JSON非常直观,易于理解和使用。
-
轻量级:
- JSON格式简洁,数据量小,适合在网络上传输,尤其是在带宽有限的环境中。
-
广泛支持:
- 几乎所有现代编程语言(如Python、Java、C++、Ruby等)都提供了对JSON的解析和生成支持。
-
数据结构:
- JSON支持多种数据类型,包括字符串、数字、布尔值、数组、对象和null。通过这些基本类型,可以构建复杂的数据结构。
-
互操作性:
- JSON是语言无关的,这意味着不同的系统和应用程序可以使用JSON进行数据交换,而不必担心语言或平台的差异。
-
常见用途:
- JSON常用于Web应用程序中,作为客户端和服务器之间的数据交换格式。它也被广泛用于配置文件、数据存储和API通信中。
以下是一个简单的JSON示例:
json
{
"name": "张三",
"age": 30,
"isStudent": false,
"courses": ["数学", "物理"],
"address": {
"city": "北京",
"postalCode": "100000"
}
}
在这个示例中,JSON对象包含了一个人的基本信息,包括姓名、年龄、是否为学生、所选课程和地址。JSON的结构使得它非常适合表示层次化的数据。
deserializeJson函数介绍
deserializeJson
是 ArduinoJson 库中的一个函数,用于将 JSON 格式的字符串解析为 JSON 文档对象
。该函数是处理 JSON 数据的核心工具,广泛用于嵌入式系统中,以便从网络、文件或其他数据源中读取和解析 JSON 数据。
函数原型
cpp
DeserializationError deserializeJson(JsonDocument& doc, const char* input);
DeserializationError deserializeJson(JsonDocument& doc, const String& input);
DeserializationError deserializeJson(JsonDocument& doc, Stream& input);
DeserializationError deserializeJson(JsonDocument& doc, char* input, size_t inputSize);
参数
-
JsonDocument& doc
: 引用一个JsonDocument
对象,用于存储解析后的 JSON 数据
。JsonDocument
可以是StaticJsonDocument
或DynamicJsonDocument
,根据内存管理需求选择。 -
const char* input
: 指向要解析的 JSON 字符串。 -
const String& input
: ArduinoString
对象,包含要解析的 JSON 数据。 -
Stream& input
: ArduinoStream
对象,支持从流中读取 JSON 数据(如串口、网络流等)。 -
char* input, size_t inputSize
: 指向 JSON 数据的字符数组及其大小。
返回值
DeserializationError
: 返回一个DeserializationError
对象,用于指示解析结果。可以通过error.c_str()
获取错误信息。
常见错误
Ok
: 解析成功。InvalidInput
: 输入数据无效或格式不正确。NoMemory
:JsonDocument
的容量不足以存储解析后的数据。
注意事项
- 确保
JsonDocument
的大小足够大,以容纳解析后的数据。 StaticJsonDocument
使用静态内存分配,适合内存受限的环境;DynamicJsonDocument
使用动态内存分配,适合需要处理大数据的场合。- 在解析复杂或嵌套的 JSON 数据时,合理设置
JsonDocument
的容量非常重要,以避免NoMemory
错误。
头文件
c
#ifndef WEATHER_H
#define WEATHER_H
#include <Arduino.h>
#include <ESP8266HTTPClient.h> // 导入HTTPClient头文件
#include <ArduinoJson.h> // 导入Json相关头文件
#define WEATHER_API_KEY "SmH8mwzAZeMp0KFmr" // 私人API-KEY
#define WEATHER_CITY "WS0E9D8WN298" // 广州的城市ID
// 路径中把API-KEY和城市ID挖空替换为%s
#define WEATHER_URL "http://api.seniverse.com/v3/weather/daily.json?key=%s&location=%s&language=zh-Hans&unit=c&start=0&days=1"
// 创建天气信息结构体,用于存储后续获取到的JSON相关数据
struct WeatherInfo{
String city; // 城市
String weather; // 天气
String temp; // 温度
String humidity; // 湿度
};
void weatherInit(); // 初始化
bool weatherUpdate(); // 更新天气
WeatherInfo weatherGetInfo(); // 获取天气信息
#endif
cpp文件
c
#include "weather.h"
#include "serial.h"
WeatherInfo currentWeather; // 创建天气结构体对象
// 初始化天气信息
void weatherInit() {
serialPrint("初始化天气模块...");
currentWeather.city = "未知";
currentWeather.weather = "未知";
currentWeather.temp = "0";
currentWeather.humidity = "0";
serialPrint("初始化天气模块完成");
}
//
bool weatherUpdate() {
serialPrint("正在更新天气信息...");
// 构建API请求URL
char url[200];
// 拼接路径 要注意理解本函数
sprintf(url,WEATHER_URL, WEATHER_API_KEY, WEATHER_CITY);
serialPrint("请求URL:" + String(url));
// 进行HTTP请求
WiFiClient client; // 初始化HTTPClient的时候需要用到
HTTPClient http;
if(http.begin(client, url)) {
serialPrint("开始HTTP请求...");
int httpCode = http.GET();
serialPrint("HTTP返回码:" + String(httpCode));
// 如果接收成功
if(httpCode == HTTP_CODE_OK) {
// 获取数据包参数并转成字符串
String payload = http.getString();
// 定义参数,用于存储处理成文档对象后的JSON字符串
DynamicJsonDocument doc(1024);
// 解析JSON包
DeserializationError error = deserializeJson(doc, payload);
// 若没有错误,则解析成功
if(!error){
JsonObject results_0 = doc["results"][0]; // 将最外层Json对象拆出,得到包含location和daily的对象
JsonObject location = results_0["location"]; // 找出地址
JsonArray daily = results_0["daily"]; // 找出日期
JsonObject today = daily[0]; // 第一个元素就是今天的所有信息
// 只要获取到的今天不为空
if(!today.isNull()){
// 把非String类型的全部转换成String类型
// 找出城市名,阴晴状况,最高温和湿度
currentWeather.city = location["name"].as<String>();
currentWeather.weather = today["text_day"].as<String>();
currentWeather.temp = today["high"].as<String>();
currentWeather.humidity = today["humidity"].as<String>();
// 在串口监视器输出,方便调试
serialPrint("城市:" + currentWeather.city);
serialPrint("天气:" + currentWeather.weather);
serialPrint("温度:" + currentWeather.temp);
serialPrint("湿度:" + currentWeather.humidity);
// 释放HTTP资源
http.end();
return true;
} else { // 如果今天的数据为空
serialPrint("JSON解析失败,找不到今日天气数据");
}
} else { // 如果JSON包解析错误,在串口监视器打印错误信息
serialPrint("JSON解析错误:" + String(error.c_str()));
}
}
// 释放HTTP资源
http.end();
}
serialPrint("天气更新失败");
return false;
}
// 获取当前天气数据
WeatherInfo weatherGetInfo() {
return currentWeather;
}
处理JSON字符串的时候用到了很多个JsonObject和JsonArray变量,这是由我们获取到的JSON文档对象和我们想要获取到的数据所决定的,为了方便理解,以下展示接收到的数据:
用JSON转换工具看得更加清晰,代码最终获取到了name(城市名),text_day(阴晴状况),high(最高温),humidity(湿度)的数据
测试结果
主函数
所有的功能都已经实现完毕,在主函数中只要调用这些函数即可。对于天气情况的获取,我设置了每10分钟更新一次、对于屏幕显示,设置为每秒更新一次。
c
#include <Arduino.h>
#include <Wire.h>
#include "serial.h"
#include "wifi.h"
#include "oled.h"
#include "ntp.h"
#include "weather.h"
// 显示所有信息(时间日期和天气)
// 在OLED屏幕上显示三行信息:
// 1. 日期和时间
// 2. 温度和湿度
// 3. 天气和城市
void showAllInfo() {
u8g2.clearBuffer();
// 第一行:日期和时间
u8g2.setCursor(0, 16);
u8g2.print(ntpGetDate() + " " + ntpGetTime());
// 第二行:温度和湿度
u8g2.setCursor(0, 35);
u8g2.print("温度 " + weatherGetInfo().temp + "°C");
u8g2.setCursor(70, 35);
u8g2.print("湿度 " + weatherGetInfo().humidity + "%");
// 第三行:天气和城市
u8g2.setCursor(0, 55);
u8g2.print(weatherGetInfo().weather);
u8g2.setCursor(70, 55);
u8g2.print(weatherGetInfo().city);
u8g2.sendBuffer();
}
// 同步网络服务(WiFi、NTP、天气)
// return: 同步是否全部成功
bool syncNetworkServices() {
if (wifiConnect()) {
oledClear();
oledShow(0, 16, "WiFi连接成功");
oledShow(0, 32, "SSID: " + String(WiFi_SSID));
oledShow(0, 48, "IP: " + WiFi.localIP().toString());
delay(1000);
} else {
oledClear();
oledShow(0, 16, "WiFi连接失败");
oledShow(0, 32, "SSID: " + String(WiFi_SSID));
return false;
}
// 初始化并同步NTP时间
oledClear();
oledShow(0, 16, "正在同步网络时间...");
if (!ntpSync()) {
serialPrint("NTP同步失败,请检查网络连接");
}
delay(1000);
// 初始化天气模块
oledClear();
oledShow(0, 16, "正在获取天气信息...");
if (!weatherUpdate()) {
serialPrint("天气信息获取失败");
}
delay(1000);
return true;
}
// Arduino启动初始化
void setup() {
// 初始化各个模块
serialInit(115200);
serialPrint("系统启动");
oledInit();
oledShow(0, 16, "系统启动中...");
oledShow(0, 32, "正在初始化WiFi...");
wifiInit();
ntpInit(); // NTP初始化
weatherInit(); // 天气模块初始化
oledShow(0, 48, "正在连接WiFi...");
oledShow(0, 64, "SSID: " + String(WiFi_SSID));
syncNetworkServices();
}
// Arduino主循环
void loop() {
// 检查WiFi连接状态
if (WiFi.status() != WL_CONNECTED) {
serialPrint("WiFi连接断开,尝试重连");
oledClear();
oledShow(0, 16, "WiFi已断开,重连中...");
oledShow(0, 32, "SSID: " + String(WiFi_SSID));
syncNetworkServices(); // 重新同步网络服务
}
if (WiFi.status() == WL_CONNECTED) {
static uint32_t lastWeatherUpdate = 0; // 最后一次更新天气的时间
static uint32_t lastDisplayUpdate = 0; // 最后一次更新显示的时间
uint32_t currentMillis = millis(); // 获取当前时间戳
// 每10分钟更新一次天气
if (currentMillis - lastWeatherUpdate >= 600000) {
if (weatherUpdate()) {
showAllInfo();
}
lastWeatherUpdate = currentMillis;
}
// 每秒更新一次显示
if (currentMillis - lastDisplayUpdate >= 1000) {
showAllInfo();
lastDisplayUpdate = currentMillis;
}
}
}
运行结果
至此天气时钟的制作已全部完成。回顾整个流程,我们连接了硬件、开发了串口模块、WiFi模块,对于调试和网络部分有了更深入的了解;学习了能够同步时间的NTP库、显示信息的四针OLED屏幕、进行HTTP请求和响应、以及JSON对象的相关处理,通过实践,在这些方面都有了更深刻的认识。