Add DAB ANTCAP calibration and fix BT1035 boot banner timing

Extend the FM-only ANTCAP antenna-varactor override to DAB, mirroring the
existing mechanism end to end (driver, tuner, service, EEPROM storage,
HTTP API). Live sweep on real hardware found no ANTCAP value beating
auto-tune on the ensembles tested, so DAB stays on auto-tune by default.

Also fix BT1035 boot: the module's real boot banner doesn't appear until
~18-24s after RESET# releases, not the 3.5s previously waited; add a
2-attempt retry and a baud-rate probe fallback for diagnostics.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 08:12:08 +02:00
co-authored by Claude Sonnet 5
parent 9ad2e42fe2
commit 3a58d33aad
38 changed files with 1444 additions and 115 deletions
@@ -125,10 +125,9 @@ start(core::ISecureStore& store, const core::DeviceIdentity& deviceIdentity)
gStore = &store;
gPendingCreds.reset();
// Copies kept for the lifetime of provisioning: wifi_prov_mgr_start_
// provisioning only borrows these pointers, it does not take ownership.
// Copy kept for the lifetime of provisioning: wifi_prov_mgr_start_
// provisioning only borrows this pointer, it does not take ownership.
static const std::string serviceName(deviceIdentity.softApSsid());
static const std::string pop(deviceIdentity.serialNumber());
const wifi_prov_mgr_config_t config{
.scheme = wifi_prov_scheme_ble,
@@ -141,7 +140,7 @@ start(core::ISecureStore& store, const core::DeviceIdentity& deviceIdentity)
return std::unexpected(NetError::BleProvisioningFailed);
}
if (wifi_prov_mgr_start_provisioning(WIFI_PROV_SECURITY_1, pop.c_str(),
if (wifi_prov_mgr_start_provisioning(WIFI_PROV_SECURITY_0, nullptr,
serviceName.c_str(),
nullptr) != ESP_OK) {
ESP_LOGE(kTag, "wifi_prov_mgr_start_provisioning failed");
@@ -149,9 +148,8 @@ start(core::ISecureStore& store, const core::DeviceIdentity& deviceIdentity)
return std::unexpected(NetError::BleProvisioningFailed);
}
ESP_LOGI(kTag,
"BLE provisioning advertising as %s (proof-of-possession: "
"device serial number)",
ESP_LOGI(kTag, "BLE provisioning advertising as %s (no PoP, same trust "
"level as the open SoftAP)",
serviceName.c_str());
return {};
}
+11 -5
View File
@@ -106,6 +106,7 @@ startSetupMode(core::ISecureStore& store, tuner::TunerService& tuner,
ota::OtaService& ota,
webradio::WebRadioService& webRadio,
PhoneStreamSink& phoneStream,
AntennaCalibration& antennaCalibration,
core::CompanionChipStatus companionChips,
const core::DeviceIdentity& deviceIdentity)
{
@@ -121,7 +122,8 @@ startSetupMode(core::ISecureStore& store, tuner::TunerService& tuner,
if (auto webResult =
webServer.start(store, NetState::SoftApSetup, tuner, audio,
bluetooth, stations, integration, ota, webRadio,
phoneStream, companionChips, deviceIdentity);
phoneStream, antennaCalibration, companionChips,
deviceIdentity);
!webResult) {
return std::unexpected(webResult.error());
}
@@ -168,6 +170,7 @@ startStaMode(core::ISecureStore& store, tuner::TunerService& tuner,
ota::OtaService& ota,
webradio::WebRadioService& webRadio,
PhoneStreamSink& phoneStream,
AntennaCalibration& antennaCalibration,
core::CompanionChipStatus companionChips,
const core::DeviceIdentity& deviceIdentity)
{
@@ -198,7 +201,8 @@ startStaMode(core::ISecureStore& store, tuner::TunerService& tuner,
if (auto webResult =
webServer.start(store, NetState::StaConnected, tuner, audio,
bluetooth, stations, integration, ota, webRadio,
phoneStream, companionChips, deviceIdentity);
phoneStream, antennaCalibration, companionChips,
deviceIdentity);
!webResult) {
return std::unexpected(webResult.error());
}
@@ -227,6 +231,7 @@ NetBootstrap::start(core::ISecureStore& store, tuner::TunerService& tuner,
ota::OtaService& ota,
webradio::WebRadioService& webRadio,
PhoneStreamSink& phoneStream,
AntennaCalibration& antennaCalibration,
core::CompanionChipStatus companionChips,
const core::DeviceIdentity& deviceIdentity)
{
@@ -241,7 +246,8 @@ NetBootstrap::start(core::ISecureStore& store, tuner::TunerService& tuner,
if (store.hasWifiCredentials()) {
auto staResult = startStaMode(store, tuner, audio, bluetooth, stations,
integration, ota, webRadio, phoneStream,
companionChips, deviceIdentity);
antennaCalibration, companionChips,
deviceIdentity);
if (staResult) {
return staResult;
}
@@ -253,8 +259,8 @@ NetBootstrap::start(core::ISecureStore& store, tuner::TunerService& tuner,
}
return startSetupMode(store, tuner, audio, bluetooth, stations, integration,
ota, webRadio, phoneStream, companionChips,
deviceIdentity);
ota, webRadio, phoneStream, antennaCalibration,
companionChips, deviceIdentity);
}
NetBootstrap::NetBootstrap(std::optional<SoftApHost> softAp,
+82 -4
View File
@@ -68,7 +68,7 @@ namespace net {
namespace {
constexpr char kTag[] = "SetupWebServer";
constexpr char kFirmwareVersion[] = "0.8.5";
constexpr char kFirmwareVersion[] = "0.9.0";
constexpr unsigned kRebootDelaySec = 3;
extern const uint8_t index_html_gz_start[] asm(
@@ -99,6 +99,7 @@ extern const uint8_t index_html_gz_end[] asm(
.ota = nullptr,
.webRadio = nullptr,
.phoneStream = nullptr,
.antennaCalibration = nullptr,
.companionChips = {},
.deviceIdentity = core::DeviceIdentity::unknown(),
};
@@ -429,10 +430,13 @@ esp_err_t tunerTunePostHandler(httpd_req_t* req)
std::expected<void, core::TunerError> result = std::unexpected(
core::TunerError::InvalidInput);
// Omitting antcap uses the board's saved calibration for that band (or
// hardware auto-tune if never calibrated) — only an explicit value in
// the request overrides it, e.g. for a calibration sweep.
if (parsed->band == core::TunerBand::Dab) {
result = ctx->tuner->tuneDab(parsed->dabFreqIndex);
result = ctx->tuner->tuneDab(parsed->dabFreqIndex, parsed->antCap);
} else if (parsed->fmFrequency) {
result = ctx->tuner->tuneFm(*parsed->fmFrequency);
result = ctx->tuner->tuneFm(*parsed->fmFrequency, parsed->antCap);
}
if (!result) {
@@ -635,6 +639,66 @@ esp_err_t tunerFullScanPostHandler(httpd_req_t* req)
return httpd_resp_send(req, json.c_str(), json.size());
}
/**
* @brief tunerCalibrateAntennaPostHandler — save the FM ANTCAP found by
* a calibration sweep as the board's permanent default.
*
* @dname tunerCalibrateAntennaPostHandler
* @param req HTTP request handle from esp_http_server.
* @return ESP_OK on success, or an esp_err_t error code.
* @pubstate writes the 24AA025E48 via route context antenna calibration
* bridge, then updates the live tuner default immediately.
*
* @author Michele Bigi
* @date 2026-08-19
*/
esp_err_t tunerCalibrateAntennaPostHandler(httpd_req_t* req)
{
auto* ctx = routeContextFrom(req);
if (ctx == nullptr || ctx->tuner == nullptr
|| ctx->antennaCalibration == nullptr) {
httpd_resp_set_status(req, "503 Service Unavailable");
return httpd_resp_send(req, nullptr, 0);
}
std::array<char, 128> body{};
if (!readRequestBody(req, body)) {
httpd_resp_set_status(req, "400 Bad Request");
return httpd_resp_send(req, nullptr, 0);
}
const auto parsed =
core::parseAntennaCalibrationJson(std::string_view(body.data()));
if (!parsed) {
const std::string json = core::serializeTunerErrorJson("invalid_json");
httpd_resp_set_status(req, "400 Bad Request");
httpd_resp_set_type(req, "application/json");
return httpd_resp_send(req, json.c_str(), json.size());
}
const bool isDab = parsed->band == core::TunerBand::Dab;
const bool saved = isDab ? ctx->antennaCalibration->saveDab(parsed->antCap)
: ctx->antennaCalibration->save(parsed->antCap);
if (!saved) {
const std::string json = core::serializeTunerErrorJson("store_failed");
httpd_resp_set_status(req, "500 Internal Server Error");
httpd_resp_set_type(req, "application/json");
return httpd_resp_send(req, json.c_str(), json.size());
}
if (isDab) {
ctx->tuner->setDefaultDabAntCap(parsed->antCap);
} else {
ctx->tuner->setDefaultFmAntCap(parsed->antCap);
}
const std::string json =
std::string("{\"status\":\"saved\",\"band\":\"")
+ (isDab ? "dab" : "fm") + "\",\"antcap\":"
+ std::to_string(parsed->antCap) + "}";
httpd_resp_set_type(req, "application/json");
return httpd_resp_send(req, json.c_str(), json.size());
}
/**
* @brief audioProfileGetHandler — serve GET /api/audio/profile JSON.
*
@@ -1865,6 +1929,7 @@ std::expected<void, NetError> SetupWebServer::start(
ota::OtaService& ota,
webradio::WebRadioService& webRadio,
PhoneStreamSink& phoneStream,
AntennaCalibration& antennaCalibration,
core::CompanionChipStatus companionChips,
const core::DeviceIdentity& deviceIdentity)
{
@@ -1889,13 +1954,18 @@ std::expected<void, NetError> SetupWebServer::start(
routeContext.ota = &ota;
routeContext.webRadio = &webRadio;
routeContext.phoneStream = &phoneStream;
routeContext.antennaCalibration = &antennaCalibration;
routeContext.companionChips = companionChips;
routeContext.deviceIdentity = deviceIdentity;
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;
@@ -1997,6 +2067,14 @@ std::expected<void, NetError> SetupWebServer::start(
};
httpd_register_uri_handler(server_, &tunerFullScanUri);
const httpd_uri_t tunerCalibrateAntennaUri = {
.uri = "/api/tuner/calibrate-antenna",
.method = HTTP_POST,
.handler = tunerCalibrateAntennaPostHandler,
.user_ctx = routeCtx,
};
httpd_register_uri_handler(server_, &tunerCalibrateAntennaUri);
const httpd_uri_t audioProfileGetUri = {
.uri = "/api/audio/profile",
.method = HTTP_GET,