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:
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user