ESP-IDF 5.5 + LVGL 9 firmware that plays random public cameras from insecam.org on the T-Embed's 320x170 screen, live MJPEG where available, a new camera every 20 seconds. Knob steps through cameras, LED ring shows the countdown. Auto-detects the original vs CC1101 board, reuses Wi-Fi credentials from NVS and falls back to a captive-portal setup hotspot.
246 lines
9.9 KiB
C
246 lines
9.9 KiB
C
#include "net.h"
|
|
#include "sdkconfig.h"
|
|
#include "esp_event.h"
|
|
#include "esp_http_server.h"
|
|
#include "esp_log.h"
|
|
#include "esp_netif.h"
|
|
#include "esp_timer.h"
|
|
#include "esp_wifi.h"
|
|
#include "nvs_flash.h"
|
|
#include "lwip/sockets.h"
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/event_groups.h"
|
|
#include "freertos/task.h"
|
|
#include <ctype.h>
|
|
#include <string.h>
|
|
|
|
static const char *TAG = "net";
|
|
#define BIT_CONNECTED 1
|
|
|
|
static EventGroupHandle_t events;
|
|
static esp_timer_handle_t reconnect_timer;
|
|
static char ip_str[16] = "0.0.0.0";
|
|
static bool portal_started;
|
|
|
|
static void reconnect_cb(void *arg) { esp_wifi_connect(); }
|
|
|
|
static void wifi_event(void *arg, esp_event_base_t base, int32_t id, void *data)
|
|
{
|
|
if (base == WIFI_EVENT && id == WIFI_EVENT_STA_START) {
|
|
esp_wifi_connect();
|
|
} else if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) {
|
|
wifi_event_sta_disconnected_t *d = data;
|
|
xEventGroupClearBits(events, BIT_CONNECTED);
|
|
ESP_LOGW(TAG, "disconnected (reason %d), retrying", d->reason);
|
|
esp_timer_start_once(reconnect_timer, 2000 * 1000);
|
|
} else if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) {
|
|
ip_event_got_ip_t *e = data;
|
|
snprintf(ip_str, sizeof ip_str, IPSTR, IP2STR(&e->ip_info.ip));
|
|
ESP_LOGI(TAG, "got IP %s", ip_str);
|
|
xEventGroupSetBits(events, BIT_CONNECTED);
|
|
}
|
|
}
|
|
|
|
void net_init(void)
|
|
{
|
|
esp_err_t err = nvs_flash_init();
|
|
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
|
ESP_ERROR_CHECK(nvs_flash_erase());
|
|
err = nvs_flash_init();
|
|
}
|
|
ESP_ERROR_CHECK(err);
|
|
|
|
events = xEventGroupCreate();
|
|
ESP_ERROR_CHECK(esp_netif_init());
|
|
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
|
esp_netif_t *sta = esp_netif_create_default_wifi_sta();
|
|
esp_netif_set_hostname(sta, "insecam-widget");
|
|
|
|
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
|
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
|
|
ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_FLASH));
|
|
esp_timer_create_args_t targs = {.callback = reconnect_cb, .name = "wifi_retry"};
|
|
ESP_ERROR_CHECK(esp_timer_create(&targs, &reconnect_timer));
|
|
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_event, NULL));
|
|
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, wifi_event, NULL));
|
|
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
|
|
ESP_ERROR_CHECK(esp_wifi_start());
|
|
ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));
|
|
|
|
wifi_config_t stored = {0};
|
|
if (esp_wifi_get_config(WIFI_IF_STA, &stored) == ESP_OK && stored.sta.ssid[0])
|
|
ESP_LOGI(TAG, "stored credentials for \"%s\"", (char *)stored.sta.ssid);
|
|
else
|
|
ESP_LOGW(TAG, "no stored Wi-Fi credentials");
|
|
}
|
|
|
|
bool net_has_credentials(void)
|
|
{
|
|
wifi_config_t stored = {0};
|
|
return esp_wifi_get_config(WIFI_IF_STA, &stored) == ESP_OK && stored.sta.ssid[0];
|
|
}
|
|
|
|
bool net_wait_connected(int timeout_ms)
|
|
{
|
|
TickType_t ticks = timeout_ms < 0 ? portMAX_DELAY : pdMS_TO_TICKS(timeout_ms);
|
|
return (xEventGroupWaitBits(events, BIT_CONNECTED, pdFALSE, pdFALSE, ticks) & BIT_CONNECTED) != 0;
|
|
}
|
|
|
|
bool net_is_connected(void) { return (xEventGroupGetBits(events) & BIT_CONNECTED) != 0; }
|
|
const char *net_ip_str(void) { return ip_str; }
|
|
|
|
// ---------------------------------------------------------------- captive portal
|
|
|
|
static const char *PAGE =
|
|
"<!doctype html><meta name=viewport content='width=device-width'><title>insecam widget setup</title>"
|
|
"<style>body{font:17px system-ui,sans-serif;background:#0e1116;color:#e6edf3;max-width:420px;margin:40px auto;padding:20px}"
|
|
"input,button{box-sizing:border-box;width:100%;padding:12px;margin:8px 0;font-size:17px;border-radius:6px;border:1px solid #444}"
|
|
"button{background:#2f81f7;color:#fff;border:0}label{display:block;margin-top:12px}</style>"
|
|
"<h2>insecam desk widget</h2><p>Give the widget your Wi-Fi so it can reach the cameras.</p>"
|
|
"<form method=post action=/save><label>Wi-Fi name<input name=ssid maxlength=32 required autocapitalize=off></label>"
|
|
"<label>Password<input name=password type=password maxlength=63></label><button>Save and restart</button></form>";
|
|
|
|
static bool form_value(const char *body, const char *key, char *out, size_t cap)
|
|
{
|
|
size_t klen = strlen(key);
|
|
for (const char *p = body; *p;) {
|
|
const char *end = strchr(p, '&');
|
|
if (!end) end = p + strlen(p);
|
|
if ((size_t)(end - p) > klen && !strncmp(p, key, klen) && p[klen] == '=') {
|
|
size_t n = 0;
|
|
for (p += klen + 1; p < end; p++) {
|
|
unsigned char ch = *p;
|
|
if (ch == '+') ch = ' ';
|
|
else if (ch == '%') {
|
|
if (end - p < 3 || !isxdigit((unsigned char)p[1]) || !isxdigit((unsigned char)p[2])) return false;
|
|
char hex[3] = {p[1], p[2], 0};
|
|
ch = strtoul(hex, NULL, 16);
|
|
p += 2;
|
|
}
|
|
if (!ch || n + 1 >= cap) return false;
|
|
out[n++] = ch;
|
|
}
|
|
out[n] = 0;
|
|
return true;
|
|
}
|
|
p = *end ? end + 1 : end;
|
|
}
|
|
if (cap) out[0] = 0;
|
|
return false;
|
|
}
|
|
|
|
static esp_err_t page_handler(httpd_req_t *req)
|
|
{
|
|
httpd_resp_set_type(req, "text/html");
|
|
return httpd_resp_sendstr(req, PAGE);
|
|
}
|
|
|
|
// Anything else (Android's /generate_204, Apple's /hotspot-detect.html, ...) bounces to the form,
|
|
// which is what makes phones pop the "sign in to network" sheet.
|
|
static esp_err_t redirect_handler(httpd_req_t *req)
|
|
{
|
|
httpd_resp_set_status(req, "302 Found");
|
|
httpd_resp_set_hdr(req, "Location", "http://192.168.4.1/");
|
|
return httpd_resp_sendstr(req, "");
|
|
}
|
|
|
|
static void restart_task(void *arg)
|
|
{
|
|
vTaskDelay(pdMS_TO_TICKS(1500));
|
|
esp_restart();
|
|
}
|
|
|
|
static esp_err_t save_handler(httpd_req_t *req)
|
|
{
|
|
char body[512], ssid[33], password[65];
|
|
if (req->content_len <= 0 || req->content_len >= sizeof body)
|
|
return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "bad form");
|
|
size_t used = 0;
|
|
while (used < req->content_len) {
|
|
int n = httpd_req_recv(req, body + used, req->content_len - used);
|
|
if (n <= 0) return ESP_FAIL;
|
|
used += n;
|
|
}
|
|
body[used] = 0;
|
|
if (!form_value(body, "ssid", ssid, sizeof ssid) || !ssid[0])
|
|
return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Wi-Fi name missing");
|
|
form_value(body, "password", password, sizeof password);
|
|
|
|
wifi_config_t cfg = {0};
|
|
memcpy(cfg.sta.ssid, ssid, strlen(ssid));
|
|
memcpy(cfg.sta.password, password, strlen(password));
|
|
cfg.sta.threshold.authmode = password[0] ? WIFI_AUTH_WPA_PSK : WIFI_AUTH_OPEN;
|
|
memset(body, 0, sizeof body);
|
|
memset(password, 0, sizeof password);
|
|
if (esp_wifi_set_config(WIFI_IF_STA, &cfg) != ESP_OK)
|
|
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "could not save");
|
|
memset(&cfg, 0, sizeof cfg);
|
|
httpd_resp_set_type(req, "text/html");
|
|
httpd_resp_sendstr(req, "<meta name=viewport content='width=device-width'><p style='font:20px sans-serif'>Saved. The widget is restarting.</p>");
|
|
xTaskCreate(restart_task, "restart", 2048, NULL, 2, NULL);
|
|
return ESP_OK;
|
|
}
|
|
|
|
// Minimal DNS: answer every A query with 192.168.4.1 so captive-portal probes land on us.
|
|
static void dns_task(void *arg)
|
|
{
|
|
int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
|
|
struct sockaddr_in addr = {.sin_family = AF_INET, .sin_port = htons(53), .sin_addr.s_addr = htonl(INADDR_ANY)};
|
|
if (sock < 0 || bind(sock, (struct sockaddr *)&addr, sizeof addr) < 0) {
|
|
ESP_LOGE(TAG, "dns socket failed");
|
|
vTaskDelete(NULL);
|
|
return;
|
|
}
|
|
uint8_t buf[512];
|
|
while (1) {
|
|
struct sockaddr_in from;
|
|
socklen_t flen = sizeof from;
|
|
int n = recvfrom(sock, buf, sizeof buf - 16, 0, (struct sockaddr *)&from, &flen);
|
|
if (n < 12 || (buf[2] & 0x80)) continue; // not a query
|
|
int qd = (buf[4] << 8) | buf[5];
|
|
if (qd < 1) continue;
|
|
int p = 12; // walk the first QNAME
|
|
while (p < n && buf[p]) p += buf[p] + 1;
|
|
if (p + 5 > n) continue;
|
|
int qtype = (buf[p + 1] << 8) | buf[p + 2];
|
|
int qend = p + 5;
|
|
buf[2] = 0x84; buf[3] = 0x00; // response, authoritative, no error
|
|
buf[4] = 0; buf[5] = 1; // one question
|
|
buf[6] = 0; buf[7] = qtype == 1 ? 1 : 0; // one answer for A queries
|
|
buf[8] = buf[9] = buf[10] = buf[11] = 0;
|
|
int len = qend;
|
|
if (qtype == 1) {
|
|
static const uint8_t ans[] = {0xC0, 0x0C, 0, 1, 0, 1, 0, 0, 0, 60, 0, 4, 192, 168, 4, 1};
|
|
memcpy(buf + len, ans, sizeof ans);
|
|
len += sizeof ans;
|
|
}
|
|
sendto(sock, buf, len, 0, (struct sockaddr *)&from, flen);
|
|
}
|
|
}
|
|
|
|
void net_start_portal(void)
|
|
{
|
|
if (portal_started) return;
|
|
portal_started = true;
|
|
|
|
esp_netif_create_default_wifi_ap();
|
|
wifi_config_t ap = {.ap = {.channel = 1, .max_connection = 4, .authmode = WIFI_AUTH_OPEN}};
|
|
strlcpy((char *)ap.ap.ssid, CONFIG_WIDGET_AP_SSID, sizeof ap.ap.ssid);
|
|
ap.ap.ssid_len = strlen((char *)ap.ap.ssid);
|
|
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_APSTA));
|
|
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &ap));
|
|
|
|
httpd_handle_t server;
|
|
httpd_config_t hc = HTTPD_DEFAULT_CONFIG();
|
|
hc.uri_match_fn = httpd_uri_match_wildcard;
|
|
ESP_ERROR_CHECK(httpd_start(&server, &hc));
|
|
httpd_uri_t root = {.uri = "/", .method = HTTP_GET, .handler = page_handler};
|
|
httpd_uri_t save = {.uri = "/save", .method = HTTP_POST, .handler = save_handler};
|
|
httpd_uri_t any = {.uri = "/*", .method = HTTP_GET, .handler = redirect_handler};
|
|
httpd_register_uri_handler(server, &root);
|
|
httpd_register_uri_handler(server, &save);
|
|
httpd_register_uri_handler(server, &any);
|
|
xTaskCreate(dns_task, "dns", 3072, NULL, 3, NULL);
|
|
ESP_LOGI(TAG, "setup hotspot \"%s\" at http://192.168.4.1", CONFIG_WIDGET_AP_SSID);
|
|
}
|