源码地址
项目介绍
使用 ESP32-S2 制作一个本地气象台/温度计,在 oled 屏幕上显示本地的实时时间和天气信息。
设计思路
功能实现
(1)连接 wifi 功能
ESP32-S2 连接 wifi 需要设置成 AP 模式。
注册 wifi 开始连接事件、wifi 断联事件和获取 IP 地址事件,在事件回调函数中对这三种情况分别处理:
static void event_handler(void* arg, esp_event_base_t event_base,
int32_t event_id, void* event_data)
{
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START)
{
esp_wifi_connect();
}
else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED)
{
if (s_retry_num < EXAMPLE_ESP_MAXIMUM_RETRY)
{
esp_wifi_connect();
s_retry_num ++;
ESP_LOGI(TAG, "retry to connect to the AP");
}
else
{
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
}
ESP_LOGI(TAG,"connect to the AP fail");
}
else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP)
{
ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
ESP_LOGI(TAG, "got ip:" IPSTR, IP2STR(&event->ip_info.ip));
s_retry_num = 0;
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
}
}
void wifi_init_sta(void)
{
s_wifi_event_group = xEventGroupCreate();
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_create_default_wifi_sta();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
esp_event_handler_instance_t instance_any_id;
esp_event_handler_instance_t instance_got_ip;
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
ESP_EVENT_ANY_ID,
&event_handler,
NULL,
&instance_any_id));
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
IP_EVENT_STA_GOT_IP,
&event_handler,
NULL,
&instance_got_ip));
wifi_config_t wifi_config = {
.sta = {
.ssid = EXAMPLE_ESP_WIFI_SSID,
.password = EXAMPLE_ESP_WIFI_PASS,
.threshold.authmode = WIFI_AUTH_WPA2_PSK,
.pmf_cfg = {
.capable = true,
.required = false
},
},
};
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
ESP_LOGI(TAG, "wifi_init_sta finished.");
EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
pdFALSE,
pdFALSE,
portMAX_DELAY);
if (bits & WIFI_CONNECTED_BIT)
{
ESP_LOGI(TAG, "connected to ap SSID:%s password:%s",
EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS);
}
else if (bits & WIFI_FAIL_BIT)
{
ESP_LOGI(TAG, "Failed to connect to SSID:%s, password:%s",
EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS);
}
else
{
ESP_LOGE(TAG, "UNEXPECTED EVENT");
}
ESP_ERROR_CHECK(esp_event_handler_instance_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, instance_got_ip));
ESP_ERROR_CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, instance_any_id));
vEventGroupDelete(s_wifi_event_group);
}
(2)通过 http 协议获取天气数据
使用 esp32 的 http client 库发送 http 协议。
使用通过流的方法:
- 通过
esp_http_client_config_t 结构体定义http 的参数 - 通过
esp_http_client_init() 进行初始化 - 通过
esp_http_client_set_method() 设置发送 get 请求 - 通过
esp_http_client_open() 与目标主机建立连接,发送请求 - 通过
esp_http_client_fetch_headers() 获取目标主机的 response 报文的头信息,判断是否成功获取数据 - 通过
esp_http_client_read_response() 获取报文的返回数据内容
获取返回的数据后,使用 cJSON 进行解包。
void display_weather(char output_buffer[])
{
cJSON* root = NULL;
root = cJSON_Parse(output_buffer);
cJSON* cjson_item = cJSON_GetObjectItem(root, "results");
cJSON* cjson_results = cJSON_GetArrayItem(cjson_item, 0);
cJSON* cjson_now = cJSON_GetObjectItem(cjson_results, "now");
cJSON* cjson_temperature = cJSON_GetObjectItem(cjson_now, "temperature");
cJSON* cjson_text = cJSON_GetObjectItem(cjson_now, "text");
cJSON* cjson_time = cJSON_GetObjectItem(cjson_results, "last_update");
printf("weather:%s\n", cjson_text->valuestring);
printf("temperature:%s\n", cjson_temperature->valuestring);
char str[80];
sprintf(str, "temperature:%s", cjson_temperature->valuestring);
oled_string(0, 35, str, 12);
sprintf(str, "weather:%s", cjson_text->valuestring);
oled_string(0, 20, str, 12);
vTaskDelay(1000 / portTICK_RATE_MS);
}
static void http_test_task(void *pvParameters)
{
char output_buffer[MAX_HTTP_OUTPUT_BUFFER] = {0};
int content_length = 0;
esp_http_client_config_t config;
memset(&config, 0, sizeof(config));
static const char *URL = WEATHER_API;
config.url = URL;
esp_http_client_handle_t client = esp_http_client_init(&config);
esp_http_client_set_method(client, HTTP_METHOD_GET);
bool last = false, cur = false;
while(1)
{
esp_err_t err = esp_http_client_open(client, 0);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err));
cur = false;
if (last) oled_clear();
last = false;
}
else
{
cur = true;
if (!last) oled_clear();
last = true;
content_length = esp_http_client_fetch_headers(client);
if (content_length < 0)
{
ESP_LOGE(TAG, "HTTP client fetch headers failed");
}
else
{
int data_read = esp_http_client_read_response(client, output_buffer, MAX_HTTP_OUTPUT_BUFFER);
if (data_read >= 0)
{
ESP_LOGI(TAG, "HTTP GET Status = %d, content_length = %d",
esp_http_client_get_status_code(client),
esp_http_client_get_content_length(client));
printf("data:%s\n", output_buffer);
display_weather(output_buffer);
}
else
{
ESP_LOGE(TAG, "Failed to read response");
}
}
}
esp_http_client_close(client);
vTaskDelay(3000 / portTICK_PERIOD_MS);
}
}
(3)获得实时时间
SNTP 同步。SNTP 协议是用来同步本地的时间到 unix 时间戳. 通常嵌入式设备上电, 连接 AP(access point) , 获取 IP 地址后, 就需要使用 SNTP 协议获取全球时间. 以便于下一步的应用交互和使用。
使用 C 库函数 time() 、localtime_r() 获得时间,strftime() 将时间转换成指定格式。
static void esp_initialize_sntp(void)
{
ESP_LOGI(TAG, "Initializing SNTP");
sntp_setoperatingmode(SNTP_OPMODE_POLL);
sntp_setservername(0, "ntp1.aliyun.com");
sntp_init();
}
void esp_wait_sntp_sync(void)
{
char strftime_buf[64];
esp_initialize_sntp();
time_t now = 0;
struct tm timeinfo = { 0 };
int retry = 0;
while (timeinfo.tm_year < (2019 - 1900)) {
ESP_LOGD(TAG, "Waiting for system time to be set... (%d)", ++retry);
vTaskDelay(100 / portTICK_PERIOD_MS);
time(&now);
localtime_r(&now, &timeinfo);
}
setenv("TZ", "CST-8", 1);
tzset();
strftime(strftime_buf, sizeof(strftime_buf), "%c", &timeinfo);
ESP_LOGI(TAG, "The current date/time in Shanghai is: %s", strftime_buf);
}
void display_time(void)
{
while (1)
{
time_t now;
char strftime_buf[64];
struct tm timeinfo;
time(&now);
setenv("TZ", "CST-8", 1);
tzset();
localtime_r(&now, &timeinfo);
strftime(strftime_buf, sizeof(strftime_buf), "%Y-%m-%d %H:%M:%S", &timeinfo);
ESP_LOGI(TAG, "The current date/time in Shanghai is: %s", strftime_buf);
oled_string(0, 50, strftime_buf, 12);
oled_refresh();
vTaskDelay(300 / portTICK_RATE_MS);
}
}
(4)oled 显示
配置 SPI 驱动 oled 屏幕显示信息,分别设置主设备输出、从设备输出引脚号(35),时钟引脚号(36),D/C引脚号(33)和复位引脚号(34)。使用 12 * 12 的1206 ASCII 字符集点阵。
DRAM_ATTR static uint8_t screen[8][128] = {0};
static spi_device_handle_t spi;
void send_cmd(const uint8_t cmd, spi_device_handle_t spi)
{
esp_err_t ret;
spi_transaction_t t;
memset(&t, 0, sizeof(t));
t.length = 8;
t.tx_buffer = &cmd;
t.user = (void*)0;
ret = spi_device_polling_transmit(spi, &t);
assert(ret == ESP_OK);
}
void send_data(spi_device_handle_t spi, const uint8_t *data, int len)
{
esp_err_t ret;
spi_transaction_t t;
if (len == 0) return;
memset(&t, 0, sizeof(t));
t.length = len * 8;
t.tx_buffer = data;
t.user = (void*)1;
ret = spi_device_polling_transmit(spi, &t);
assert(ret == ESP_OK);
}
void oled_spi_pre_transfer_callback(spi_transaction_t *t)
{
int dc = (int)t->user;
gpio_set_level(PIN_NUM_DC, dc);
}
void oled_refresh()
{
uint8_t m;
for (m = 0; m < 8; m ++)
{
send_cmd(0xb0 + m, spi);
send_cmd(0x00, spi);
send_cmd(0x10, spi);
send_data(spi, screen[m], 128);
}
}
void oled_drawpoint(uint8_t x, uint8_t y)
{
uint8_t i = 0, j = 0;
x = 127 - x;
i = y / 8;
j = y % 8;
j = 1 << j;
screen[i][x] |= j;
}
void oled_clearpoint(uint8_t x, uint8_t y)
{
uint8_t i = 0, j = 0;
x = 127 - x;
i = y / 8;
j = y % 8;
j = 1 << j;
screen[i][x] = ~screen[i][x];
screen[i][x] |= j;
screen[i][x] = ~screen[i][x];
}
void oled_char(uint8_t x, uint8_t y, char chr,uint8_t size1)
{
uint8_t i, m, temp, size2, chr1;
uint8_t y0 = y;
size2 = (size1 / 8 + ((size1 % 8) ? 1 : 0)) * (size1 / 2);
chr1 = chr - ' ';
for (i = 0; i < size2; i ++)
{
if (size1 == 12)
{
temp = asc2_1206[chr1][i];
}
else if (size1 == 16)
{
temp = asc2_1608[chr1][i];
}
else return;
for (m = 0; m < 8; m ++)
{
if (temp & 0x80) oled_drawpoint(x, y);
else oled_clearpoint(x, y);
temp <<= 1;
y --;
if ((y0 - y) == size1)
{
y = y0;
x ++;
break;
}
}
}
}
void oled_string(uint8_t x, uint8_t y, char *chr, uint8_t size1)
{
while ((*chr >= ' ') && (*chr <= '~') && (*chr != '.'))
{
oled_char(x, y, *chr, size1);
x += size1 / 2;
if (x > 128 - size1)
{
x = 0;
y -= size1;
}
chr ++;
}
}
void oled_init()
{
gpio_set_direction(PIN_NUM_DC, GPIO_MODE_OUTPUT);
gpio_set_direction(PIN_NUM_RST, GPIO_MODE_OUTPUT);
gpio_set_level(PIN_NUM_RST, 0);
vTaskDelay(100 / portTICK_RATE_MS);
gpio_set_level(PIN_NUM_RST, 1);
vTaskDelay(100 / portTICK_RATE_MS);
}
void oled_clear()
{
uint8_t i, j;
for (i = 0; i < 8; i ++)
for (j = 0; j < 128; j ++)
screen[i][j] = 0;
oled_refresh();
}
void oled_start()
{
esp_err_t ret;
spi_bus_config_t buscfg = {
.miso_io_num=PIN_NUM_MISO,
.mosi_io_num=PIN_NUM_MOSI,
.sclk_io_num=PIN_NUM_CLK,
.quadwp_io_num=-1,
.quadhd_io_num=-1,
.max_transfer_sz=128*8,
};
spi_device_interface_config_t devcfg = {
.clock_speed_hz=20*1000*1000,
.mode=0,
.spics_io_num=PIN_NUM_CS,
.queue_size=1,
.pre_cb=oled_spi_pre_transfer_callback,
};
ret = spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO);
ESP_ERROR_CHECK(ret);
ret = spi_bus_add_device(SPI2_HOST, &devcfg, &spi);
ESP_ERROR_CHECK(ret);
oled_init();
send_cmd(0x8D, spi);
send_cmd(0x14, spi);
send_cmd(0xAF, spi);
char *str = "Connecting to wifi";
oled_string(10, 30, str, 12);
oled_refresh();
vTaskDelay(1000 / portTICK_RATE_MS);
}
使用方法
下载源码后,根据自身情况修改 wifi 账号、密码和 API 的秘钥(your_key):
#define EXAMPLE_ESP_WIFI_SSID "account"
#define EXAMPLE_ESP_WIFI_PASS "password"
#define WEATHER_API "https://api.seniverse.com/v3/weather/now.json?key=your_key&location=ip&language=en&unit=c"
编译烧录成功之后,即可在 oled 屏幕上显示时间和天气信息。
|