Fix empty DAB service list and silently-dropped last HTTP route

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0178rASQ6ZETPMUamvpoR2KR
This commit is contained in:
2026-08-19 01:17:52 +02:00
co-authored by Claude Sonnet 5
parent 3244b9f084
commit f9f3e58d64
2 changed files with 30 additions and 13 deletions
@@ -1121,26 +1121,37 @@ Si4684Driver::fetchDabServiceList()
} }
// raw[5]=RESP4=SIZE[7:0], raw[6]=RESP5=SIZE[15:8] (see readFmRds()). // 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); 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); return std::unexpected(Si4684Error::ReplyTooShort);
} }
// DATA_0 (first byte of the AN649 Table 14 "DAB/DMB Digital Service // DATA_0 (first byte of the payload) is RESP6 = body[7]: lead-in(1) +
// List" structure) is RESP6 = body[7]: lead-in(1) + STATUS0-3(4) + // STATUS0-3(4) + SIZE(2) = 7 header bytes before it. Total frame is
// SIZE(2) = 7 header bytes before it. // lead-in(1) + STATUS0-3(4) + SIZE(2) + payload(SIZE-2) = SIZE+5.
std::vector<std::uint8_t> body(payloadSize + 7U, 0U); std::vector<std::uint8_t> body(payloadSize + 5U, 0U);
if (auto rd = readRaw(body); !rd) { if (auto rd = readRaw(body); !rd) {
return std::unexpected(rd.error()); return std::unexpected(rd.error());
} }
// Table 14: List Size(2) + Version(2) + NumServices(1) + AlignPad(3) = // From DATA_0 (body[7]): Version(2) + NumServices/flags(1) +
// 8 bytes, then Service 1 begins. // AlignPad(3) = 6 bytes, then Service 1 begins at body[13]. (The
const std::uint8_t serviceCount = body[11]; // 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<Si4684DabService> services; std::vector<Si4684DabService> services;
services.reserve(serviceCount); services.reserve(serviceCount);
std::size_t offset = 15U; std::size_t offset = 13U;
for (std::uint8_t i = 0; i < serviceCount; ++i) { for (std::uint8_t i = 0; i < serviceCount; ++i) {
// Fixed per-service part: ServiceID(4) + ServiceInfo1-3(3) + // Fixed per-service part: ServiceID(4) + ServiceInfo1-3(3) +
// AlignPad(1) + Label(16) = 24 bytes. // AlignPad(1) + Label(16) = 24 bytes.
@@ -1155,11 +1166,13 @@ Si4684Driver::fetchDabServiceList()
entry.label[16] = '\0'; entry.label[16] = '\0';
offset += 24U; 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 = // component's ID is exposed on this DTO. Every component (M =
// componentCount) must still be skipped to keep the next service // componentCount) must still be skipped to keep the next service
// entry aligned, each one ComponentID(2) + ComponentInfo(1) + // entry aligned, each one 2 bytes packed field + ServiceType/
// ValidFlags(1) = 4 bytes. // flags(1) + ValidFlags(1) = 4 bytes.
if (componentCount > 0U && offset + 2U <= body.size()) { if (componentCount > 0U && offset + 2U <= body.size()) {
entry.componentId = readLe16(body.data() + offset); entry.componentId = readLe16(body.data() + offset);
} }
@@ -1895,7 +1895,11 @@ std::expected<void, NetError> SetupWebServer::start(
httpd_config_t config = HTTPD_DEFAULT_CONFIG(); httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.stack_size = 12288; config.stack_size = 12288;
config.max_open_sockets = 3; 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.server_port = 80;
config.lru_purge_enable = true; config.lru_purge_enable = true;
config.recv_wait_timeout = 60; config.recv_wait_timeout = 60;