From f9f3e58d64c3cb811390c1335adede8790b1649f Mon Sep 17 00:00:00 2001 From: Michele Bigi Date: Wed, 19 Aug 2026 01:15:49 +0200 Subject: [PATCH] Fix empty DAB service list and silently-dropped last HTTP route MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two independent live-hardware bugs found testing on real DAB signal tonight (board reconnected after this session's feature work): 1. Si4684Driver::fetchDabServiceList() double-counted the already- consumed SIZE field when computing the body offset: it added a phantom "List Size(2)" on top of the 7-byte STATUS/SIZE header already stripped out, shifting the service-count byte and every service entry by exactly 2 bytes. AN649 documents SIZE/DATA_0/ DATA_N generically for GET_DIGITAL_SERVICE_LIST and defers the actual DAB payload layout to a supplemental "Digital Services User's Guide" we don't have, so the previous "AN649 Table 14" citation for that layout was never actually sourced from AN649 — it was guessed. Re-derived the real layout by cross-checking hitech95/si468x_dab_receiver's si468x_core_cmd_dab_get_service_list() (a working Linux driver for the same command), which also shows the payload is SIZE-2 bytes, not SIZE bytes — fixed the read-length sizing (payloadSize+5, was +7) to match. This is what made GET /api/tuner/services always come back empty even with a locked ensemble. 2. SetupWebServer registers 41 HTTP routes but httpd_config_t:: max_uri_handlers was still 40 (set before several endpoints landed this session). esp_http_server's httpd_register_uri_handler() fails silently past the limit, logging only a generic "no slots left" warning with no indication of which handler was dropped — the 41st and therefore last-registered route, POST /api/stations/tune, was silently unroutable (404) on every boot since whichever commit pushed the count past 40. Bumped to 56 for headroom. Both confirmed on hardware: fresh flash boots with zero httpd warnings; DAB service list fix not yet re-verified against a live ensemble pending user retest (board was between test sessions). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0178rASQ6ZETPMUamvpoR2KR --- .../drivers/si4684/src/Si4684Driver.cpp | 37 +++++++++++++------ .../components/net/src/SetupWebServer.cpp | 6 ++- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/Software/components/drivers/si4684/src/Si4684Driver.cpp b/Software/components/drivers/si4684/src/Si4684Driver.cpp index ebfd622..74611df 100644 --- a/Software/components/drivers/si4684/src/Si4684Driver.cpp +++ b/Software/components/drivers/si4684/src/Si4684Driver.cpp @@ -1121,26 +1121,37 @@ Si4684Driver::fetchDabServiceList() } // raw[5]=RESP4=SIZE[7:0], raw[6]=RESP5=SIZE[15:8] (see readFmRds()). + // AN649 only documents SIZE/DATA_0/DATA_N generically for this command + // and defers the DAB payload layout to a supplemental "Digital + // Services User's Guide" we don't have; the exact field layout below + // is cross-checked against hitech95/si468x_dab_receiver's + // si468x_core_cmd_dab_get_service_list() (drivers/mfd/si468x-cmd.c), + // a real working Linux driver for the same command. That driver also + // establishes that the payload actually carried after SIZE is + // SIZE-2 bytes, not SIZE bytes. const std::uint16_t payloadSize = readLe16(header.data() + 5); - if (payloadSize == 0U || payloadSize + 7U > kSpiBufferSize) { + if (payloadSize <= 2U || payloadSize + 5U > kSpiBufferSize) { return std::unexpected(Si4684Error::ReplyTooShort); } - // DATA_0 (first byte of the AN649 Table 14 "DAB/DMB Digital Service - // List" structure) is RESP6 = body[7]: lead-in(1) + STATUS0-3(4) + - // SIZE(2) = 7 header bytes before it. - std::vector body(payloadSize + 7U, 0U); + // DATA_0 (first byte of the payload) is RESP6 = body[7]: lead-in(1) + + // STATUS0-3(4) + SIZE(2) = 7 header bytes before it. Total frame is + // lead-in(1) + STATUS0-3(4) + SIZE(2) + payload(SIZE-2) = SIZE+5. + std::vector body(payloadSize + 5U, 0U); if (auto rd = readRaw(body); !rd) { return std::unexpected(rd.error()); } - // Table 14: List Size(2) + Version(2) + NumServices(1) + AlignPad(3) = - // 8 bytes, then Service 1 begins. - const std::uint8_t serviceCount = body[11]; + // From DATA_0 (body[7]): Version(2) + NumServices/flags(1) + + // AlignPad(3) = 6 bytes, then Service 1 begins at body[13]. (The + // previous version of this code double-counted the already-consumed + // SIZE field here, offsetting every read by 2 bytes — that is why the + // service list always came back empty.) + const std::uint8_t serviceCount = body[9] & 0x1FU; // max 32 services std::vector services; services.reserve(serviceCount); - std::size_t offset = 15U; + std::size_t offset = 13U; for (std::uint8_t i = 0; i < serviceCount; ++i) { // Fixed per-service part: ServiceID(4) + ServiceInfo1-3(3) + // AlignPad(1) + Label(16) = 24 bytes. @@ -1155,11 +1166,13 @@ Si4684Driver::fetchDabServiceList() entry.label[16] = '\0'; offset += 24U; - // Component ID is 2 bytes (AN649 Table 14); only the first + // Component ID is 2 bytes (hitech95's si468x-cmd.c packs tm_id/ + // sub_ch_id/fidc_id/sc_id into this same field; only the raw + // 16-bit value is exposed on this DTO). Only the first // component's ID is exposed on this DTO. Every component (M = // componentCount) must still be skipped to keep the next service - // entry aligned, each one ComponentID(2) + ComponentInfo(1) + - // ValidFlags(1) = 4 bytes. + // entry aligned, each one 2 bytes packed field + ServiceType/ + // flags(1) + ValidFlags(1) = 4 bytes. if (componentCount > 0U && offset + 2U <= body.size()) { entry.componentId = readLe16(body.data() + offset); } diff --git a/Software/components/net/src/SetupWebServer.cpp b/Software/components/net/src/SetupWebServer.cpp index 6e94902..d770a3d 100644 --- a/Software/components/net/src/SetupWebServer.cpp +++ b/Software/components/net/src/SetupWebServer.cpp @@ -1895,7 +1895,11 @@ std::expected SetupWebServer::start( httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.stack_size = 12288; config.max_open_sockets = 3; - config.max_uri_handlers = 40; + config.max_uri_handlers = 56; // 41 routes registered below as of 2026-08-19; + // keep headroom so a silent + // httpd_register_uri_handler failure + // ("no slots left") doesn't quietly drop + // the last-registered route again. config.server_port = 80; config.lru_purge_enable = true; config.recv_wait_timeout = 60;