first commit

This commit is contained in:
matt committed 2026-08-03 20:31:28 -07:00
commit 0ef2017d18
28 files changed
+1882

No files matched your search

+34
View File
@@ -0,0 +1,34 @@
idf_component_register(
SRCS
"main.c"
"wifi/wifi_manager.c"
"drivers/i2c_bus.c"
"drivers/backlight/backlight.c"
"drivers/display/display_rgb.c"
"drivers/touch/touch_gt911.c"
"ui/lvgl_port.c"
"ui/ui_manager.c"
"api/ambient_weather.c"
"utils/utils.c"
INCLUDE_DIRS
"."
"wifi"
"drivers"
"drivers/backlight"
"drivers/display"
"drivers/touch"
"ui"
"api"
"utils"
REQUIRES
lvgl
esp_wifi
esp_http_client
esp-tls
json
esp_driver_i2c
esp_driver_gpio
esp_lcd
esp_timer
nvs_flash
)
+189
View File
@@ -0,0 +1,189 @@
/*
* Ambient Weather REST API client.
*
* GET https://rt.ambientweather.net/v1/devices/{macAddress}
* ?apiKey=...&applicationKey=...&limit=1
*
* Returns a JSON array of data records, newest first.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "cJSON.h"
#include "esp_crt_bundle.h"
#include "esp_http_client.h"
#include "esp_log.h"
#include "app_config.h"
#include "ambient_weather.h"
static const char *TAG = "ambient_weather";
#define WEATHER_URL_MAX 320
#define WEATHER_RESP_MAX 4096
/* A first TLS handshake on this hardware can take well over 10s. */
#define WEATHER_TIMEOUT_MS 30000
static char resp_buf[WEATHER_RESP_MAX];
static size_t resp_len = 0;
static esp_err_t http_event_handler(esp_http_client_event_t *evt)
{
switch (evt->event_id) {
case HTTP_EVENT_ON_CONNECTED:
resp_len = 0;
break;
case HTTP_EVENT_ON_DATA:
/*
* Accumulate into a fixed buffer. The response is chunked, so
* Content-Length is often -1 -- sizing an allocation from it (as this
* previously did) yields a zero-length buffer and a heap overflow.
*/
if (resp_len + evt->data_len < sizeof(resp_buf)) {
memcpy(resp_buf + resp_len, evt->data, evt->data_len);
resp_len += evt->data_len;
} else {
ESP_LOGW(TAG, "Response exceeds %d bytes, truncating", WEATHER_RESP_MAX);
}
break;
default:
break;
}
return ESP_OK;
}
esp_err_t weather_init(void)
{
if (AMBIENT_WEATHER_API_KEY[0] == '\0' ||
AMBIENT_WEATHER_APP_KEY[0] == '\0' ||
AMBIENT_WEATHER_DEVICE_ID[0] == '\0') {
ESP_LOGW(TAG, "API credentials not set in main/app_config.h -- "
"weather updates will be skipped");
return ESP_ERR_INVALID_STATE;
}
return ESP_OK;
}
static void parse_record(const cJSON *record, weather_data_t *data)
{
const cJSON *tempf = cJSON_GetObjectItemCaseSensitive(record, "tempf");
if (cJSON_IsNumber(tempf)) {
data->temp_f = (float)tempf->valuedouble;
data->has_temp = true;
}
const cJSON *humidity = cJSON_GetObjectItemCaseSensitive(record, "humidity");
if (cJSON_IsNumber(humidity)) {
data->humidity = (float)humidity->valuedouble;
data->has_humidity = true;
}
const cJSON *wind = cJSON_GetObjectItemCaseSensitive(record, "windspeedmph");
if (cJSON_IsNumber(wind)) {
data->wind_mph = (float)wind->valuedouble;
data->has_wind = true;
}
}
esp_err_t weather_update(weather_data_t *data)
{
if (data == NULL) {
return ESP_ERR_INVALID_ARG;
}
/* Caller may reuse the struct across polls; start from a known state so a
* partial response cannot leave stale values looking fresh. */
memset(data, 0, sizeof(*data));
char url[WEATHER_URL_MAX];
const int written = snprintf(url, sizeof(url),
"https://rt.ambientweather.net/v1/devices/%s"
"?apiKey=%s&applicationKey=%s&limit=1",
AMBIENT_WEATHER_DEVICE_ID, AMBIENT_WEATHER_API_KEY, AMBIENT_WEATHER_APP_KEY);
if (written < 0 || written >= (int)sizeof(url)) {
ESP_LOGE(TAG, "URL truncated -- check credential lengths");
return ESP_ERR_INVALID_SIZE;
}
const esp_http_client_config_t config = {
.url = url,
.method = HTTP_METHOD_GET,
.timeout_ms = WEATHER_TIMEOUT_MS,
.event_handler = http_event_handler,
.crt_bundle_attach = esp_crt_bundle_attach,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
if (client == NULL) {
return ESP_ERR_NO_MEM;
}
esp_err_t err = esp_http_client_perform(client);
if (err != ESP_OK) {
ESP_LOGE(TAG, "HTTP request failed: %s", esp_err_to_name(err));
esp_http_client_cleanup(client);
return err;
}
const int status = esp_http_client_get_status_code(client);
esp_http_client_cleanup(client);
if (status != 200) {
ESP_LOGE(TAG, "HTTP %d from Ambient Weather", status);
return ESP_FAIL;
}
if (resp_len == 0) {
ESP_LOGE(TAG, "Empty response");
return ESP_FAIL;
}
resp_buf[resp_len] = '\0';
cJSON *root = cJSON_Parse(resp_buf);
if (root == NULL) {
ESP_LOGE(TAG, "Failed to parse JSON response");
return ESP_FAIL;
}
err = ESP_FAIL;
if (cJSON_IsArray(root) && cJSON_GetArraySize(root) > 0) {
parse_record(cJSON_GetArrayItem(root, 0), data);
if (data->has_temp) {
data->last_update = time(NULL);
err = ESP_OK;
} else {
ESP_LOGW(TAG, "Response contained no tempf field");
}
} else {
ESP_LOGW(TAG, "Response was not a non-empty array");
}
cJSON_Delete(root);
return err;
}
void weather_get_string(const weather_data_t *data, char *buf, size_t len)
{
if (data == NULL || buf == NULL || len == 0) {
return;
}
int n = snprintf(buf, len, "%.1f°F", data->has_temp ? data->temp_f : 0.0f);
if (data->has_humidity && n > 0 && (size_t)n < len) {
n += snprintf(buf + n, len - n, " · %.0f%% RH", data->humidity);
}
if (data->has_wind && n > 0 && (size_t)n < len) {
snprintf(buf + n, len - n, " · %.1f mph", data->wind_mph);
}
}
+32
View File
@@ -0,0 +1,32 @@
#ifndef AMBIENT_WEATHER_H
#define AMBIENT_WEATHER_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <time.h>
#include "esp_err.h"
/*
* Fields mirror the Ambient Weather device data spec:
* https://github.com/ambient-weather/api-docs/wiki/Device-Data-Specs
*
* Note the API reports no textual "condition" or "icon" -- it is a personal
* weather station feed, not a forecast service.
*/
typedef struct {
float temp_f;
float humidity; /* percent */
float wind_mph;
bool has_temp;
bool has_humidity;
bool has_wind;
time_t last_update;
} weather_data_t;
esp_err_t weather_init(void);
esp_err_t weather_update(weather_data_t *data);
void weather_get_string(const weather_data_t *data, char *buf, size_t len);
#endif
+19
View File
@@ -0,0 +1,19 @@
/*
* Copy this file to app_config.h and fill in your own values.
* app_config.h is gitignored -- it holds credentials.
*/
#ifndef APP_CONFIG_H
#define APP_CONFIG_H
/* From https://ambientweather.net/account
* DEVICE_ID is the station's MAC address, e.g. "AA:BB:CC:DD:EE:FF" */
#define AMBIENT_WEATHER_API_KEY ""
#define AMBIENT_WEATHER_APP_KEY ""
#define AMBIENT_WEATHER_DEVICE_ID ""
#define WIFI_SSID ""
#define WIFI_PASSWORD ""
#define WEATHER_UPDATE_INTERVAL_MS (1 * 60 * 1000)
#endif
+80
View File
@@ -0,0 +1,80 @@
/*
* STC8H1K28 board controller, CrowPanel 7" Advance v1.3.
*
* A small 8051-class MCU sitting on the I2C bus at 0x30. It drives the LCD
* backlight and, critically, the GT911 reset line -- there is no ESP32 GPIO
* for touch reset on this board, so the GT911 stays mute until the sequence
* below has run.
*
* Protocol is single raw bytes, no register addressing:
* 0x19 wake
* 0x10, 0x18 configure / release GT911 from reset
* 0x00 - 0xF5 brightness, INVERTED (0x00 = full, 0xF5 = off)
*
* v1.2 boards use a different, narrower range (0x05 = off .. 0x10 = max).
*/
#include "esp_check.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "backlight.h"
#include "i2c_bus.h"
static const char *TAG = "backlight";
#define STC8_ADDR 0x30
#define STC8_I2C_HZ 100000
#define STC8_TIMEOUT_MS 100
#define STC8_CMD_WAKE 0x19
#define STC8_CMD_CFG1 0x10
#define STC8_CMD_CFG2 0x18
/* Inverted brightness range. */
#define STC8_DUTY_MAX_ON 0x00
#define STC8_DUTY_OFF 0xF5
static i2c_master_dev_handle_t dev = NULL;
static esp_err_t stc8_cmd(uint8_t cmd)
{
return i2c_master_transmit(dev, &cmd, 1, STC8_TIMEOUT_MS);
}
esp_err_t backlight_init(void)
{
const i2c_device_config_t cfg = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
.device_address = STC8_ADDR,
.scl_speed_hz = STC8_I2C_HZ,
};
ESP_RETURN_ON_ERROR(i2c_master_bus_add_device(i2c_bus_get(), &cfg, &dev), TAG,
"could not add STC8H1K28 at 0x%02X", STC8_ADDR);
ESP_RETURN_ON_ERROR(stc8_cmd(STC8_CMD_WAKE), TAG, "wake failed");
vTaskDelay(pdMS_TO_TICKS(10));
/* These two also release the GT911 from reset. */
ESP_RETURN_ON_ERROR(stc8_cmd(STC8_CMD_CFG1), TAG, "config 1 failed");
vTaskDelay(pdMS_TO_TICKS(10));
ESP_RETURN_ON_ERROR(stc8_cmd(STC8_CMD_CFG2), TAG, "config 2 failed");
vTaskDelay(pdMS_TO_TICKS(100));
ESP_LOGI(TAG, "STC8H1K28 initialized (GT911 released from reset)");
return ESP_OK;
}
esp_err_t backlight_set(uint8_t brightness)
{
if (dev == NULL) {
return ESP_ERR_INVALID_STATE;
}
/* Map 0..255 (intuitive) onto 0xF5..0x00 (wire, inverted). */
const uint8_t duty = (uint8_t)(STC8_DUTY_OFF -
((uint32_t)brightness * STC8_DUTY_OFF) / 255);
return stc8_cmd(duty);
}
+22
View File
@@ -0,0 +1,22 @@
#ifndef BACKLIGHT_H
#define BACKLIGHT_H
#include <stdbool.h>
#include <stdint.h>
#include "esp_err.h"
/*
* STC8H1K28 board controller (I2C 0x30) on the CrowPanel 7" Advance.
*
* It owns more than the backlight: its init sequence also drives the GT911
* reset line, so this must run before the touch controller will answer on
* the bus at all. Requires i2c_bus_init() first.
*/
esp_err_t backlight_init(void);
/* 0 = off, 255 = full. The v1.3 wire protocol is inverted; that is handled
* internally so callers can use the intuitive direction. */
esp_err_t backlight_set(uint8_t brightness);
#endif
+229
View File
@@ -0,0 +1,229 @@
/*
* RGB parallel panel driver for the Elecrow CrowPanel 7" ADVANCE (800x480).
*
* NOTE: the Advance and the Basic CrowPanel 7" share almost no pins. Every
* data line, every sync line, the pixel clock, the porches and the sync
* polarities differ, and the Advance drives its backlight over I2C rather
* than from a GPIO. Configuration below is the Advance; do not mix the two.
*
* Reference: Elecrow Advance-series wiki and the LovyanGFX board config in
* the FluidTouch project (elecrow-crowpanel-7-advance-v13).
*/
#include <stdlib.h>
#include <string.h>
#include "driver/gpio.h"
#include "esp_heap_caps.h"
#include "esp_check.h"
#include "esp_lcd_panel_ops.h"
#include "esp_lcd_panel_rgb.h"
#include "esp_log.h"
#include "lvgl.h"
#include "display_rgb.h"
static const char *TAG = "display_rgb";
/* --- Sync / control pins (Advance) --- */
#define RGB_DE_GPIO 42
#define RGB_VSYNC_GPIO 41
#define RGB_HSYNC_GPIO 40
#define RGB_PCLK_GPIO 39
/*
* --- Data pins (Advance), 16-bit RGB565 ---
* data_gpio_nums[0..4] = B0-B4, [5..10] = G0-G5, [11..15] = R0-R4.
*/
#define RGB_B0_GPIO 21
#define RGB_B1_GPIO 47
#define RGB_B2_GPIO 48
#define RGB_B3_GPIO 45
#define RGB_B4_GPIO 38
#define RGB_G0_GPIO 9
#define RGB_G1_GPIO 10
#define RGB_G2_GPIO 11
#define RGB_G3_GPIO 12
#define RGB_G4_GPIO 13
#define RGB_G5_GPIO 14
#define RGB_R0_GPIO 7
#define RGB_R1_GPIO 17
#define RGB_R2_GPIO 18
#define RGB_R3_GPIO 3
#define RGB_R4_GPIO 46
/*
* --- Panel timings (Advance) ---
*
* 18MHz is not tunable in practice. With htotal 820 and vtotal 500 it gives
* ~44 Hz, which this panel locks to; dropping to 14MHz yields ~34 Hz and the
* image rolls vertically because the panel cannot lock that slow. Bandwidth
* is dealt with by the bounce buffer instead.
*/
#define RGB_PCLK_HZ (18 * 1000 * 1000)
#define RGB_HSYNC_PULSE_WIDTH 4
#define RGB_HSYNC_BACK_PORCH 8
#define RGB_HSYNC_FRONT_PORCH 8
#define RGB_VSYNC_PULSE_WIDTH 4
#define RGB_VSYNC_BACK_PORCH 8
#define RGB_VSYNC_FRONT_PORCH 8
/*
* Scanlines staged in internal RAM per bounce-buffer chunk (two are
* allocated, so this costs 2 * 800 * LINES * 2 bytes of internal RAM).
*
* Bigger is better here: each chunk buys the refill ISR that much more time
* to copy the next one out of PSRAM before the LCD needs it. When it runs
* late -- which WiFi interrupt activity is enough to cause -- the panel
* displays whatever was left in the buffer, showing up as short stale lines
* at the left edge where each chunk begins.
*/
/* 16 is the practical ceiling: two buffers at 20 lines need 64KB of internal
* RAM and the allocation fails once WiFi is up. */
#define BOUNCE_BUFFER_LINES 16
static esp_lcd_panel_handle_t panel = NULL;
static lv_display_t *disp = NULL;
/*
* Partial-mode flush: blit the dirty rectangle from a small internal-RAM
* draw buffer into the single PSRAM framebuffer.
*
* Page-flipping two framebuffers instead would be cheaper, but the swap
* lands mid-scanout on this panel and tears visibly. Copying into one
* framebuffer that the LCD scans continuously is what the vendor driver does.
*/
/*
* Double-buffered flush.
*
* LVGL renders into whichever framebuffer the LCD is NOT currently scanning,
* and the final flush of a refresh hands that buffer to the panel, which
* switches to it. Nothing ever writes into the buffer being displayed.
*
* This matters more than it sounds: copying a partial rectangle into the live
* framebuffer races the LCD's own reads, and the corruption scales with how
* much is written -- forcing full-width flushes made it visibly worse. Those
* corrupt pixels then persist indefinitely, because LVGL has no idea they are
* wrong and never reinvalidates them.
*/
static void rgb_flush_cb(lv_display_t *display, const lv_area_t *area, uint8_t *px_map)
{
if (lv_display_flush_is_last(display)) {
esp_lcd_panel_draw_bitmap(panel, 0, 0, DISPLAY_H_RES, DISPLAY_V_RES, px_map);
}
lv_display_flush_ready(display);
}
esp_err_t display_init(void)
{
ESP_LOGI(TAG, "Initializing 800x480 RGB panel (CrowPanel Advance)");
/* lv_display_create() below allocates from LVGL's heap. Without lv_init()
* that heap is a null pool and the call faults with LoadProhibited. */
ESP_RETURN_ON_FALSE(lv_is_initialized(), ESP_ERR_INVALID_STATE, TAG,
"lv_init() must run before display_init()");
const esp_lcd_rgb_panel_config_t panel_config = {
.clk_src = LCD_CLK_SRC_DEFAULT,
.timings = {
.pclk_hz = RGB_PCLK_HZ,
.h_res = DISPLAY_H_RES,
.v_res = DISPLAY_V_RES,
.hsync_pulse_width = RGB_HSYNC_PULSE_WIDTH,
.hsync_back_porch = RGB_HSYNC_BACK_PORCH,
.hsync_front_porch = RGB_HSYNC_FRONT_PORCH,
.vsync_pulse_width = RGB_VSYNC_PULSE_WIDTH,
.vsync_back_porch = RGB_VSYNC_BACK_PORCH,
.vsync_front_porch = RGB_VSYNC_FRONT_PORCH,
.flags = {
/*
* The Advance runs both syncs idle-HIGH (LovyanGFX
* hsync_polarity/vsync_polarity = 1), which is esp_lcd's
* default, so the *_idle_low flags stay clear. Its
* pclk_idle_high = 1 maps onto LCD_CK_OUT_EDGE, which esp_lcd
* exposes as pclk_active_neg.
*/
.pclk_active_neg = true,
},
},
.data_width = 16,
.bits_per_pixel = 16,
/* Two framebuffers so LVGL never draws into the one being scanned. */
.num_fbs = 2,
/*
* Bounce buffer in internal RAM. Fetching the framebuffer straight
* from PSRAM at 18MHz underruns the LCD FIFO and the image walks
* sideways; this stages each chunk in internal RAM first.
*
* Its refill ISR reads PSRAM, and PSRAM shares the cache with flash
* on the S3, so a flash write while the panel is running faults it
* with "Cache disabled but cached memory region accessed". app_main
* therefore finishes all NVS/WiFi flash activity *before* calling
* display_init(), and CONFIG_ESP_WIFI_NVS_ENABLED is off so the WiFi
* stack does not write to flash later.
*/
.bounce_buffer_size_px = DISPLAY_H_RES * BOUNCE_BUFFER_LINES,
.dma_burst_size = 64,
.hsync_gpio_num = RGB_HSYNC_GPIO,
.vsync_gpio_num = RGB_VSYNC_GPIO,
.de_gpio_num = RGB_DE_GPIO,
.pclk_gpio_num = RGB_PCLK_GPIO,
.disp_gpio_num = GPIO_NUM_NC,
.data_gpio_nums = {
RGB_B0_GPIO, RGB_B1_GPIO, RGB_B2_GPIO, RGB_B3_GPIO, RGB_B4_GPIO,
RGB_G0_GPIO, RGB_G1_GPIO, RGB_G2_GPIO, RGB_G3_GPIO, RGB_G4_GPIO, RGB_G5_GPIO,
RGB_R0_GPIO, RGB_R1_GPIO, RGB_R2_GPIO, RGB_R3_GPIO, RGB_R4_GPIO,
},
.flags = {
.fb_in_psram = true,
},
};
ESP_RETURN_ON_ERROR(esp_lcd_new_rgb_panel(&panel_config, &panel), TAG,
"esp_lcd_new_rgb_panel failed");
ESP_RETURN_ON_ERROR(esp_lcd_panel_reset(panel), TAG, "panel reset failed");
ESP_RETURN_ON_ERROR(esp_lcd_panel_init(panel), TAG, "panel init failed");
void *fb0 = NULL;
void *fb1 = NULL;
ESP_RETURN_ON_ERROR(esp_lcd_rgb_panel_get_frame_buffer(panel, 2, &fb0, &fb1), TAG,
"failed to get framebuffers");
const size_t fb_size = DISPLAY_H_RES * DISPLAY_V_RES * sizeof(uint16_t);
memset(fb0, 0, fb_size);
memset(fb1, 0, fb_size);
disp = lv_display_create(DISPLAY_H_RES, DISPLAY_V_RES);
ESP_RETURN_ON_FALSE(disp != NULL, ESP_FAIL, TAG, "lv_display_create failed");
lv_display_set_color_format(disp, LV_COLOR_FORMAT_RGB565);
lv_display_set_flush_cb(disp, rgb_flush_cb);
/*
* DIRECT mode with the two framebuffers as LVGL's own buffers. LVGL draws
* the dirty regions into the back buffer and keeps both in sync itself;
* no separate internal draw buffer is needed, which also hands ~25KB of
* internal RAM back for the bounce buffers.
*/
lv_display_set_buffers(disp, fb0, fb1, fb_size, LV_DISPLAY_RENDER_MODE_DIRECT);
ESP_LOGI(TAG, "Panel up: %dx%d @ %d MHz, double-buffered fb0=%p fb1=%p (%u KB each)",
DISPLAY_H_RES, DISPLAY_V_RES, RGB_PCLK_HZ / 1000000,
fb0, fb1, (unsigned)(fb_size / 1024));
ESP_LOGI(TAG, "Free internal heap: %u KB",
(unsigned)(heap_caps_get_free_size(MALLOC_CAP_INTERNAL) / 1024));
return ESP_OK;
}
lv_display_t *display_get_default(void)
{
return disp;
}
esp_lcd_panel_handle_t display_get_panel(void)
{
return panel;
}
+21
View File
@@ -0,0 +1,21 @@
#ifndef DISPLAY_RGB_H
#define DISPLAY_RGB_H
#include <stdbool.h>
#include "esp_err.h"
#include "esp_lcd_panel_ops.h"
#include "lvgl.h"
#define DISPLAY_H_RES 800
#define DISPLAY_V_RES 480
/* Requires lv_init() to have already run -- call lvgl_port_init() first.
* Backlight is NOT owned here on the Advance; see drivers/backlight. */
esp_err_t display_init(void);
lv_display_t *display_get_default(void);
/* Underlying esp_lcd panel, for drawing that bypasses LVGL. */
esp_lcd_panel_handle_t display_get_panel(void);
#endif
+61
View File
@@ -0,0 +1,61 @@
#include "esp_check.h"
#include "esp_log.h"
#include "i2c_bus.h"
static const char *TAG = "i2c_bus";
/*
* CrowPanel 7" Advance pins. The Basic model uses 19/20 instead -- and on the
* ESP32-S3 those are the native USB D-/D+ pins, which is a separate trap.
*/
#define I2C_BUS_SDA_GPIO 15
#define I2C_BUS_SCL_GPIO 16
#define I2C_BUS_PORT I2C_NUM_0
static i2c_master_bus_handle_t bus = NULL;
esp_err_t i2c_bus_init(void)
{
if (bus != NULL) {
return ESP_OK;
}
const i2c_master_bus_config_t cfg = {
.i2c_port = I2C_BUS_PORT,
.sda_io_num = I2C_BUS_SDA_GPIO,
.scl_io_num = I2C_BUS_SCL_GPIO,
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7,
.flags.enable_internal_pullup = true,
};
ESP_RETURN_ON_ERROR(i2c_new_master_bus(&cfg, &bus), TAG, "i2c bus init failed");
ESP_LOGI(TAG, "I2C bus up on SDA=%d SCL=%d", I2C_BUS_SDA_GPIO, I2C_BUS_SCL_GPIO);
return ESP_OK;
}
i2c_master_bus_handle_t i2c_bus_get(void)
{
return bus;
}
void i2c_bus_scan(const char *tag)
{
if (bus == NULL) {
ESP_LOGW(tag, "I2C bus not initialized");
return;
}
int n = 0;
for (uint8_t addr = 0x03; addr < 0x78; addr++) {
if (i2c_master_probe(bus, addr, 20) == ESP_OK) {
const char *what = "?";
if (addr == 0x30) what = "STC8H1K28 board controller";
else if (addr == 0x5D || addr == 0x14) what = "GT911 touch";
ESP_LOGI(tag, " I2C 0x%02X %s", addr, what);
n++;
}
}
ESP_LOGI(tag, " %d device(s) on the bus", n);
}
+19
View File
@@ -0,0 +1,19 @@
#ifndef I2C_BUS_H
#define I2C_BUS_H
#include "driver/i2c_master.h"
#include "esp_err.h"
/*
* Shared I2C bus. On the CrowPanel Advance both the STC8H1K28 board
* controller (0x30) and the GT911 touch controller (0x5D) live on it, so a
* single owner avoids two drivers trying to install the same port.
*/
esp_err_t i2c_bus_init(void);
i2c_master_bus_handle_t i2c_bus_get(void);
/* Logs every responding address. Diagnostics for bring-up. */
void i2c_bus_scan(const char *tag);
#endif
+172
View File
@@ -0,0 +1,172 @@
/*
* GT911 capacitive touch controller, CrowPanel 7" Advance.
*
* The GT911 uses 16-bit register addressing, so every read writes the two
* address bytes (MSB first) then does a repeated-start read.
*
* On the Advance there is no ESP32 GPIO for touch reset or interrupt -- the
* STC8H1K28 board controller owns both. backlight_init() must therefore run
* first, or the GT911 is still in reset and NACKs every address.
*/
#include <string.h>
#include "esp_check.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "lvgl.h"
#include "display_rgb.h"
#include "i2c_bus.h"
#include "touch_gt911.h"
static const char *TAG = "touch_gt911";
#define TOUCH_I2C_HZ 400000
#define TOUCH_I2C_TIMEOUT_MS 100
#define GT911_ADDR_LOW 0x5D
#define GT911_ADDR_HIGH 0x14
#define GT911_REG_PRODUCT_ID 0x8140
#define GT911_REG_STATUS 0x814E
#define GT911_REG_TRACK_ID 0x814F
/* Coordinate block: x_lo, x_hi, y_lo, y_hi, size_lo, size_hi */
#define GT911_REG_POINT1_XY 0x8150
#define GT911_STATUS_READY 0x80
#define GT911_POINT_COUNT_MASK 0x0F
#define GT911_MAX_POINTS 5
static i2c_master_dev_handle_t dev = NULL;
static lv_indev_t *indev = NULL;
static esp_err_t gt911_read(uint16_t reg, uint8_t *data, size_t len)
{
const uint8_t addr[2] = { (uint8_t)(reg >> 8), (uint8_t)(reg & 0xFF) };
return i2c_master_transmit_receive(dev, addr, sizeof(addr), data, len,
TOUCH_I2C_TIMEOUT_MS);
}
static esp_err_t gt911_write_u8(uint16_t reg, uint8_t value)
{
const uint8_t buf[3] = { (uint8_t)(reg >> 8), (uint8_t)(reg & 0xFF), value };
return i2c_master_transmit(dev, buf, sizeof(buf), TOUCH_I2C_TIMEOUT_MS);
}
/* The status register latches until cleared; leaving it set stops the
* controller reporting new coordinates. */
static void gt911_clear_status(void)
{
const esp_err_t err = gt911_write_u8(GT911_REG_STATUS, 0x00);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Failed to clear status register: %s", esp_err_to_name(err));
}
}
static void touch_read_cb(lv_indev_t *drv, lv_indev_data_t *data)
{
/* LVGL expects the last coordinate to persist through release, otherwise
* the release event lands at (0,0) and cancels the gesture. */
static int32_t last_x = 0;
static int32_t last_y = 0;
static bool was_pressed = false;
data->point.x = last_x;
data->point.y = last_y;
data->state = LV_INDEV_STATE_RELEASED;
uint8_t status = 0;
if (gt911_read(GT911_REG_STATUS, &status, 1) != ESP_OK ||
!(status & GT911_STATUS_READY)) {
was_pressed = false;
return;
}
const uint8_t points = status & GT911_POINT_COUNT_MASK;
if (points == 0 || points > GT911_MAX_POINTS) {
was_pressed = false;
gt911_clear_status();
return;
}
/* Only the first contact is used -- the UI has no multi-touch gestures. */
uint8_t buf[4];
if (gt911_read(GT911_REG_POINT1_XY, buf, sizeof(buf)) == ESP_OK) {
const uint16_t x = (uint16_t)((buf[1] << 8) | buf[0]);
const uint16_t y = (uint16_t)((buf[3] << 8) | buf[2]);
if (x < DISPLAY_H_RES && y < DISPLAY_V_RES) {
/* Log only the initial contact: the panel reports ~30 samples a
* second for as long as a finger stays down. */
if (!was_pressed) {
ESP_LOGD(TAG, "touch at (%u, %u)", x, y);
}
was_pressed = true;
last_x = x;
last_y = y;
data->point.x = x;
data->point.y = y;
data->state = LV_INDEV_STATE_PRESSED;
} else {
ESP_LOGW(TAG, "touch out of range: raw (%u, %u)", x, y);
}
}
gt911_clear_status();
}
static esp_err_t gt911_try_address(uint8_t addr)
{
const i2c_device_config_t dev_cfg = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
.device_address = addr,
.scl_speed_hz = TOUCH_I2C_HZ,
};
ESP_RETURN_ON_ERROR(i2c_master_bus_add_device(i2c_bus_get(), &dev_cfg, &dev), TAG,
"failed to add device 0x%02X", addr);
uint8_t product_id[4] = {0};
const esp_err_t err = gt911_read(GT911_REG_PRODUCT_ID, product_id, sizeof(product_id));
if (err != ESP_OK) {
i2c_master_bus_rm_device(dev);
dev = NULL;
return err;
}
ESP_LOGI(TAG, "GT911 found at 0x%02X (product ID \"%.4s\")", addr, (char *)product_id);
return ESP_OK;
}
esp_err_t touch_init(void)
{
ESP_RETURN_ON_FALSE(i2c_bus_get() != NULL, ESP_ERR_INVALID_STATE, TAG,
"i2c_bus_init() must run first");
if (gt911_try_address(GT911_ADDR_LOW) != ESP_OK &&
gt911_try_address(GT911_ADDR_HIGH) != ESP_OK) {
ESP_LOGE(TAG, "No GT911 at 0x%02X or 0x%02X", GT911_ADDR_LOW, GT911_ADDR_HIGH);
i2c_bus_scan(TAG);
return ESP_ERR_NOT_FOUND;
}
gt911_clear_status();
indev = lv_indev_create();
ESP_RETURN_ON_FALSE(indev != NULL, ESP_FAIL, TAG, "lv_indev_create failed");
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
lv_indev_set_read_cb(indev, touch_read_cb);
lv_indev_set_display(indev, display_get_default());
ESP_LOGI(TAG, "Touch registered as LVGL pointer input");
return ESP_OK;
}
lv_indev_t *touch_get_indev(void)
{
return indev;
}
+17
View File
@@ -0,0 +1,17 @@
#ifndef TOUCH_GT911_H
#define TOUCH_GT911_H
#include "esp_err.h"
#include "lvgl.h"
/*
* Probes the GT911 and registers it as an LVGL pointer input device.
*
* Requires i2c_bus_init(), backlight_init() (the STC8H1K28 holds the GT911
* in reset until it runs), lv_init() and display_init() to have happened.
*/
esp_err_t touch_init(void);
lv_indev_t *touch_get_indev(void);
#endif
+5
View File
@@ -0,0 +1,5 @@
dependencies:
idf:
version: ">=5.3"
lvgl/lvgl:
version: "^9.2.0"
+135
View File
@@ -0,0 +1,135 @@
#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "ambient_weather.h"
#include "app_config.h"
#include "backlight.h"
#include "display_rgb.h"
#include "i2c_bus.h"
#include "lvgl_port.h"
#include "touch_gt911.h"
#include "ui_manager.h"
#include "wifi_manager.h"
static const char *TAG = "main";
#define WEATHER_TASK_STACK 6144
#define WEATHER_TASK_PRIO 3
#define WEATHER_FIRST_DELAY_MS 3000
static void weather_update_task(void *arg)
{
weather_data_t weather;
char line[96];
vTaskDelay(pdMS_TO_TICKS(WEATHER_FIRST_DELAY_MS));
while (1) {
if (!wifi_is_connected()) {
ui_set_status("WiFi: reconnecting...");
vTaskDelay(pdMS_TO_TICKS(WEATHER_UPDATE_INTERVAL_MS));
continue;
}
/*
* Freeze rendering for the duration of the fetch. The TLS session,
* the LVGL redraw and the flush into the PSRAM framebuffer all
* compete with the LCD's own DMA out of that same PSRAM; letting them
* overlap is what produces visible artefacts. The panel keeps
* scanning the last completed frame, so the screen simply holds still.
*/
lvgl_port_pause();
const esp_err_t err = weather_update(&weather);
lvgl_port_resume();
if (err == ESP_OK) {
weather_get_string(&weather, line, sizeof(line));
ESP_LOGI(TAG, "Weather: %s", line);
char detail[64];
snprintf(detail, sizeof(detail), "%.0f%% humidity",
weather.has_humidity ? weather.humidity : 0.0f);
ui_update_weather((int16_t)(weather.temp_f + 0.5f),
weather.has_humidity ? detail : "Outdoor");
wifi_get_status(line, sizeof(line));
ui_set_status(line);
} else {
ESP_LOGW(TAG, "Weather update failed");
ui_set_status("Weather update failed");
}
vTaskDelay(pdMS_TO_TICKS(WEATHER_UPDATE_INTERVAL_MS));
}
}
void app_main(void)
{
ESP_LOGI(TAG, "Thermostat HMI -- CrowPanel 7.0\" ESP32-S3 (800x480)");
/*
* WiFi comes up FIRST, before the RGB panel exists.
*
* The panel's bounce-buffer ISR reads the framebuffer out of PSRAM, and
* PSRAM shares the cache with flash on the ESP32-S3. Bringing up NVS and
* the WiFi PHY writes calibration data to flash, which disables the cache
* and faults that ISR. Doing it here means every flash write is finished
* before the panel starts scanning.
*/
const esp_err_t wifi_err = wifi_init_sta();
if (wifi_err != ESP_OK) {
ESP_LOGE(TAG, "WiFi failed -- continuing, weather will be disabled");
}
/* LVGL core must exist before the display and touch drivers register
* themselves with it. */
ESP_ERROR_CHECK(lvgl_port_init());
/* Shared bus: STC8H1K28 board controller and GT911 both live on it. */
ESP_ERROR_CHECK(i2c_bus_init());
/* Must precede touch_init(): the STC8H1K28 holds the GT911 in reset
* until its init sequence runs. */
if (backlight_init() != ESP_OK) {
ESP_LOGE(TAG, "STC8H1K28 init failed -- backlight and touch will not work");
}
i2c_bus_scan(TAG);
ESP_ERROR_CHECK(display_init());
if (touch_init() != ESP_OK) {
/* A dead touch panel should not stop the display from working. */
ESP_LOGW(TAG, "Touch unavailable -- continuing without input");
}
ui_init();
ESP_ERROR_CHECK(lvgl_port_start());
/* First frame is rendered by now, so it is safe to light the panel. */
vTaskDelay(pdMS_TO_TICKS(100));
ESP_ERROR_CHECK(backlight_set(255));
if (wifi_err != ESP_OK) {
ui_set_status("WiFi: failed");
return;
}
char status[96];
wifi_get_status(status, sizeof(status));
ui_set_status(status);
if (weather_init() != ESP_OK) {
ui_set_status("Weather: no API credentials");
return;
}
xTaskCreate(weather_update_task, "weather", WEATHER_TASK_STACK, NULL,
WEATHER_TASK_PRIO, NULL);
ESP_LOGI(TAG, "System ready");
}
+135
View File
@@ -0,0 +1,135 @@
/*
* Minimal LVGL 9 port layer: tick source, service task, and a recursive lock.
*
* LVGL needs three things the application must supply: a millisecond tick, a
* periodic call to lv_timer_handler(), and mutual exclusion around its API.
*/
#include "esp_check.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
#include "lvgl.h"
#include "lvgl_port.h"
static const char *TAG = "lvgl_port";
#define LVGL_TICK_PERIOD_MS 2
#define LVGL_TASK_STACK_SIZE 8192
#define LVGL_TASK_PRIORITY 4
#define LVGL_TASK_MAX_DELAY_MS 500
#define LVGL_TASK_MIN_DELAY_MS 1
static SemaphoreHandle_t lvgl_mutex = NULL;
static esp_timer_handle_t tick_timer = NULL;
static volatile bool paused = false;
static void lvgl_tick_cb(void *arg)
{
lv_tick_inc(LVGL_TICK_PERIOD_MS);
}
static void lvgl_task(void *arg)
{
ESP_LOGI(TAG, "LVGL task started");
while (1) {
uint32_t delay_ms = LVGL_TASK_MAX_DELAY_MS;
if (paused) {
/* Leave the framebuffer untouched -- the panel keeps scanning it
* out, so the last frame stays on screen. */
vTaskDelay(pdMS_TO_TICKS(20));
continue;
}
if (lvgl_port_lock(-1)) {
delay_ms = lv_timer_handler();
lvgl_port_unlock();
}
if (delay_ms > LVGL_TASK_MAX_DELAY_MS) {
delay_ms = LVGL_TASK_MAX_DELAY_MS;
} else if (delay_ms < LVGL_TASK_MIN_DELAY_MS) {
delay_ms = LVGL_TASK_MIN_DELAY_MS;
}
vTaskDelay(pdMS_TO_TICKS(delay_ms));
}
}
esp_err_t lvgl_port_init(void)
{
/* Recursive: lv_timer_handler() runs callbacks that may re-enter code
* which also takes the lock. */
lvgl_mutex = xSemaphoreCreateRecursiveMutex();
ESP_RETURN_ON_FALSE(lvgl_mutex != NULL, ESP_ERR_NO_MEM, TAG, "mutex alloc failed");
lv_init();
ESP_LOGI(TAG, "LVGL %d.%d.%d initialized",
lv_version_major(), lv_version_minor(), lv_version_patch());
return ESP_OK;
}
esp_err_t lvgl_port_start(void)
{
const esp_timer_create_args_t timer_args = {
.callback = lvgl_tick_cb,
.name = "lvgl_tick",
};
ESP_RETURN_ON_ERROR(esp_timer_create(&timer_args, &tick_timer), TAG,
"tick timer create failed");
ESP_RETURN_ON_ERROR(esp_timer_start_periodic(tick_timer, LVGL_TICK_PERIOD_MS * 1000),
TAG, "tick timer start failed");
const BaseType_t ok = xTaskCreate(lvgl_task, "lvgl", LVGL_TASK_STACK_SIZE,
NULL, LVGL_TASK_PRIORITY, NULL);
ESP_RETURN_ON_FALSE(ok == pdPASS, ESP_ERR_NO_MEM, TAG, "LVGL task create failed");
return ESP_OK;
}
void lvgl_port_pause(void)
{
if (paused) {
return;
}
/*
* Take the lock before setting the flag so we cannot stop the task
* mid-render, then release it -- the flag alone keeps the task idle and
* callers stay free to mutate widgets while paused.
*/
if (lvgl_port_lock(1000)) {
paused = true;
lvgl_port_unlock();
} else {
ESP_LOGW(TAG, "pause: lock timed out, pausing anyway");
paused = true;
}
}
void lvgl_port_resume(void)
{
paused = false;
}
bool lvgl_port_is_paused(void)
{
return paused;
}
bool lvgl_port_lock(int timeout_ms)
{
const TickType_t ticks = (timeout_ms < 0) ? portMAX_DELAY : pdMS_TO_TICKS(timeout_ms);
return xSemaphoreTakeRecursive(lvgl_mutex, ticks) == pdTRUE;
}
void lvgl_port_unlock(void)
{
xSemaphoreGiveRecursive(lvgl_mutex);
}
+36
View File
@@ -0,0 +1,36 @@
#ifndef LVGL_PORT_H
#define LVGL_PORT_H
#include <stdbool.h>
#include "esp_err.h"
/* Calls lv_init() and creates the LVGL mutex. Must run before display_init()
* and touch_init(), which register themselves with LVGL. */
esp_err_t lvgl_port_init(void);
/* Starts the tick source and the task that services lv_timer_handler().
* Call once the display and input devices are registered. */
esp_err_t lvgl_port_start(void);
/*
* LVGL is not thread safe. Any code touching an lv_* object from outside the
* LVGL task must hold this lock. Returns false on timeout.
*/
bool lvgl_port_lock(int timeout_ms);
void lvgl_port_unlock(void);
/*
* Suspend/resume LVGL rendering.
*
* The panel keeps scanning out whatever frame is already in the framebuffer,
* so the image stays on screen -- it simply stops being redrawn. Used to keep
* LVGL off the PSRAM bus while a network fetch is in flight, since the redraw,
* the flush and the TLS session otherwise contend with the LCD's own DMA.
*
* Calls do not nest.
*/
void lvgl_port_pause(void);
void lvgl_port_resume(void);
bool lvgl_port_is_paused(void);
#endif
+162
View File
@@ -0,0 +1,162 @@
#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "lvgl.h"
#include "lvgl_port.h"
#include "ui_manager.h"
static const char *TAG = "ui_manager";
#define UI_LOCK_TIMEOUT_MS 1000
static lv_obj_t *temp_label = NULL;
static lv_obj_t *cond_label = NULL;
static lv_obj_t *status_label = NULL;
static lv_obj_t *touch_label = NULL;
/*
* lv_label_set_text() invalidates the label unconditionally, even when the
* new text is identical -- which would repaint the temperature every poll
* whether or not it moved. Comparing first means an unchanged value costs
* nothing: no invalidation, so no redraw and no flush.
*/
static void label_set_if_changed(lv_obj_t *label, const char *text)
{
if (label == NULL || text == NULL) {
return;
}
const char *current = lv_label_get_text(label);
if (current != NULL && strcmp(current, text) == 0) {
return;
}
lv_label_set_text(label, text);
}
/*
* Live readout of the last contact.
*
* Polled from an LVGL timer rather than driven by a widget event. An event
* handler on the screen object only fires for taps that actually reach the
* screen: LVGL does not bubble events to parents by default, so anything
* landing on the header or the temperature card is consumed by that child and
* never seen here. That made whole regions of the panel look like dead touch
* zones when the hardware was reporting them perfectly well.
*/
static void touch_readout_timer_cb(lv_timer_t *timer)
{
lv_indev_t *indev = lv_indev_get_next(NULL);
if (indev == NULL || touch_label == NULL) {
return;
}
lv_point_t p;
lv_indev_get_point(indev, &p);
char buf[48];
if (lv_indev_get_state(indev) == LV_INDEV_STATE_PRESSED) {
snprintf(buf, sizeof(buf), "touch %d, %d", (int)p.x, (int)p.y);
} else {
snprintf(buf, sizeof(buf), "touch %d, %d (released)", (int)p.x, (int)p.y);
}
label_set_if_changed(touch_label, buf);
}
void ui_update_weather(int16_t temp_f, const char *condition)
{
if (!lvgl_port_lock(UI_LOCK_TIMEOUT_MS)) {
ESP_LOGW(TAG, "Dropped weather update: LVGL lock timed out");
return;
}
char buf[32];
snprintf(buf, sizeof(buf), "%d°F", temp_f);
label_set_if_changed(temp_label, buf);
label_set_if_changed(cond_label, condition);
lvgl_port_unlock();
}
void ui_set_status(const char *status)
{
if (status == NULL) {
return;
}
if (!lvgl_port_lock(UI_LOCK_TIMEOUT_MS)) {
ESP_LOGW(TAG, "Dropped status update: LVGL lock timed out");
return;
}
label_set_if_changed(status_label, status);
lvgl_port_unlock();
}
void ui_init(void)
{
/* ui_init() runs from app_main, not the LVGL task, so it needs the lock
* like any other outside caller. */
if (!lvgl_port_lock(UI_LOCK_TIMEOUT_MS)) {
ESP_LOGE(TAG, "Could not acquire LVGL lock for UI init");
return;
}
lv_obj_t *scr = lv_screen_active();
lv_obj_set_style_bg_color(scr, lv_color_hex(0x1A2332), LV_PART_MAIN);
lv_obj_set_style_pad_all(scr, 0, LV_PART_MAIN);
lv_obj_set_flex_flow(scr, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(scr, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER);
lv_obj_t *header = lv_obj_create(scr);
lv_obj_set_size(header, LV_PCT(100), 64);
lv_obj_set_style_radius(header, 0, LV_PART_MAIN);
lv_obj_set_style_bg_color(header, lv_color_hex(0x0D1620), LV_PART_MAIN);
lv_obj_set_style_border_width(header, 0, LV_PART_MAIN);
lv_obj_clear_flag(header, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t *title = lv_label_create(header);
lv_label_set_text(title, "Thermostat");
lv_obj_set_style_text_color(title, lv_color_white(), LV_PART_MAIN);
lv_obj_set_style_text_font(title, &lv_font_montserrat_28, LV_PART_MAIN);
lv_obj_center(title);
lv_obj_t *card = lv_obj_create(scr);
lv_obj_set_size(card, LV_PCT(80), LV_SIZE_CONTENT);
lv_obj_set_style_bg_color(card, lv_color_hex(0x2C3E50), LV_PART_MAIN);
lv_obj_set_style_border_width(card, 2, LV_PART_MAIN);
lv_obj_set_style_border_color(card, lv_color_hex(0x3498DB), LV_PART_MAIN);
lv_obj_set_style_radius(card, 12, LV_PART_MAIN);
lv_obj_set_style_pad_all(card, 24, LV_PART_MAIN);
lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(card, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER);
lv_obj_clear_flag(card, LV_OBJ_FLAG_SCROLLABLE);
temp_label = lv_label_create(card);
lv_label_set_text(temp_label, "--°F");
lv_obj_set_style_text_color(temp_label, lv_color_white(), LV_PART_MAIN);
lv_obj_set_style_text_font(temp_label, &lv_font_montserrat_48, LV_PART_MAIN);
cond_label = lv_label_create(card);
lv_label_set_text(cond_label, "Waiting for data...");
lv_obj_set_style_text_color(cond_label, lv_color_hex(0xBDC3C7), LV_PART_MAIN);
status_label = lv_label_create(scr);
lv_label_set_text(status_label, "Starting...");
lv_obj_set_style_text_color(status_label, lv_color_hex(0x7F8C8D), LV_PART_MAIN);
touch_label = lv_label_create(scr);
lv_label_set_text(touch_label, "touch --, --");
lv_obj_set_style_text_color(touch_label, lv_color_hex(0x3498DB), LV_PART_MAIN);
lv_timer_create(touch_readout_timer_cb, 100, NULL);
lvgl_port_unlock();
ESP_LOGI(TAG, "UI initialized");
}
+13
View File
@@ -0,0 +1,13 @@
#ifndef UI_MANAGER_H
#define UI_MANAGER_H
#include <stdint.h>
#include "lvgl.h"
void ui_init(void);
/* Safe to call from any task -- these take the LVGL lock internally. */
void ui_update_weather(int16_t temp_f, const char *condition);
void ui_set_status(const char *status);
#endif
+37
View File
@@ -0,0 +1,37 @@
#include <ctype.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "utils.h"
void utils_delay_ms(uint32_t ms)
{
vTaskDelay(pdMS_TO_TICKS(ms));
}
uint32_t utils_get_tick_ms(void)
{
return (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS);
}
void utils_trim(char *str)
{
if (!str) return;
char *start = str;
while (*start && isspace((unsigned char)*start)) start++;
if (*start == '\0') {
*str = '\0';
return;
}
char *end = start + strlen(start) - 1;
while (end > start && isspace((unsigned char)*end)) end--;
size_t len = end - start + 1;
memmove(str, start, len);
str[len] = '\0';
}
+11
View File
@@ -0,0 +1,11 @@
#ifndef UTILS_H
#define UTILS_H
#include <stddef.h>
#include <stdint.h>
void utils_delay_ms(uint32_t ms);
uint32_t utils_get_tick_ms(void);
void utils_trim(char *str);
#endif
+156
View File
@@ -0,0 +1,156 @@
#include <string.h>
#include "esp_event.h"
#include "esp_log.h"
#include "esp_netif.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
#include "nvs_flash.h"
#include "app_config.h"
#include "wifi_manager.h"
static const char *TAG = "wifi_manager";
#define WIFI_CONNECTED_BIT BIT0
#define WIFI_FAIL_BIT BIT1
#define WIFI_MAX_RETRIES 8
#define WIFI_CONNECT_TIMEOUT_MS 30000
static EventGroupHandle_t s_wifi_event_group = NULL;
static int s_retry_count = 0;
static void wifi_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();
return;
}
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
/*
* Only give up after a bounded number of retries. Signalling failure
* on the first disconnect (as this previously did) meant a single
* transient drop was reported as a hard failure.
*/
if (s_retry_count < WIFI_MAX_RETRIES) {
s_retry_count++;
ESP_LOGW(TAG, "Disconnected, retry %d/%d", s_retry_count, WIFI_MAX_RETRIES);
esp_wifi_connect();
} else {
ESP_LOGE(TAG, "Giving up after %d retries", WIFI_MAX_RETRIES);
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
}
return;
}
if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
const ip_event_got_ip_t *event = (const ip_event_got_ip_t *)event_data;
ESP_LOGI(TAG, "Got IP: " IPSTR, IP2STR(&event->ip_info.ip));
s_retry_count = 0;
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
}
}
esp_err_t wifi_init_sta(void)
{
if (WIFI_SSID[0] == '\0') {
ESP_LOGE(TAG, "WIFI_SSID is empty -- set it in main/app_config.h");
return ESP_ERR_INVALID_STATE;
}
s_wifi_event_group = xEventGroupCreate();
if (s_wifi_event_group == NULL) {
return ESP_ERR_NO_MEM;
}
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
/* The TCP/IP stack and the default STA netif were never set up here, so
* IP_EVENT_STA_GOT_IP could never fire and every connect timed out. */
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_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
&wifi_event_handler, NULL, NULL));
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
&wifi_event_handler, NULL, NULL));
/* wifi_config_t.sta.ssid is a uint8_t array; it cannot be initialised
* from a string literal in a designated initialiser. */
wifi_config_t wifi_config = {
.sta = {
.threshold.authmode = WIFI_AUTH_WPA2_PSK,
.sae_pwe_h2e = WPA3_SAE_PWE_BOTH,
},
};
strlcpy((char *)wifi_config.sta.ssid, WIFI_SSID, sizeof(wifi_config.sta.ssid));
strlcpy((char *)wifi_config.sta.password, WIFI_PASSWORD,
sizeof(wifi_config.sta.password));
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());
/* Modem sleep adds hundreds of ms of latency per round trip, which is
* enough to push a TLS handshake past its timeout on this board. */
ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));
const EventBits_t bits = xEventGroupWaitBits(
s_wifi_event_group,
WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
pdFALSE, pdFALSE,
pdMS_TO_TICKS(WIFI_CONNECT_TIMEOUT_MS));
if (bits & WIFI_CONNECTED_BIT) {
ESP_LOGI(TAG, "Connected to SSID: %s", WIFI_SSID);
return ESP_OK;
}
if (bits & WIFI_FAIL_BIT) {
ESP_LOGE(TAG, "Failed to connect to SSID: %s", WIFI_SSID);
return ESP_FAIL;
}
ESP_LOGE(TAG, "Timed out waiting for WiFi connection");
return ESP_ERR_TIMEOUT;
}
bool wifi_is_connected(void)
{
if (s_wifi_event_group == NULL) {
return false;
}
return (xEventGroupGetBits(s_wifi_event_group) & WIFI_CONNECTED_BIT) != 0;
}
void wifi_get_status(char *buf, size_t len)
{
if (buf == NULL || len == 0) {
return;
}
if (!wifi_is_connected()) {
snprintf(buf, len, "WiFi: not connected");
return;
}
wifi_ap_record_t ap;
if (esp_wifi_sta_get_ap_info(&ap) == ESP_OK) {
snprintf(buf, len, "WiFi: %s (%d dBm)", (char *)ap.ssid, ap.rssi);
} else {
snprintf(buf, len, "WiFi: connected");
}
}
+14
View File
@@ -0,0 +1,14 @@
#ifndef WIFI_MANAGER_H
#define WIFI_MANAGER_H
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include "esp_err.h"
esp_err_t wifi_init_sta(void);
bool wifi_is_connected(void);
void wifi_get_status(char *buf, size_t len);
#endif