src.nth.io/

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuke Hoersten <[email protected]>2026-07-17 18:56:03 -0500
committerLuke Hoersten <[email protected]>2026-07-17 18:56:03 -0500
commit9d94f710f7b00d109bf6f6ee57996f43bce5c67f (patch)
treeb86320353884536d2d1b342efa27600b19c845de
parent866015678918560a0d0519be8a2549e4fc23b2cd (diff)
main: code-review fixes — brightness units, OTA stall cap, stats + locking
- display: s_last_pwm cached the raw 0-100 percentage at init but display_wake writes it straight to REG_PWM (0-255 duty) — first wake ran visibly dim until a /config brightness change. Shared pct_to_duty helper now converts in both paths. - http_api: cap consecutive OTA recv timeouts (a stalled client spun forever holding s_ota_in_progress, wedging OTA until reboot); cap viewport name at 54 chars so viewport-<name> fits the 63-byte mDNS label; log mdns_service_refresh failures; /state builds JSON from a snapshot instead of holding the state lock across ~25 cJSON allocs; respond_400 delegates to respond_status. - stream_server: bytes_in_window now counts painted frames only (frames discarded while asleep inflated the first post-wake window's MB/s, avg-jpeg and chunk/wire averages; recv_bytes folded in); so_rcvbuf carried into the stats snapshot under the mux; drop dead HEADER_BYTES; merge read_body_instrumented into read_n. - state_machine: transition mutex serializes concurrent wake/sleep (display side-effects ran after the state lock dropped, so racing callers could leave the backlight contradicting st->state); wake-path placeholder paint now takes the decoder lock like the stream path (concurrent esp_lcd_panel_draw_bitmap from two tasks isn't safe). - local_screens: overlay paint takes the decoder lock too; check the overlay timer create; panel dims from viewport_state.h. - jpeg_decoder: try_lock before init returns busy instead of passing a NULL semaphore to xSemaphoreTake. - net_eth: clear cached IP string on link down (stale /state + info). - dead code: display_present_bgr888, TOUCH_FT5426_ADDR, touch s_task, JPEG_DECODER_MAX_OUTPUT_BYTES; doc drift in nvs_config.h / jpeg_decoder.h / app_main flag legend.
-rw-r--r--main/app_main.c4
-rw-r--r--main/display.c33
-rw-r--r--main/display.h10
-rw-r--r--main/http_api.c59
-rw-r--r--main/jpeg_decoder.c3
-rw-r--r--main/jpeg_decoder.h6
-rw-r--r--main/local_screens.c15
-rw-r--r--main/net_eth.c3
-rw-r--r--main/nvs_config.h4
-rw-r--r--main/ota.c3
-rw-r--r--main/state_machine.c25
-rw-r--r--main/stream_server.c75
-rw-r--r--main/stream_server.h2
-rw-r--r--main/touch.c3
14 files changed, 143 insertions, 102 deletions
diff --git a/main/app_main.c b/main/app_main.c
index f1aae13..c131fe0 100644
--- a/main/app_main.c
+++ b/main/app_main.c
@@ -56,7 +56,9 @@ void app_main(void)
// don't get a DHCP lease. mDNS + HTTP advertise / bind anyway and will
// start serving the moment the link comes up.
// ------------------------------------------------------------------
- char flags[8] = { '-','-','-','-','-','-','-', 0 }; // E M H S D J T
+ char flags[5] = { '-','-','-','-', 0 }; // E M H S (display-task
+ // subsystems D J T report
+ // separately in dsp_init)
esp_err_t eth_err = net_eth_init();
mark(eth_err, 'E', &flags[0]);
diff --git a/main/display.c b/main/display.c
index 7441200..8b86812 100644
--- a/main/display.c
+++ b/main/display.c
@@ -32,7 +32,6 @@ static const char *TAG = "display";
#define I2C_FREQ_HZ 400000
#define PANEL_MCU_ADDR 0x45 // ATTINY88 on the Pi 7" panel architecture
-#define TOUCH_FT5426_ADDR 0x38
// ATTINY88 register map. Source:
// linux/drivers/regulator/rpi-panel-attiny-regulator.c (rpi-6.6.y)
@@ -63,8 +62,8 @@ enum {
// ESP32-P4. The Linux upstream modeline differs in HFP/VSW; if pixels look
// shifted left/right these are the first knobs to revisit.
// ============================================================================
-#define PANEL_H_ACTIVE 800
-#define PANEL_V_ACTIVE 480
+#define PANEL_H_ACTIVE VIEWPORT_PANEL_WIDTH
+#define PANEL_V_ACTIVE VIEWPORT_PANEL_HEIGHT
#define RPI_HSW 2
#define RPI_HBP 46
#define RPI_HFP 210
@@ -428,6 +427,18 @@ static esp_err_t dsi_bring_up(void)
// ============================================================================
// Public API
// ============================================================================
+
+// 0-100 user percentage → gamma-corrected 0-255 PWM duty. s_last_pwm
+// always holds a DUTY, never a raw percentage — display_wake() writes it
+// straight to REG_PWM.
+static uint8_t pct_to_duty(uint8_t pct)
+{
+ if (pct > 100) pct = 100;
+ float frac = (float)pct / 100.0f;
+ float gamma = powf(frac, 2.2f);
+ return (uint8_t)(gamma * 255.0f + 0.5f);
+}
+
esp_err_t display_init(void)
{
if (s_up) return ESP_OK;
@@ -448,8 +459,9 @@ esp_err_t display_init(void)
// Cache configured brightness, leave backlight off — first wake applies it.
viewport_state_lock();
- s_last_pwm = viewport_state_get()->brightness;
+ uint8_t pct = viewport_state_get()->brightness;
viewport_state_unlock();
+ s_last_pwm = pct_to_duty(pct);
mcu_write_u8(REG_PWM, 0);
return ESP_OK;
@@ -461,10 +473,7 @@ i2c_master_bus_handle_t display_i2c_bus(void) { return s_i2c_bus; }
esp_err_t display_set_brightness(uint8_t pct)
{
- if (pct > 100) pct = 100;
- float frac = (float)pct / 100.0f;
- float gamma = powf(frac, 2.2f);
- uint8_t duty = (uint8_t)(gamma * 255.0f + 0.5f);
+ uint8_t duty = pct_to_duty(pct);
s_last_pwm = duty;
if (!s_up) return ESP_ERR_INVALID_STATE;
@@ -497,14 +506,6 @@ static inline void rgb565_to_rgb888(uint16_t px, uint8_t *out)
out[2] = (uint8_t)(((px >> 11) & 0x1F) * 255 / 31); // R
}
-esp_err_t display_present_bgr888(const void *bgr888)
-{
- if (!s_up) return ESP_ERR_INVALID_STATE;
- return esp_lcd_panel_draw_bitmap(s_panel, 0, 0,
- PANEL_H_ACTIVE, PANEL_V_ACTIVE,
- bgr888);
-}
-
void *display_back_buffer(size_t *out_size)
{
if (!s_up) return NULL;
diff --git a/main/display.h b/main/display.h
index 679a408..1a0e0a0 100644
--- a/main/display.h
+++ b/main/display.h
@@ -38,16 +38,6 @@ esp_err_t display_present_rgb565(const uint16_t *src,
uint16_t src_w,
uint16_t src_h);
-// Zero-copy hot path. Source is already 800x480 with bytes in [B, G, R]
-// memory order (i.e. the format the panel pipeline natively wants);
-// hand it straight to esp_lcd_panel_draw_bitmap. No CPU pixel work, no
-// format conversion, no rotation — Scrypted is responsible for sending
-// the buffer pre-rotated and pre-scaled to panel-native dimensions.
-// If the buffer happens to be one of the panel's own framebuffers
-// (see display_back_buffer / display_flip_back_buffer below), the IDF
-// driver skips the memcpy entirely; otherwise it copies via CPU.
-esp_err_t display_present_bgr888(const void *bgr888);
-
// Framebuffer accessors for the zero-memcpy frame path. The DPI panel
// owns three BGR888 framebuffers (triple buffering): one scanning out,
// one pending display at the next frame boundary, one free.
diff --git a/main/http_api.c b/main/http_api.c
index 5e84db6..6954974 100644
--- a/main/http_api.c
+++ b/main/http_api.c
@@ -33,14 +33,22 @@ static const char *TAG = "http_api";
#define MAX_BODY_BYTES 2048
#define MIN_IDLE_TIMEOUT 5000
+// mDNS hostname is "viewport-<name>" and a DNS label caps at 63 bytes,
+// so the name itself must stay <= 63 - strlen("viewport-") = 54 chars.
+#define MAX_VIEWPORT_NAME 54
// ============================================================================
// GET /state
// ============================================================================
static esp_err_t state_get_handler(httpd_req_t *req)
{
+ // Snapshot under the lock, build JSON unlocked — the stream decode-task
+ // takes this mutex on every painted frame, so keep the hold time to a
+ // struct copy rather than ~25 cJSON allocations.
viewport_state_lock();
- viewport_state_t *st = viewport_state_get();
+ viewport_state_t snap = *viewport_state_get();
+ viewport_state_unlock();
+ viewport_state_t *st = &snap;
uint64_t now_us = (uint64_t)esp_timer_get_time();
uint64_t up_ms = (now_us - st->boot_us) / 1000;
@@ -88,8 +96,6 @@ static esp_err_t state_get_handler(httpd_req_t *req)
round((double)temp_c * 10.0) / 10.0);
}
- viewport_state_unlock();
-
// Most recent closed window of live-stream stats. Populated every
// 30 painted stream frames; zero before the first window rolls.
// last_paint_event_us_low is the low 32 bits of the Scrypted
@@ -188,12 +194,17 @@ static esp_err_t config_get_handler(httpd_req_t *req)
// ============================================================================
// POST /config — partial-update, atomic, validated
// ============================================================================
-static esp_err_t respond_400(httpd_req_t *req, const char *reason)
+static esp_err_t respond_status(httpd_req_t *req, const char *status, const char *body)
{
- ESP_LOGW(TAG, "/config 400: %s", reason);
- httpd_resp_set_status(req, "400 Bad Request");
+ httpd_resp_set_status(req, status);
httpd_resp_set_type(req, "text/plain");
- return httpd_resp_send(req, reason, HTTPD_RESP_USE_STRLEN);
+ return httpd_resp_send(req, body, body ? HTTPD_RESP_USE_STRLEN : 0);
+}
+
+static esp_err_t respond_400(httpd_req_t *req, const char *reason)
+{
+ ESP_LOGW(TAG, "400: %s", reason);
+ return respond_status(req, "400 Bad Request", reason);
}
static esp_err_t read_body(httpd_req_t *req, char *buf, size_t cap)
@@ -233,9 +244,10 @@ static esp_err_t config_post_handler(httpd_req_t *req)
if ((j = cJSON_GetObjectItemCaseSensitive(root, "viewport"))) {
if (!cJSON_IsString(j) || j->valuestring[0] == '\0' ||
- strlen(j->valuestring) >= sizeof(vp)) {
+ strlen(j->valuestring) > MAX_VIEWPORT_NAME) {
cJSON_Delete(root);
- return respond_400(req, "viewport must be a non-empty string < 64 chars");
+ return respond_400(req, "viewport must be a non-empty string <= 54 chars"
+ " (mDNS hostname label limit)");
}
strncpy(vp, j->valuestring, sizeof(vp) - 1);
have_vp = true;
@@ -333,7 +345,11 @@ static esp_err_t config_post_handler(httpd_req_t *req)
display_set_brightness(bright);
}
if (name_or_orient_changed) {
- mdns_service_refresh();
+ esp_err_t mdns_err = mdns_service_refresh();
+ if (mdns_err != ESP_OK) {
+ ESP_LOGW(TAG, "mdns_service_refresh failed: %s",
+ esp_err_to_name(mdns_err));
+ }
}
httpd_resp_set_status(req, "204 No Content");
@@ -383,13 +399,6 @@ static esp_err_t state_post_handler(httpd_req_t *req)
// ============================================================================
// POST /frame
// ============================================================================
-static esp_err_t respond_status(httpd_req_t *req, const char *status, const char *body)
-{
- httpd_resp_set_status(req, status);
- httpd_resp_set_type(req, "text/plain");
- return httpd_resp_send(req, body, body ? HTTPD_RESP_USE_STRLEN : 0);
-}
-
static esp_err_t frame_post_handler(httpd_req_t *req)
{
// Content-Type must be image/jpeg.
@@ -573,10 +582,24 @@ static esp_err_t firmware_post_handler(httpd_req_t *req)
uint8_t buf[4096];
int remaining = req->content_len;
int last_logged_pct = -1;
+ int consecutive_timeouts = 0;
while (remaining > 0) {
int want = remaining < (int)sizeof(buf) ? remaining : (int)sizeof(buf);
int n = httpd_req_recv(req, (char *)buf, want);
- if (n == HTTPD_SOCK_ERR_TIMEOUT) continue;
+ if (n == HTTPD_SOCK_ERR_TIMEOUT) {
+ // A stalled client would otherwise spin here forever with
+ // s_ota_in_progress held, wedging OTA until reboot. Each
+ // timeout is one httpd recv-timeout period, so ~10 in a row
+ // means the sender is gone.
+ if (++consecutive_timeouts >= 10) {
+ err_status = "408 Request Timeout";
+ err_msg = "body stalled";
+ result = ESP_FAIL;
+ goto done;
+ }
+ continue;
+ }
+ consecutive_timeouts = 0;
if (n <= 0) {
err_status = "400 Bad Request";
err_msg = "body read failed";
diff --git a/main/jpeg_decoder.c b/main/jpeg_decoder.c
index 7fee771..8afecca 100644
--- a/main/jpeg_decoder.c
+++ b/main/jpeg_decoder.c
@@ -44,6 +44,9 @@ esp_err_t jpeg_decoder_init(void)
bool jpeg_decoder_try_lock(uint32_t timeout_ms)
{
+ // Callable before jpeg_decoder_init (e.g. a wake POST landing in the
+ // display-task boot window) — treat "no decoder yet" as busy.
+ if (!s_mutex) return false;
return xSemaphoreTake(s_mutex, pdMS_TO_TICKS(timeout_ms)) == pdTRUE;
}
diff --git a/main/jpeg_decoder.h b/main/jpeg_decoder.h
index 495b1a5..beb907b 100644
--- a/main/jpeg_decoder.h
+++ b/main/jpeg_decoder.h
@@ -7,10 +7,10 @@
#include "esp_err.h"
#define JPEG_DECODER_MAX_INPUT_BYTES (1024 * 1024) // 1 MB
-#define JPEG_DECODER_MAX_OUTPUT_BYTES (800 * 480 * 3) // BGR888 panel native
-// One-time setup of the ESP32-P4 hardware JPEG decoder. Allocates reusable
-// DMA-aligned input + output buffers in PSRAM.
+// One-time setup of the ESP32-P4 hardware JPEG decoder. Allocates a reusable
+// DMA-aligned input buffer in PSRAM; output goes to caller-provided buffers
+// (the display back-framebuffer in the hot paths).
esp_err_t jpeg_decoder_init(void);
// The decoder owns shared scratch buffers. Caller must serialize access.
diff --git a/main/local_screens.c b/main/local_screens.c
index 1277d00..351c7c5 100644
--- a/main/local_screens.c
+++ b/main/local_screens.c
@@ -9,13 +9,14 @@
#include "chip_temp.h"
#include "display.h"
+#include "jpeg_decoder.h"
#include "net_eth.h"
#include "viewport_state.h"
static const char *TAG = "screens";
-#define PANEL_W 800
-#define PANEL_H 480
+#define PANEL_W VIEWPORT_PANEL_WIDTH
+#define PANEL_H VIEWPORT_PANEL_HEIGHT
#define MAX_BUF_PX (PANEL_W * PANEL_H)
// 8x8 bitmap font, lit pixels = foreground. Covers the characters used by
@@ -150,7 +151,11 @@ esp_err_t local_screens_init(void)
.callback = &overlay_expired_cb,
.name = "info_overlay",
};
- esp_timer_create(&args, &s_overlay_timer);
+ esp_err_t err = esp_timer_create(&args, &s_overlay_timer);
+ if (err != ESP_OK) {
+ ESP_LOGE(TAG, "overlay timer create failed: %s", esp_err_to_name(err));
+ return err;
+ }
return ESP_OK;
}
@@ -174,7 +179,11 @@ esp_err_t local_screens_overlay(uint32_t duration_ms)
if (!display_is_up()) return ESP_ERR_INVALID_STATE;
esp_timer_stop(s_overlay_timer);
display_wake();
+ // Serialize the overlay paint against the stream decode-task's paints
+ // (concurrent esp_lcd_panel_draw_bitmap calls aren't safe).
+ if (!jpeg_decoder_try_lock(500)) return ESP_ERR_TIMEOUT;
esp_err_t err = local_screens_show_info();
+ jpeg_decoder_unlock();
if (err != ESP_OK) return err;
s_overlay_active = true;
return esp_timer_start_once(s_overlay_timer,
diff --git a/main/net_eth.c b/main/net_eth.c
index 28ef782..3b4a76c 100644
--- a/main/net_eth.c
+++ b/main/net_eth.c
@@ -53,6 +53,9 @@ static void on_eth_event(void *arg, esp_event_base_t event_base,
case ETHERNET_EVENT_DISCONNECTED:
ESP_LOGW(TAG, "link down");
xEventGroupClearBits(s_event_group, BIT_GOT_IP);
+ // Clear the cached address so /state and the info screen don't
+ // keep reporting a lease we no longer hold.
+ s_ip_str[0] = '\0';
break;
case ETHERNET_EVENT_START:
ESP_LOGI(TAG, "ethernet started");
diff --git a/main/nvs_config.h b/main/nvs_config.h
index b90577d..6157517 100644
--- a/main/nvs_config.h
+++ b/main/nvs_config.h
@@ -4,8 +4,8 @@
// Read the persisted config into viewport_state. Safe to call on a fresh
// device: missing keys keep their first-boot defaults from viewport_state_init.
-// Sets state = ASLEEP and configured = true if a viewport name + scrypted URL
-// are both present.
+// Recomputes `configured` (true iff a scrypted URL is present); run state
+// is untouched (stays at the boot default).
esp_err_t nvs_config_load(void);
// Persist the current viewport_state to NVS atomically. The caller is expected
diff --git a/main/ota.c b/main/ota.c
index 3924dca..ee636da 100644
--- a/main/ota.c
+++ b/main/ota.c
@@ -29,6 +29,9 @@ void ota_arm_healthy_timer(void)
.callback = &healthy_cb,
.name = "ota_healthy",
};
+ // The handle is deliberately never deleted: one ~50-byte allocation
+ // per boot, and deleting from inside the callback would race the
+ // dispatcher.
esp_timer_handle_t t = NULL;
if (esp_timer_create(&args, &t) != ESP_OK) return;
esp_timer_start_once(t, OTA_HEALTHY_DELAY_US);
diff --git a/main/state_machine.c b/main/state_machine.c
index 5e688bc..eb2548c 100644
--- a/main/state_machine.c
+++ b/main/state_machine.c
@@ -4,15 +4,22 @@
#include "esp_log.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
+#include "freertos/semphr.h"
#include "freertos/task.h"
#include "display.h"
+#include "jpeg_decoder.h"
#include "local_screens.h"
#include "state_client.h"
static const char *TAG = "state";
static esp_timer_handle_t s_idle_timer;
+// Serializes whole transitions (state write + display side-effects).
+// Without it, concurrent wake+sleep callers (HTTP task, touch task, idle
+// timer) can interleave display_wake/display_sleep after the state lock
+// is dropped and leave the backlight not matching st->state.
+static SemaphoreHandle_t s_transition_mutex;
// ms = idle_timeout_ms snapshotted under the caller's state lock.
// ms == 0 disables the timer (caller's idle_timeout_ms feature).
@@ -31,6 +38,8 @@ static void idle_timer_fired(void *arg)
esp_err_t state_machine_init(void)
{
+ s_transition_mutex = xSemaphoreCreateMutex();
+ if (!s_transition_mutex) return ESP_ERR_NO_MEM;
esp_timer_create_args_t args = {
.callback = &idle_timer_fired,
.name = "viewport_idle",
@@ -43,11 +52,14 @@ esp_err_t state_machine_set(viewport_run_state_t target)
if (target != VIEWPORT_STATE_AWAKE && target != VIEWPORT_STATE_ASLEEP)
return ESP_ERR_INVALID_ARG;
+ xSemaphoreTake(s_transition_mutex, portMAX_DELAY);
+
viewport_state_lock();
viewport_state_t *st = viewport_state_get();
if (st->state == target) {
viewport_state_unlock();
+ xSemaphoreGive(s_transition_mutex);
return ESP_OK; // idempotent no-op
}
st->state = target;
@@ -63,8 +75,16 @@ esp_err_t state_machine_set(viewport_run_state_t target)
// backlight comes up. Without the delay, esp_lcd_panel_draw_bitmap
// returns before the panel has been refreshed and the user sees
// a flash of the previous /frame's contents.
- if (configured) local_screens_show_loading();
- else local_screens_show_info();
+ //
+ // Decoder lock: the stream decode-task paints under this lock,
+ // and concurrent esp_lcd_panel_draw_bitmap calls from two tasks
+ // aren't safe. If the decoder is busy >500 ms a live frame is
+ // painting anyway — skip the placeholder and just light up.
+ if (jpeg_decoder_try_lock(500)) {
+ if (configured) local_screens_show_loading();
+ else local_screens_show_info();
+ jpeg_decoder_unlock();
+ }
vTaskDelay(pdMS_TO_TICKS(33));
display_wake();
}
@@ -75,6 +95,7 @@ esp_err_t state_machine_set(viewport_run_state_t target)
if (display_is_up()) display_sleep();
ESP_LOGI(TAG, "ASLEEP");
}
+ xSemaphoreGive(s_transition_mutex);
return ESP_OK;
}
diff --git a/main/stream_server.c b/main/stream_server.c
index a5c6613..9fc0063 100644
--- a/main/stream_server.c
+++ b/main/stream_server.c
@@ -31,9 +31,6 @@ static const char *TAG = "stream";
#define HEADER_V0_BYTES 8 // legacy: jpeg_len + seq
#define HEADER_V1_BYTES 16 // current: magic + jpeg_len + seq + event_us_low
-#define HEADER_BYTES HEADER_V0_BYTES // FIONREAD threshold for
- // "another header may already
- // be queued" check.
#define MAGIC_V1 0x56505254u // "VPRT" big-endian
#define NUM_BODY_BUFS 3 // ping-pong ring: 1 recv + 1 pending
// + 1 decode → recv never blocks.
@@ -132,41 +129,29 @@ static esp_err_t alloc_body_bufs(void)
return ESP_OK;
}
-// recv() in a loop until n bytes are read or the connection drops.
-// Used for short fixed-size reads (header bytes).
-static esp_err_t read_n(int fd, void *buf, size_t n)
-{
- uint8_t *p = (uint8_t *)buf;
- size_t got = 0;
- while (got < n) {
- ssize_t r = recv(fd, p + got, n - got, 0);
- if (r <= 0) return ESP_FAIL;
- got += (size_t)r;
- }
- return ESP_OK;
-}
-
// ───────────────────────── recv-task ─────────────────────────────────────────
-// Per-frame body read with instrumentation: count recv() syscalls and track
-// chunk size distribution. Returns ESP_OK on full body; ESP_FAIL on EOF/error.
-static esp_err_t read_body_instrumented(int fd, void *buf, size_t n,
- uint32_t *out_calls,
- uint32_t *out_chunk_min,
- uint32_t *out_chunk_max)
+// recv() in a loop until n bytes are read or the connection drops, with
+// optional instrumentation: count recv() syscalls and track chunk size
+// distribution (pass NULL out-params to skip, e.g. for short header reads).
+// Returns ESP_OK on full read; ESP_FAIL on EOF/error.
+static esp_err_t read_n(int fd, void *buf, size_t n,
+ uint32_t *out_calls,
+ uint32_t *out_chunk_min,
+ uint32_t *out_chunk_max)
{
uint8_t *p = (uint8_t *)buf;
size_t got = 0;
- *out_calls = 0;
- *out_chunk_min = UINT32_MAX;
- *out_chunk_max = 0;
+ if (out_calls) *out_calls = 0;
+ if (out_chunk_min) *out_chunk_min = UINT32_MAX;
+ if (out_chunk_max) *out_chunk_max = 0;
while (got < n) {
ssize_t r = recv(fd, p + got, n - got, 0);
if (r <= 0) return ESP_FAIL;
uint32_t rb = (uint32_t)r;
- (*out_calls)++;
- if (rb < *out_chunk_min) *out_chunk_min = rb;
- if (rb > *out_chunk_max) *out_chunk_max = rb;
+ if (out_calls) (*out_calls)++;
+ if (out_chunk_min && rb < *out_chunk_min) *out_chunk_min = rb;
+ if (out_chunk_max && rb > *out_chunk_max) *out_chunk_max = rb;
got += (size_t)r;
}
return ESP_OK;
@@ -247,7 +232,7 @@ static void handle_client_recv(int fd, const char *peer)
while (1) {
uint8_t first4[4];
- if (read_n(fd, first4, 4) != ESP_OK) {
+ if (read_n(fd, first4, 4, NULL, NULL, NULL) != ESP_OK) {
ESP_LOGI(TAG, "client %s disconnected (header read)", peer);
return;
}
@@ -257,7 +242,7 @@ static void handle_client_recv(int fd, const char *peer)
uint32_t jpeg_len, seq, event_us_low;
if (first_word == MAGIC_V1) {
uint8_t rest[HEADER_V1_BYTES - 4];
- if (read_n(fd, rest, sizeof(rest)) != ESP_OK) {
+ if (read_n(fd, rest, sizeof(rest), NULL, NULL, NULL) != ESP_OK) {
ESP_LOGI(TAG, "client %s disconnected (v1 header read)", peer);
return;
}
@@ -269,7 +254,7 @@ static void handle_client_recv(int fd, const char *peer)
| ((uint32_t)rest[10] << 8) | (uint32_t)rest[11];
} else {
uint8_t rest[HEADER_V0_BYTES - 4];
- if (read_n(fd, rest, sizeof(rest)) != ESP_OK) {
+ if (read_n(fd, rest, sizeof(rest), NULL, NULL, NULL) != ESP_OK) {
ESP_LOGI(TAG, "client %s disconnected (v0 header read)", peer);
return;
}
@@ -306,8 +291,8 @@ static void handle_client_recv(int fd, const char *peer)
uint8_t *body = s_bufs[s_recv_idx];
int64_t t0 = esp_timer_get_time();
uint32_t calls = 0, chunk_min = 0, chunk_max = 0;
- if (read_body_instrumented(fd, body, jpeg_len,
- &calls, &chunk_min, &chunk_max) != ESP_OK) {
+ if (read_n(fd, body, jpeg_len,
+ &calls, &chunk_min, &chunk_max) != ESP_OK) {
ESP_LOGI(TAG, "client %s disconnected (body read)", peer);
return;
}
@@ -431,7 +416,6 @@ static void decode_task(void *arg)
uint64_t gap_samples = 0; // first frame of a conn has no gap
int64_t page_min = INT64_MAX, page_max = 0, page_sum = 0;
uint32_t wire_min = UINT32_MAX, wire_max = 0; // kbit/s
- uint64_t recv_bytes = 0; // bytes behind recv_sum
uint32_t queued_min = UINT32_MAX, queued_max = 0; uint64_t queued_sum = 0;
uint32_t calls_min = UINT32_MAX, calls_max = 0; uint64_t calls_sum = 0;
uint32_t chunk_min = UINT32_MAX, chunk_max = 0; uint64_t chunk_total_calls = 0;
@@ -455,8 +439,6 @@ static void decode_task(void *arg)
int64_t idle_us = t_prev_paint_done ? (t_entry - t_prev_paint_done) : 0;
int64_t dwait_us = t_signal - t_wait_start;
- bytes_in_window += meta.jpeg_len;
-
// New connection? Reset stale-seq tracking. recv-task bumped
// conn_id at accept; if we observe a different one here, we're
// starting fresh and seq=1 should paint.
@@ -558,9 +540,12 @@ static void decode_task(void *arg)
if (recv_us < recv_min) recv_min = recv_us;
if (recv_us > recv_max) recv_max = recv_us;
- recv_sum += recv_us;
- recv_bytes += meta.jpeg_len; // painted-frame bytes, matches recv_sum
- // for the window wire-rate average
+ recv_sum += recv_us;
+ // Painted-frame bytes only, so avg-jpeg / MB/s / chunk_avg /
+ // wire_avg all share the same denominator population as the
+ // other per-frame stats (frames discarded while asleep or
+ // stale-seq'd don't skew the window).
+ bytes_in_window += meta.jpeg_len;
if (dec_us < dec_min) dec_min = dec_us;
if (dec_us > dec_max) dec_max = dec_us;
dec_sum += dec_us;
@@ -600,7 +585,7 @@ static void decode_task(void *arg)
uint32_t chunk_avg = chunk_total_calls > 0
? (uint32_t)(bytes_in_window / chunk_total_calls) : 0;
uint32_t wire_avg = recv_sum > 0
- ? (uint32_t)((recv_bytes * 8000u) / (uint64_t)recv_sum) : 0;
+ ? (uint32_t)((bytes_in_window * 8000u) / (uint64_t)recv_sum) : 0;
int64_t gap_avg = gap_samples > 0
? (gap_sum / (int64_t)gap_samples) : 0;
@@ -671,7 +656,6 @@ static void decode_task(void *arg)
.recv_chunk_min = (chunk_min == UINT32_MAX ? 0 : chunk_min),
.recv_chunk_avg = chunk_avg,
.recv_chunk_max = chunk_max,
- .so_rcvbuf = s_stats.so_rcvbuf,
.recv_dropped_oldest = dropped_oldest,
.decode_idle_min_us = (uint32_t)(dwait_min == INT64_MAX ? 0 : dwait_min),
.decode_idle_avg_us = (uint32_t)(dwait_sum / window_samples),
@@ -688,7 +672,10 @@ static void decode_task(void *arg)
.pend_age_max_us = (uint32_t)page_max,
};
portENTER_CRITICAL(&s_stats_mux);
- s_stats = snap;
+ // so_rcvbuf is written by recv-task under this same mux at
+ // accept time; carry it forward inside the critical section.
+ snap.so_rcvbuf = s_stats.so_rcvbuf;
+ s_stats = snap;
portEXIT_CRITICAL(&s_stats_mux);
t_window_start = now;
@@ -699,7 +686,7 @@ static void decode_task(void *arg)
gap_min = page_min = INT64_MAX;
gap_max = page_max = gap_sum = page_sum = 0;
gap_samples = 0;
- wire_min = UINT32_MAX; wire_max = 0; recv_bytes = 0;
+ wire_min = UINT32_MAX; wire_max = 0;
queued_min = calls_min = chunk_min = UINT32_MAX;
queued_max = calls_max = chunk_max = 0;
queued_sum = calls_sum = chunk_total_calls = 0;
diff --git a/main/stream_server.h b/main/stream_server.h
index 2562738..eff4fa8 100644
--- a/main/stream_server.h
+++ b/main/stream_server.h
@@ -44,7 +44,7 @@ esp_err_t stream_server_start(uint16_t port);
// microseconds. Fields are zero before the first window rolls.
typedef struct {
uint64_t frames; // count of painted frames in the window
- uint64_t bytes; // total body bytes received in the window
+ uint64_t bytes; // body bytes of painted frames in the window
uint64_t window_us; // wall-clock span of the window
uint64_t window_end_us; // esp_timer_get_time() at window roll
uint32_t recv_min_us, recv_avg_us, recv_max_us;
diff --git a/main/touch.c b/main/touch.c
index b470536..54c309f 100644
--- a/main/touch.c
+++ b/main/touch.c
@@ -30,7 +30,6 @@ static const char *TAG = "touch";
#define TAP_DEBOUNCE_MS 150
static i2c_master_dev_handle_t s_dev;
-static TaskHandle_t s_task;
static esp_err_t ft_read(uint8_t reg, uint8_t *buf, size_t n)
{
@@ -124,6 +123,6 @@ esp_err_t touch_init(void)
"(tap=toggle wake/sleep, %dms hold=info overlay)",
LONG_PRESS_MS);
- BaseType_t ok = xTaskCreate(touch_task, "touch", 3072, NULL, 4, &s_task);
+ BaseType_t ok = xTaskCreate(touch_task, "touch", 3072, NULL, 4, NULL);
return (ok == pdPASS) ? ESP_OK : ESP_FAIL;
}