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.
225 lines
8.1 KiB
C
225 lines
8.1 KiB
C
#include "httpstream.h"
|
|
#include "esp_log.h"
|
|
#include "lwip/netdb.h"
|
|
#include "lwip/sockets.h"
|
|
#include <ctype.h>
|
|
#include <errno.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
static const char *TAG = "http";
|
|
#define RX_CAP 4096
|
|
#define MAX_REDIRECTS 3
|
|
#define HEADER_TIMEOUT_MS 5000
|
|
|
|
typedef struct { char host[128]; uint16_t port; char path[512]; } url_t;
|
|
|
|
static bool parse_url(const char *url, url_t *u)
|
|
{
|
|
if (strncasecmp(url, "http://", 7)) return false; // https cameras aren't worth TLS here
|
|
const char *p = url + 7, *end = p + strcspn(p, "/?");
|
|
const char *colon = memchr(p, ':', end - p);
|
|
size_t hlen = (colon ? colon : end) - p;
|
|
if (!hlen || hlen >= sizeof u->host) return false;
|
|
memcpy(u->host, p, hlen);
|
|
u->host[hlen] = 0;
|
|
u->port = colon ? atoi(colon + 1) : 80;
|
|
if (!u->port) return false;
|
|
if (*end == 0) strcpy(u->path, "/");
|
|
else if (*end == '?') snprintf(u->path, sizeof u->path, "/%s", end);
|
|
else strlcpy(u->path, end, sizeof u->path);
|
|
return true;
|
|
}
|
|
|
|
static int connect_timeout(const url_t *u, int timeout_ms)
|
|
{
|
|
char port[8];
|
|
snprintf(port, sizeof port, "%u", u->port);
|
|
struct addrinfo hints = {.ai_family = AF_INET, .ai_socktype = SOCK_STREAM}, *res = NULL;
|
|
if (getaddrinfo(u->host, port, &hints, &res) != 0 || !res) {
|
|
ESP_LOGW(TAG, "dns failed for %s", u->host);
|
|
return -1;
|
|
}
|
|
int fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
|
|
if (fd < 0) { freeaddrinfo(res); return -1; }
|
|
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);
|
|
int rc = connect(fd, res->ai_addr, res->ai_addrlen);
|
|
freeaddrinfo(res);
|
|
if (rc < 0 && errno != EINPROGRESS) { close(fd); return -1; }
|
|
if (rc < 0) {
|
|
fd_set wf;
|
|
FD_ZERO(&wf);
|
|
FD_SET(fd, &wf);
|
|
struct timeval tv = {.tv_sec = timeout_ms / 1000, .tv_usec = (timeout_ms % 1000) * 1000};
|
|
int so_err = 0;
|
|
socklen_t slen = sizeof so_err;
|
|
if (select(fd + 1, NULL, &wf, NULL, &tv) <= 0 ||
|
|
getsockopt(fd, SOL_SOCKET, SO_ERROR, &so_err, &slen) < 0 || so_err) {
|
|
close(fd);
|
|
return -1;
|
|
}
|
|
}
|
|
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) & ~O_NONBLOCK);
|
|
return fd;
|
|
}
|
|
|
|
static int recv_more(httpstream_t *s)
|
|
{
|
|
if (s->pos && s->pos == s->len) s->pos = s->len = 0;
|
|
if (s->len == s->cap) return HTTPSTREAM_ERR; // caller must drain first
|
|
int n = recv(s->fd, s->buf + s->len, s->cap - s->len, 0);
|
|
if (n > 0) { s->len += n; return n; }
|
|
if (n == 0) return HTTPSTREAM_EOF;
|
|
return (errno == EAGAIN || errno == EWOULDBLOCK) ? HTTPSTREAM_TIMEOUT : HTTPSTREAM_ERR;
|
|
}
|
|
|
|
// Reads one CRLF-terminated line from the buffer into out (without CRLF); pulls more data as needed.
|
|
static int read_line(httpstream_t *s, char *out, size_t cap)
|
|
{
|
|
size_t n = 0;
|
|
while (1) {
|
|
while (s->pos < s->len) {
|
|
char c = s->buf[s->pos++];
|
|
if (c == '\n') { out[n] = 0; if (n && out[n - 1] == '\r') out[n - 1] = 0; return 1; }
|
|
if (n + 1 < cap) out[n++] = c;
|
|
}
|
|
int r = recv_more(s);
|
|
if (r <= 0) return r;
|
|
}
|
|
}
|
|
|
|
static int read_headers(httpstream_t *s, char *location, size_t loc_cap)
|
|
{
|
|
char line[512];
|
|
int r = read_line(s, line, sizeof line);
|
|
if (r <= 0) return r;
|
|
if (strncmp(line, "HTTP/", 5) || strlen(line) < 12) return HTTPSTREAM_ERR;
|
|
s->status = atoi(line + 9);
|
|
s->content_length = -1;
|
|
s->chunked = false;
|
|
s->content_type[0] = 0;
|
|
location[0] = 0;
|
|
while (1) {
|
|
r = read_line(s, line, sizeof line);
|
|
if (r <= 0) return r;
|
|
if (!line[0]) return 1;
|
|
char *v = strchr(line, ':');
|
|
if (!v) continue;
|
|
*v++ = 0;
|
|
while (*v == ' ' || *v == '\t') v++;
|
|
if (!strcasecmp(line, "Content-Length")) s->content_length = atoll(v);
|
|
else if (!strcasecmp(line, "Content-Type")) strlcpy(s->content_type, v, sizeof s->content_type);
|
|
else if (!strcasecmp(line, "Transfer-Encoding") && strcasestr(v, "chunked")) s->chunked = true;
|
|
else if (!strcasecmp(line, "Location")) strlcpy(location, v, loc_cap);
|
|
}
|
|
}
|
|
|
|
esp_err_t httpstream_open(httpstream_t *s, const char *url, const char *user_agent,
|
|
int connect_timeout_ms, int rx_timeout_ms)
|
|
{
|
|
char current[640], location[512];
|
|
strlcpy(current, url, sizeof current);
|
|
memset(s, 0, sizeof *s);
|
|
s->fd = -1;
|
|
s->rx_timeout_ms = rx_timeout_ms;
|
|
s->buf = malloc(RX_CAP);
|
|
if (!s->buf) return ESP_ERR_NO_MEM;
|
|
s->cap = RX_CAP;
|
|
|
|
for (int hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
|
url_t u;
|
|
if (!parse_url(current, &u)) { httpstream_close(s); return ESP_ERR_INVALID_ARG; }
|
|
s->fd = connect_timeout(&u, connect_timeout_ms);
|
|
if (s->fd < 0) { httpstream_close(s); return ESP_ERR_TIMEOUT; }
|
|
// generous while waiting for the status line; the caller's slice timeout applies to the body
|
|
struct timeval tv = {.tv_sec = HEADER_TIMEOUT_MS / 1000, .tv_usec = 0};
|
|
setsockopt(s->fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
|
|
setsockopt(s->fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
|
|
strlcpy(s->host, u.host, sizeof s->host);
|
|
s->port = u.port;
|
|
|
|
char hosthdr[140], req[900];
|
|
if (u.port == 80) strlcpy(hosthdr, u.host, sizeof hosthdr);
|
|
else snprintf(hosthdr, sizeof hosthdr, "%s:%u", u.host, u.port);
|
|
int n = snprintf(req, sizeof req,
|
|
"GET %s HTTP/1.1\r\nHost: %s\r\nUser-Agent: %s\r\nAccept: */*\r\nConnection: close\r\n\r\n",
|
|
u.path, hosthdr, user_agent);
|
|
if (n <= 0 || n >= (int)sizeof req || send(s->fd, req, n, 0) != n) { httpstream_close(s); return ESP_FAIL; }
|
|
|
|
s->len = s->pos = 0;
|
|
int r = read_headers(s, location, sizeof location);
|
|
if (r <= 0) { httpstream_close(s); return r == HTTPSTREAM_TIMEOUT ? ESP_ERR_TIMEOUT : ESP_FAIL; }
|
|
|
|
if ((s->status == 301 || s->status == 302 || s->status == 303 || s->status == 307 || s->status == 308) && location[0]) {
|
|
close(s->fd);
|
|
s->fd = -1;
|
|
if (location[0] == '/') snprintf(current, sizeof current, "http://%s:%u%s", u.host, u.port, location);
|
|
else strlcpy(current, location, sizeof current);
|
|
ESP_LOGI(TAG, "redirect -> %s", current);
|
|
continue;
|
|
}
|
|
s->chunk_left = 0;
|
|
s->body_read = 0;
|
|
s->eof = false;
|
|
tv.tv_sec = rx_timeout_ms / 1000;
|
|
tv.tv_usec = (rx_timeout_ms % 1000) * 1000;
|
|
setsockopt(s->fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
|
|
return ESP_OK;
|
|
}
|
|
httpstream_close(s);
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
static int read_raw(httpstream_t *s, uint8_t *out, size_t n)
|
|
{
|
|
if (s->pos == s->len) {
|
|
int r = recv_more(s);
|
|
if (r <= 0) return r;
|
|
}
|
|
size_t avail = s->len - s->pos;
|
|
if (n > avail) n = avail;
|
|
memcpy(out, s->buf + s->pos, n);
|
|
s->pos += n;
|
|
return n;
|
|
}
|
|
|
|
int httpstream_read(httpstream_t *s, uint8_t *out, size_t n)
|
|
{
|
|
if (s->eof || s->fd < 0) return HTTPSTREAM_EOF;
|
|
if (!s->chunked) {
|
|
if (s->content_length >= 0) {
|
|
int64_t left = s->content_length - s->body_read;
|
|
if (left <= 0) { s->eof = true; return HTTPSTREAM_EOF; }
|
|
if ((int64_t)n > left) n = left;
|
|
}
|
|
int r = read_raw(s, out, n);
|
|
if (r == HTTPSTREAM_EOF) s->eof = true;
|
|
if (r > 0) s->body_read += r;
|
|
return r;
|
|
}
|
|
// chunked
|
|
while (s->chunk_left == 0) {
|
|
char line[64];
|
|
int r = read_line(s, line, sizeof line);
|
|
if (r <= 0) return r;
|
|
if (!line[0]) continue; // CRLF after the previous chunk
|
|
s->chunk_left = strtoll(line, NULL, 16);
|
|
if (s->chunk_left == 0) { s->eof = true; return HTTPSTREAM_EOF; }
|
|
}
|
|
if ((int64_t)n > s->chunk_left) n = s->chunk_left;
|
|
int r = read_raw(s, out, n);
|
|
if (r > 0) { s->chunk_left -= r; s->body_read += r; }
|
|
else if (r == HTTPSTREAM_EOF) s->eof = true;
|
|
return r;
|
|
}
|
|
|
|
void httpstream_close(httpstream_t *s)
|
|
{
|
|
if (s->fd >= 0) close(s->fd);
|
|
s->fd = -1;
|
|
free(s->buf);
|
|
s->buf = NULL;
|
|
s->eof = true;
|
|
}
|