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:
@@ -86,6 +86,10 @@ public:
|
||||
*
|
||||
* @dname tuneDab
|
||||
* @param freqIndex Ensemble index 0–37.
|
||||
* @param antCap Front-end antenna varactor override (0-128).
|
||||
* 0 = automatic; other values force a specific
|
||||
* varactor setting, for antenna calibration
|
||||
* sweeps. See tuneFm's antCap doc for details.
|
||||
* @return Ok on success, or WrongBand / TuneFailed / NotBooted.
|
||||
* @pubstate writes last tune target in the adapter.
|
||||
*
|
||||
@@ -93,13 +97,21 @@ public:
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] virtual std::expected<void, TunerError> tuneDab(
|
||||
std::uint8_t freqIndex) = 0;
|
||||
std::uint8_t freqIndex, std::uint8_t antCap = 0U) = 0;
|
||||
|
||||
/**
|
||||
* @brief tuneFm — tune to an FM centre frequency.
|
||||
*
|
||||
* @dname tuneFm
|
||||
* @param frequency Validated FM centre frequency.
|
||||
* @param antCap Front-end antenna varactor override (0-128).
|
||||
* 0 = automatic (chip's own FE_VARM/VARB-derived
|
||||
* tuning); other values force a specific varactor
|
||||
* setting, for antenna calibration sweeps. Chip-
|
||||
* specific concept (AN851 Appendix A on the
|
||||
* Si4684), exposed here only because ANTCAP has no
|
||||
* other reasonable home without duplicating the
|
||||
* whole tune path per driver.
|
||||
* @return Ok on success, or WrongBand / TuneFailed / NotBooted.
|
||||
* @pubstate writes last tune target in the adapter.
|
||||
*
|
||||
@@ -107,7 +119,7 @@ public:
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] virtual std::expected<void, TunerError> tuneFm(
|
||||
FrequencyKHz frequency) = 0;
|
||||
FrequencyKHz frequency, std::uint8_t antCap = 0U) = 0;
|
||||
|
||||
/**
|
||||
* @brief seekFm — seek to the next valid FM station.
|
||||
|
||||
@@ -41,6 +41,9 @@ struct TunerTuneRequest {
|
||||
TunerBand band; ///< Target band (Dab or Fm).
|
||||
std::uint8_t dabFreqIndex; ///< Band III ensemble index (0–37) when band is Dab.
|
||||
std::optional<FrequencyKHz> fmFrequency; ///< FM centre frequency when band is Fm.
|
||||
std::optional<std::uint8_t> antCap; ///< Antenna varactor override (0–128),
|
||||
///< for calibration sweeps; applies to
|
||||
///< whichever band is being tuned.
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -239,4 +242,38 @@ struct TunerFmScannedStation {
|
||||
[[nodiscard]] std::string serializeTunerFmBandScanJson(
|
||||
const std::vector<TunerFmScannedStation>& stations);
|
||||
|
||||
/**
|
||||
* @brief AntennaCalibrationRequest — parsed POST
|
||||
* /api/tuner/calibrate-antenna body.
|
||||
*
|
||||
* @dname AntennaCalibrationRequest
|
||||
* @return n/a (type)
|
||||
* @pubstate Plain DTO filled by parseAntennaCalibrationJson at the HTTP
|
||||
* boundary.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-08-20
|
||||
*/
|
||||
struct AntennaCalibrationRequest {
|
||||
TunerBand band; ///< Which band's ANTCAP default this saves.
|
||||
std::uint8_t antCap; ///< Value to persist (0-128).
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief parseAntennaCalibrationJson — validate POST
|
||||
* /api/tuner/calibrate-antenna body.
|
||||
*
|
||||
* @dname parseAntennaCalibrationJson
|
||||
* @param json Untrusted request body from the HTTP handler.
|
||||
* @return Band + ANTCAP value (0-128) on success, or a ParseError.
|
||||
* `band` defaults to Fm when the field is omitted, preserving
|
||||
* the original FM-only request shape.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-08-19
|
||||
*/
|
||||
[[nodiscard]] std::expected<AntennaCalibrationRequest, ParseError>
|
||||
parseAntennaCalibrationJson(std::string_view json);
|
||||
|
||||
} // namespace core
|
||||
|
||||
@@ -203,6 +203,10 @@ std::expected<TunerTuneRequest, ParseError> parseTunerTuneJson(
|
||||
} else {
|
||||
return std::unexpected(ParseError::InvalidJson);
|
||||
}
|
||||
unsigned long antCap = 0U;
|
||||
if (extractJsonUint(json, "antcap", antCap) && antCap <= 128U) {
|
||||
req.antCap = static_cast<std::uint8_t>(antCap);
|
||||
}
|
||||
return req;
|
||||
}
|
||||
|
||||
@@ -336,4 +340,28 @@ std::string serializeTunerFmBandScanJson(
|
||||
return out.str();
|
||||
}
|
||||
|
||||
std::expected<AntennaCalibrationRequest, ParseError>
|
||||
parseAntennaCalibrationJson(std::string_view json)
|
||||
{
|
||||
if (json.find('{') == std::string_view::npos) {
|
||||
return std::unexpected(ParseError::InvalidJson);
|
||||
}
|
||||
unsigned long antCap = 0U;
|
||||
if (!extractJsonUint(json, "antcap", antCap) || antCap > 128U) {
|
||||
return std::unexpected(ParseError::MissingField);
|
||||
}
|
||||
|
||||
AntennaCalibrationRequest req = {};
|
||||
req.antCap = static_cast<std::uint8_t>(antCap);
|
||||
const std::string_view band = extractJsonString(json, "band");
|
||||
if (band == "dab") {
|
||||
req.band = TunerBand::Dab;
|
||||
} else if (band.empty() || band == "fm") {
|
||||
req.band = TunerBand::Fm;
|
||||
} else {
|
||||
return std::unexpected(ParseError::InvalidJson);
|
||||
}
|
||||
return req;
|
||||
}
|
||||
|
||||
} // namespace core
|
||||
|
||||
@@ -94,6 +94,10 @@ add_executable(dsp_param_json_test dsp_param_json_test.cpp)
|
||||
target_link_libraries(dsp_param_json_test PRIVATE digiradio_core)
|
||||
add_test(NAME dsp_param_json_test COMMAND dsp_param_json_test)
|
||||
|
||||
add_executable(bluetooth_json_test bluetooth_json_test.cpp)
|
||||
target_link_libraries(bluetooth_json_test PRIVATE digiradio_core)
|
||||
add_test(NAME bluetooth_json_test COMMAND bluetooth_json_test)
|
||||
|
||||
add_executable(frequency_khz_test frequency_khz_test.cpp)
|
||||
target_link_libraries(frequency_khz_test PRIVATE digiradio_core)
|
||||
add_test(NAME frequency_khz_test COMMAND frequency_khz_test)
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* @file bluetooth_json_test.cpp
|
||||
* @brief Host tests for Bluetooth JSON parse/serialise.
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-08-19
|
||||
*/
|
||||
|
||||
#include "core/BluetoothJson.hpp"
|
||||
#include "core/ParseError.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] bool expectEqual(const std::string& actual,
|
||||
const std::string& expected)
|
||||
{
|
||||
if (actual == expected) {
|
||||
return true;
|
||||
}
|
||||
std::cerr << "expected: " << expected << "\nactual: " << actual << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] int runStatusSerialiseTest()
|
||||
{
|
||||
const core::BluetoothStatus status{
|
||||
.booted = true,
|
||||
.pairing = false,
|
||||
.a2dpState = core::Bt1035A2dpState::Streaming,
|
||||
.deviceName = "DigiRadio-CC4DB4",
|
||||
.autoReconnect = 3U,
|
||||
};
|
||||
const std::string json = core::serializeBluetoothStatusJson(status);
|
||||
if (!expectEqual(json,
|
||||
R"({"booted":true,"pairing":false,"a2dp":"streaming",)"
|
||||
R"("device_name":"DigiRadio-CC4DB4","auto_reconnect":3})")) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]] int runScanSerialiseTest()
|
||||
{
|
||||
const std::vector<core::Bt1035ScannedDevice> devices{
|
||||
core::Bt1035ScannedDevice{
|
||||
.index = 1U,
|
||||
.addressType = 2U,
|
||||
.mac = "001122334455",
|
||||
.rssiDbm = -58,
|
||||
.name = "Bose SoundLink",
|
||||
.deviceClass = "240404",
|
||||
},
|
||||
};
|
||||
const std::string json = core::serializeBluetoothScanJson(devices);
|
||||
if (!expectEqual(json,
|
||||
R"({"devices":[{"index":1,"mac":"001122334455",)"
|
||||
R"("name":"Bose SoundLink","rssi_dbm":-58}]})")) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]] int runPairedSerialiseTest()
|
||||
{
|
||||
const std::vector<core::Bt1035PairedDevice> devices{
|
||||
core::Bt1035PairedDevice{.index = 1U, .mac = "AABBCCDDEEFF", .name = "Phone"},
|
||||
};
|
||||
const std::string json = core::serializeBluetoothPairedJson(devices);
|
||||
if (!expectEqual(json,
|
||||
R"({"devices":[{"index":1,"mac":"AABBCCDDEEFF","name":"Phone"}]})")) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]] int runAutoReconnectParseTest()
|
||||
{
|
||||
const auto ok = core::parseBluetoothAutoReconnectJson(R"({"times":5})");
|
||||
if (!ok || *ok != 5U) {
|
||||
std::cerr << "auto-reconnect valid parse failed\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
const auto tooHigh = core::parseBluetoothAutoReconnectJson(R"({"times":16})");
|
||||
if (tooHigh) {
|
||||
std::cerr << "auto-reconnect out-of-range accepted\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
const auto missing = core::parseBluetoothAutoReconnectJson(R"({})");
|
||||
if (missing || missing.error() != core::ParseError::MissingField) {
|
||||
std::cerr << "auto-reconnect missing field mis-reported\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]] int runConnectJsonParseTest()
|
||||
{
|
||||
const auto ok =
|
||||
core::parseBluetoothConnectJson(R"({"mac":"001122334455"})");
|
||||
if (!ok || *ok != "001122334455") {
|
||||
std::cerr << "connect mac parse failed\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
// Normalises to uppercase.
|
||||
const auto lower =
|
||||
core::parseBluetoothConnectJson(R"({"mac":"aabbccddeeff"})");
|
||||
if (!lower || *lower != "AABBCCDDEEFF") {
|
||||
std::cerr << "connect mac uppercasing failed\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
const auto invalid = core::parseBluetoothConnectJson(R"({"mac":"not-a-mac"})");
|
||||
if (invalid) {
|
||||
std::cerr << "connect invalid mac accepted\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]] int runConnectRequestParseTest()
|
||||
{
|
||||
const auto request = core::parseBluetoothConnectRequest(
|
||||
R"({"mac":"001122334455","name":"Bose SoundLink","save":true})");
|
||||
if (request.mac != "001122334455" || request.name != "Bose SoundLink"
|
||||
|| !request.save) {
|
||||
std::cerr << "connect request full parse failed\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
const auto minimal =
|
||||
core::parseBluetoothConnectRequest(R"({"mac":"001122334455"})");
|
||||
if (minimal.mac != "001122334455" || !minimal.name.empty()
|
||||
|| minimal.save) {
|
||||
std::cerr << "connect request minimal parse failed\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
const auto badMac = core::parseBluetoothConnectRequest(R"({})");
|
||||
if (!badMac.mac.empty()) {
|
||||
std::cerr << "connect request missing mac should stay empty\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]] int runSpeakerRoundTripTest()
|
||||
{
|
||||
const core::BtSpeakerTarget target{.mac = "001122334455",
|
||||
.name = "Bose SoundLink"};
|
||||
const std::string json = core::serializeBluetoothSpeakerJson(&target);
|
||||
if (!expectEqual(json,
|
||||
R"({"configured":true,"mac":"001122334455",)"
|
||||
R"("name":"Bose SoundLink"})")) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
const std::string unset = core::serializeBluetoothSpeakerJson(nullptr);
|
||||
if (!expectEqual(unset, R"({"configured":false})")) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
const auto parsed = core::parseBluetoothSpeakerJson(
|
||||
R"({"mac":"001122334455","name":"Bose SoundLink"})");
|
||||
if (!parsed || parsed->mac != "001122334455"
|
||||
|| parsed->name != "Bose SoundLink") {
|
||||
std::cerr << "speaker parse round-trip failed\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
const auto invalid = core::parseBluetoothSpeakerJson(R"({"mac":"bad"})");
|
||||
if (invalid) {
|
||||
std::cerr << "speaker parse invalid mac accepted\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]] int runErrorSerialiseTest()
|
||||
{
|
||||
const std::string json = core::serializeBluetoothErrorJson("scan_failed");
|
||||
if (!expectEqual(json, R"({"status":"error","reason":"scan_failed"})")) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
if (runStatusSerialiseTest() != EXIT_SUCCESS) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (runScanSerialiseTest() != EXIT_SUCCESS) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (runPairedSerialiseTest() != EXIT_SUCCESS) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (runAutoReconnectParseTest() != EXIT_SUCCESS) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (runConnectJsonParseTest() != EXIT_SUCCESS) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (runConnectRequestParseTest() != EXIT_SUCCESS) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (runSpeakerRoundTripTest() != EXIT_SUCCESS) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (runErrorSerialiseTest() != EXIT_SUCCESS) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
@@ -111,13 +111,13 @@ public:
|
||||
}
|
||||
|
||||
[[nodiscard]] std::expected<void, core::TunerError> tuneDab(
|
||||
std::uint8_t) override
|
||||
std::uint8_t, std::uint8_t) override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] std::expected<void, core::TunerError> tuneFm(
|
||||
core::FrequencyKHz) override
|
||||
core::FrequencyKHz, std::uint8_t) override
|
||||
{
|
||||
tunedFm = true;
|
||||
return {};
|
||||
|
||||
@@ -53,13 +53,13 @@ public:
|
||||
}
|
||||
|
||||
[[nodiscard]] std::expected<void, core::TunerError> tuneDab(
|
||||
std::uint8_t) override
|
||||
std::uint8_t, std::uint8_t) override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] std::expected<void, core::TunerError> tuneFm(
|
||||
core::FrequencyKHz) override
|
||||
core::FrequencyKHz, std::uint8_t) override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user