src.nth.io/

summaryrefslogtreecommitdiff
path: root/main/display.h
AgeCommit message (Collapse)AuthorFilesLines
3 daysmain: code-review fixes — brightness units, OTA stall cap, stats + lockingLuke Hoersten1-10/+0
- 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.
5 daysdisplay: tear-free frame path via triple buffering + scan trackingLuke Hoersten1-6/+13
draw_bitmap on a direct fb pointer only updates the driver's cur_fb_index; the DPI DMA reloads that index at the END of the in-progress frame scan (~21ms period at ~47Hz). Under double buffering, flipping and immediately decoding the next frame into the other fb writes a buffer the DMA may still be scanning out — a torn frame. This regime is common now that the TCP window fix delivers frames back-to-back (decode starts ~6ms after flip). Fix with zero added latency: num_fbs 2 -> 3 (+1.15MB PSRAM of 25MB free), track the actually-scanning fb via on_refresh_done (fires in the DMA-done ISR exactly when the DMA reloads cur_fb_index), and pick the decode target as the fb that is neither pending display nor scanning. Three buffers minus at most two excluded roles = always a free one; no waiting on vsync anywhere. Instrumented: /state tear_guard_engaged counts back-buffer picks made while the previous fb was still mid-scan — each one is a frame that would have torn under double buffering.
2026-06-15firmware: double-buffer the panel + zero-copy decode → ~22 fps ceilingLuke Hoersten1-0/+14
Per-frame paint cost drops from ~24 ms to ~45 µs (≈500× faster) by enabling num_fbs=2 on the DPI panel and decoding straight into the back framebuffer. Measured on the bench: before: lock=7us ttfb=370us body=40ms dec=6ms paint=24ms post=35us = ~70ms / ~14fps after : lock=7us ttfb=330us body=38ms dec=6ms paint=42us post=25us = ~45ms / ~22fps How the win actually lands: - num_fbs=2 in the esp_lcd_dpi_panel_config_t makes the IDF driver allocate two framebuffers and stream from one while we fill the other. - display_back_buffer() returns the inactive fb pointer + its size. - jpeg_decoder_decode() now accepts a caller-provided destination buffer instead of owning its own scratch. http_api passes the panel back-fb so the hardware JPEG decoder writes BGR888 pixels straight into where the DSI will eventually scan from. Zero memcpy in the hot path. - display_flip_back_buffer() calls esp_lcd_panel_draw_bitmap with the fb pointer. Because the buffer is inside the panel's own fb range, the IDF driver skips its memcpy and just does a cache writeback + swaps cur_fb_index. The actual flip happens on the next vsync, asynchronously — the call returns in microseconds. The remaining ceiling is network body time (~38 ms for ~210 KB JPEGs) and the hardware decoder (~6 ms). Per-viewport JPEG quality (smaller files = shorter body) is the next lever; everything firmware-side is already at or near floor. Also drop the old static jpeg output scratch + JPEG_DECODER_MAX_OUTPUT_BYTES constant — nothing references them anymore.
2026-06-15firmware: zero-copy JPEG → BGR888 → DSI hot path; Scrypted pre-rotatesLuke Hoersten1-1/+10
Architectural rework of the /frame hot path. Scrypted now ships every JPEG already scaled + rotated to the panel's native dimensions (read from the firmware's /state response so nothing is hardcoded on the Scrypted side); the firmware decodes the JPEG straight into a BGR888 buffer that's directly draw_bitmap'able by the DSI driver, with zero CPU pixel work and zero rotation work in between. Firmware - jpeg_decoder now uses JPEG_DECODE_OUT_FORMAT_RGB888 + JPEG_DEC_RGB_ELEMENT_ORDER_BGR. Output buffer sized for 800*480*3. - New display_present_bgr888() is a one-liner that hands the decoder's output straight to esp_lcd_panel_draw_bitmap. - /frame handler validates dimensions against the panel-native VIEWPORT_PANEL_WIDTH x VIEWPORT_PANEL_HEIGHT (was effective_dims branching on orientation). Returns 400 if it's anything else. - /state JSON adds panel_width + panel_height so Scrypted can read them without hardcoding board-specific knowledge. - display_present_rgb565 + s_rot_buf stay for the local-screens cold path (info screen, loading) which still does its own CPU conversion + rotation — infrequent enough that it's not worth the rewrite. Scrypted - startStream() GETs /state at stream-start time, caches panel_width and panel_height in viewport storage, and uses them as the ffmpeg scale target. Falls back to cached or 800x480 if /state is mid-reboot. - For portrait viewports the ffmpeg pipeline now does scale=H:W:flags=lanczos,transpose=1 so the JPEG arrives pre-rotated 90° CW into panel-native dimensions. Landscape is just scale=W:H. - No more in-firmware rotation; Scrypted is the single source of truth for "how do I get this camera frame into a panel-shaped JPEG". Expected ceiling lift: ~5 fps → ~10 fps, gated by the JPEG decoder hardware throughput instead of the CPU rgb565→bgr888 + rotation loop.
2026-06-14Default name = MAC; brief wake on boot; drop dead touch defencesLuke Hoersten1-5/+0
- Seed viewport_name with the full base MAC, colons stripped (e8:f6:0a:e0:90:94 → "e8f60ae09094"). Stable across reboots, globally unique, 12 alphanumeric chars — well inside the mDNS hostname limit even with the "viewport-" prefix. - Add a mac_str field to viewport_state and expose it as the new "mac" key in GET /state and as a "mac" line on the info screen. - "Configured" no longer requires a viewport_name (it always has the MAC default); the scrypted URL alone gates outbound POSTs and the loading-vs-info screen choice. - Strip viewport_name[0] fallbacks in http_api / mdns / local_screens now that the field is never empty. - Replace the wake-on-boot-stays-on behaviour with a ~600 ms flash followed by sleep — long enough for the FT5426 touch IC to come out of its initial unresponsive state, short enough that an idle device doesn't burn the backlight. - Drop the touch defensive cruft added when we were chasing a PoE-power theory: the DEV_MODE=0x00 write at init, the touch_reset_pulse() call at init, and the runtime wedge-self-heal in the polling loop didn't actually fix anything — the wake-on-boot flash did. Also delete the display_touch_reset_pulse() helper since it now has zero callers.
2026-06-14touch: retry + hard-reset when FT5426 boots into wedged 0xff stateLuke Hoersten1-0/+5
After a long bench session of taps + power cycles, the FT5426 on this unit started returning dev_mode=0xff on every read. The polling loop interpreted that as 15 simultaneous touches forever (td_status bits 0..3 all set), wedged `was_down=true` permanently, and no tap or long-press ever fired — the screen looked dead. Two layers of defence in touch_init: 1. retry the dev_mode read up to 10 × 30 ms in case the chip is just slow to come up after PC_RST_TP_N is released. 2. if still stuck, pulse PC_RST_TP_N via a new display_touch_reset_pulse() helper (drop reset for 20 ms then release + 50 ms settle), then retry the read another 10 × 30 ms. Plus a polling-loop sanity check: an FT5x06 supports max 5 simultaneous points, so a TD_STATUS lower-nibble value > 5 is bogus and the poll cycle is skipped. That keeps a wedged chip from producing phantom "15 touches" reports if it slips into 0xff during runtime. If both attempts still report 0xff, we log loudly and let the polling task run — it harmlessly drops cycles via the >5 sanity check. Wake still works via POST /state from Scrypted; this only disables touch gestures on a hardware-stuck boot.
2026-06-14Tidy: delete dead code, inline single-use helpers, fix double-lockLuke Hoersten1-8/+0
Code-review pass after the M5 colour fix. -126 net lines (-215 / +89). Dead code removed: - display_fill, display_test_pattern — M3 bring-up self-tests, no callers - net_eth_is_up — no callers - nvs_config_reset — no callers (factory-reset gesture was removed earlier; NVS wipe now goes through `idf.py erase-flash`) Helpers inlined or collapsed (each had one call site): - state_name / orientation_name — ternaries at the cJSON site - fmt_bytes / fmt_uptime — inline scope in local_screens_show_info - expected_dims (http_api) + effective_dims (local_screens) — merged into viewport_state_effective_dims() in viewport_state, one canonical source state_machine cleanup: - arm_idle_timer_unlocked / disarm_idle_timer collapsed to one arm_idle_timer(ms) that takes the snapshotted timeout (ms==0 disables). Removes the misleading name and the second viewport_state lock acquisition per painted frame. - state_machine_set + state_machine_frame_painted now snapshot state + idle_ms under one lock. mdns_service cleanup: - snapshot_state / apply_hostname / apply_txt collapsed into a single apply_state(include_hostname) helper; mdns_service_start grabs hostname once for hostname_set + log instead of going under the lock three times. http_api cleanup: - POST /config brightness-changed path uses the local `bright` it already validated instead of re-locking + re-reading st->brightness. - Trimmed verbose bring-up comments (stack-size justification, M5 saga, DSI shadow struct) to one line each — kept the load-bearing facts. Smoke-tested on hardware: boot clean, display + JPEG + touch all up ([DJT]), info screen renders, no crashes.
2026-06-13M7: touch + outbound /state POST to ScryptedLuke Hoersten1-0/+6
state_client.{h,c}: - Worker task drains a depth-1 queue (xQueueOverwrite gives the replace-on-full semantics from the spec — in-flight POST is never cancelled; the next queued entry is overwritten by newer state). - esp_http_client POST to <scrypted>/state with Content-Type application/ json, User-Agent ScryptedViewport/<version>, Connection: close, 1s timeout. Body: {"viewport":"<name>","state":"wake"|"sleep"}. - Any non-2xx or transport error increments state_post_failures and is otherwise ignored. Silently drops if no Scrypted URL is configured. touch.{h,c}: - FT5426 capacitive touch on the shared I2C bus at 0x38. - 30ms polling task; tracks down/up transitions, detects taps as down-then-up within 500ms with a 150ms debounce. - On tap, toggles wake/sleep via state_machine_set_local(), which drives the local transition AND fires state_client_post() at Scrypted. state_machine adds state_machine_set_local(): runs the same transition as state_machine_set() then enqueues an outbound POST. Idle-timer expiry now uses this path so Scrypted sees idle-driven sleeps too (the spec's "tighten the race with /frame 409" path stays in place as the fallback). display.h exposes display_i2c_bus(); touch.c uses it instead of re-initializing the same I2C port. app_main starts state_client right after the state machine and starts touch after display init (touch is skipped if display isn't up since they share the bus). CMakeLists.txt: add esp_http_client to REQUIRES. Build clean against ESP-IDF 5.4 (binary ~860 KB; jumped ~210 KB from M6 because esp_http_client + cJSON path pulls in tcp_transport, mbedtls, http_parser). TESTING.md M7: flask-based test receiver, tap dispatch verification, failure-path check (kill receiver, confirm counter increments), queue-coalescing behavior, no-POST-on-Scrypted-initiated, no-POST- when-unconfigured.
2026-06-13M5: POST /frame — hardware JPEG decode + orientation-aware paintLuke Hoersten1-0/+9
jpeg_decoder.{h,c} wraps esp_driver_jpeg (ESP32-P4 hardware decoder). - One-time engine + DMA-aligned PSRAM scratch buffer setup (1 MB input, 768 KB output @ panel native 800x480 RGB565). - try_lock + unlock so concurrent /frame POSTs get 503 instead of queueing, per spec. - jpeg_decoder_get_info() reports the dimensions; the http handler validates them against the effective resolution before painting. display.h adds display_present_rgb565(src, w, h): - Landscape: src is 800x480, memcpy 1:1 into the panel framebuffer. - Portrait: src is 480x800, software rotate 90° CW into the 800x480 panel framebuffer. (PPA / 2D-DMA hardware rotation is a later optimization if portrait latency matters.) http_api.c adds POST /frame: - Content-Type must be image/jpeg → else 400. - Empty body → 400. > 1 MB → 413. Display not initialized → 500. - jpeg_decoder_try_lock(0) for concurrency: second post returns 503. - Body streamed into the decoder's input buffer in chunks. - Decode failure or dimension mismatch → 400 + decode_errors++. - Paint failure → 500. - Success → frames_received++, last_frame_us = esp_timer_get_time(), 204 No Content. app_main initializes the JPEG decoder after display_init(). Both are best-effort: failures log a warning and leave the rest of the firmware running. CMakeLists.txt: add esp_driver_jpeg to REQUIRES. Known gap (M6 closes it): /frame currently paints regardless of wake/sleep state. The 409-when-asleep rule lands with POST /state in M6. Build clean against ESP-IDF 5.4 (binary ~640 KB). TESTING.md M5 expanded with portrait/landscape test commands, ImageMagick test-image recipes, the full negative matrix (wrong Content-Type, oversize, wrong dims, concurrent, garbage), and the M6 dependency note.
2026-06-13M3: display driver (Pi-style DSI panel: TC358762 + ATTINY MCU)Luke Hoersten1-0/+30
display.{h,c} brings up the Hosyond 5\" 800x480 panel via the Raspberry Pi 7\" touchscreen architecture: - I2C init to the panel-side MCU at 0x45 (Pi 7\" register map ported from drivers/gpu/drm/panel/panel-raspberrypi-touchscreen.c). - POWERON write + 120ms settle. - ESP32-P4 MIPI-DSI bring-up in DPI video mode: 2-lane, 480 Mbps, 800x480 @ 60Hz, RGB565, canonical Pi 7\" timings (HSW=18/HBP=20/HFP=62, VSW=4/VBP=27/VFP=18, 30 MHz pixel clock). - Gamma-corrected (^2.2) PWM brightness via REG_PWM (0x86). - display_sleep / display_wake hooks for M6. - display_fill() and display_test_pattern() (8 vertical color bars) for M3 acceptance and reuse by M8 local screens. app_main calls display_init() as best-effort — if the panel isn't attached or wiring is wrong, the rest of the firmware (Ethernet, mDNS, /state) keeps running. On success it paints the test pattern. Pin assignments (PIN_I2C_SDA=7, PIN_I2C_SCL=8) match Waveshare's bundled-panel BSP convention but are flagged TODO until the user's specific FPC adapter wiring is confirmed. main/CMakeLists.txt: add driver, esp_driver_i2c, esp_lcd to REQUIRES. Build verified clean against ESP-IDF 5.4 for target esp32p4. M3 acceptance (color bars on screen) cannot be verified without a flashed board + connected panel.