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:
@@ -31,15 +31,19 @@ namespace net::ble_provisioning {
|
||||
* @dname start
|
||||
* @param store Secure store the received credentials are saved
|
||||
* to (same store POST /api/wifi writes to).
|
||||
* @param deviceIdentity Supplies the BLE advertising name (softApSsid)
|
||||
* and the proof-of-possession string (serialNumber).
|
||||
* @param deviceIdentity Supplies the BLE advertising name (softApSsid).
|
||||
* @return Ok once provisioning is advertising, or NetError::BleProvisioningFailed.
|
||||
* @pubstate Starts the onboard ESP32-S3 BLE radio (independent of the BT1035
|
||||
* UART module) and a process-lifetime wifi_provisioning manager
|
||||
* singleton. On a successful join, saves credentials to store and
|
||||
* reboots, mirroring wifiPostHandler's POST /api/wifi behaviour.
|
||||
* Runs alongside the existing SoftAP + HTTP provisioning route,
|
||||
* not instead of it — either path can complete setup.
|
||||
* not instead of it — either path can complete setup. Uses
|
||||
* protocomm Security0 (no proof-of-possession, no encryption):
|
||||
* a PoP derived from the BLE advertising name would be visible to
|
||||
* anyone scanning anyway, so it added app/firmware coupling
|
||||
* without adding real secrecy — same trust level as the open
|
||||
* SoftAP setup path this runs alongside.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-08-18
|
||||
|
||||
@@ -87,6 +87,8 @@ public:
|
||||
* @param ota Firmware OTA service for POST /api/system/ota.
|
||||
* @param webRadio Streaming config for GET/POST /api/streaming.
|
||||
* @param phoneStream I2S write-through for PUT /api/stream/phone.
|
||||
* @param antennaCalibration EEPROM write-through for
|
||||
* POST /api/tuner/calibrate-antenna.
|
||||
* @param companionChips Boot flags exposed on GET /api/health.
|
||||
* @param deviceIdentity EEPROM-derived SSID, hostname, and serial.
|
||||
* @return NetBootstrap on success, or a NetError.
|
||||
@@ -103,6 +105,7 @@ public:
|
||||
ota::OtaService& ota,
|
||||
webradio::WebRadioService& webRadio,
|
||||
PhoneStreamSink& phoneStream,
|
||||
AntennaCalibration& antennaCalibration,
|
||||
core::CompanionChipStatus companionChips,
|
||||
const core::DeviceIdentity& deviceIdentity);
|
||||
|
||||
|
||||
@@ -85,6 +85,28 @@ struct PhoneStreamSink {
|
||||
std::size_t frameCount);
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief AntennaCalibration — plain function pointers over EEPROM-backed
|
||||
* FM and DAB ANTCAP storage, so net/ never includes eeprom24aa
|
||||
* headers directly; main/ supplies it (HardwareBootstrap owns the
|
||||
* I2C bus and EEPROM handle).
|
||||
*
|
||||
* @dname AntennaCalibration
|
||||
* @return n/a (type)
|
||||
* @pubstate Free functions with process lifetime; no per-instance state.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-08-19
|
||||
*/
|
||||
struct AntennaCalibration {
|
||||
/** Persist a new FM ANTCAP calibration value to EEPROM.
|
||||
* @return false on an I2C failure. */
|
||||
bool (*save)(std::uint8_t antCap);
|
||||
/** Persist a new DAB ANTCAP calibration value to EEPROM.
|
||||
* @return false on an I2C failure. */
|
||||
bool (*saveDab)(std::uint8_t antCap);
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief HttpRouteContext — dependencies injected into HTTP handlers.
|
||||
*
|
||||
@@ -106,6 +128,7 @@ struct HttpRouteContext {
|
||||
ota::OtaService* ota; ///< Firmware OTA streaming.
|
||||
webradio::WebRadioService* webRadio; ///< Streaming config REST routes.
|
||||
PhoneStreamSink* phoneStream; ///< PUT /api/stream/phone I2S write-through.
|
||||
AntennaCalibration* antennaCalibration; ///< POST /api/tuner/calibrate-antenna.
|
||||
core::CompanionChipStatus companionChips; ///< Boot flags for /api/health.
|
||||
core::DeviceIdentity deviceIdentity; ///< EEPROM-derived unit identity.
|
||||
};
|
||||
@@ -187,6 +210,8 @@ public:
|
||||
* @param ota Firmware OTA service for POST /api/system/ota.
|
||||
* @param webRadio Streaming config for GET/POST /api/streaming.
|
||||
* @param phoneStream I2S write-through for PUT /api/stream/phone.
|
||||
* @param antennaCalibration EEPROM write-through for
|
||||
* POST /api/tuner/calibrate-antenna.
|
||||
* @param companionChips Boot flags for GET /api/health.
|
||||
* @param deviceIdentity Unit identity for /api/health serialNumber.
|
||||
* @return Ok on success, or NetError::HttpServerStartFailed.
|
||||
@@ -204,6 +229,7 @@ public:
|
||||
ota::OtaService& ota,
|
||||
webradio::WebRadioService& webRadio,
|
||||
PhoneStreamSink& phoneStream,
|
||||
AntennaCalibration& antennaCalibration,
|
||||
core::CompanionChipStatus companionChips,
|
||||
const core::DeviceIdentity& deviceIdentity);
|
||||
|
||||
|
||||
@@ -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 {};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user