Add BT1035 pairing and station presets (fw 0.7.0).

Expose discoverable mode and A2DP control over REST, add persisted
DAB/FM preset list with web UI, and document gaps in docs/TODO.md.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-06 17:42:11 +02:00
co-authored by Cursor
parent a08a9c19ee
commit 82c9f401da
46 changed files with 2819 additions and 33 deletions
@@ -35,6 +35,12 @@ add_library(digiradio_core STATIC
"${CORE_SRC_DIR}/AudioProfile.cpp"
"${CORE_SRC_DIR}/AudioProfileJson.cpp"
"${CORE_SRC_DIR}/Bt1035At.cpp"
"${CORE_SRC_DIR}/StationName.cpp"
"${CORE_SRC_DIR}/PresetSlot.cpp"
"${CORE_SRC_DIR}/Station.cpp"
"${CORE_SRC_DIR}/StationList.cpp"
"${CORE_SRC_DIR}/StationListJson.cpp"
"${CORE_SRC_DIR}/BluetoothJson.cpp"
)
target_include_directories(digiradio_core PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/../include"
@@ -72,6 +78,10 @@ add_executable(enhancements_design_test enhancements_design_test.cpp)
target_link_libraries(enhancements_design_test PRIVATE digiradio_core)
add_test(NAME enhancements_design_test COMMAND enhancements_design_test)
add_executable(station_list_test station_list_test.cpp)
target_link_libraries(station_list_test PRIVATE digiradio_core)
add_test(NAME station_list_test COMMAND station_list_test)
add_executable(bt1035_at_test bt1035_at_test.cpp)
target_link_libraries(bt1035_at_test PRIVATE digiradio_core)
add_test(NAME bt1035_at_test COMMAND bt1035_at_test)
@@ -54,6 +54,23 @@ namespace {
std::cerr << "unexpected parse failed\n";
return EXIT_FAILURE;
}
if (core::parseBt1035AtResponse("+A2DPSTAT=3\r\nOK\r\n")
!= core::Bt1035AtResponseKind::Ok) {
std::cerr << "expected multiline OK detection\n";
return EXIT_FAILURE;
}
const std::string pairOn =
core::buildBt1035AtLine(core::Bt1035AtCommand::PairDiscoverable);
if (pairOn != "AT+PAIR=1\r\n") {
std::cerr << "expected AT+PAIR=1 line\n";
return EXIT_FAILURE;
}
const auto a2dp =
core::parseBt1035A2dpStatResponse("+A2DPSTAT=4\r\nOK\r\n");
if (!a2dp || *a2dp != core::Bt1035A2dpState::Streaming) {
std::cerr << "expected streaming A2DP state\n";
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
@@ -0,0 +1,98 @@
/**
* @file station_list_test.cpp
* @brief Host tests for station preset list domain and JSON.
*
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
*
* Copyright 2026 Michele Bigi
* SPDX-License-Identifier: Apache-2.0
*
* @author Michele Bigi
* @date 2026-07-06
*/
#include "core/FrequencyKHz.hpp"
#include "core/PresetSlot.hpp"
#include "core/Station.hpp"
#include "core/StationList.hpp"
#include "core/StationListError.hpp"
#include "core/StationListJson.hpp"
#include "core/StationName.hpp"
#include "core/TunerBand.hpp"
#include <cstdlib>
#include <iostream>
namespace {
[[nodiscard]] core::Station makeFmStation(const char* name, std::uint32_t khz,
unsigned slot)
{
auto label = core::StationName::tryFrom(name);
auto frequency = core::FrequencyKHz::tryFromKhz(khz);
auto preset = core::PresetSlot::tryFrom(slot);
return core::Station(*label, core::TunerBand::Fm, 0U, std::nullopt,
std::nullopt, *frequency, *preset);
}
[[nodiscard]] int runDuplicateRejectionTest()
{
core::StationList list;
if (auto added = list.add(makeFmStation("Radio 2", 88500U, 1U)); !added) {
std::cerr << "first add failed\n";
return EXIT_FAILURE;
}
if (list.add(makeFmStation("Radio 2 dup", 88500U, 2U))) {
std::cerr << "expected duplicate tune target rejection\n";
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
[[nodiscard]] int runJsonRoundTripTest()
{
core::StationList list;
if (auto added = list.add(makeFmStation("Jazz FM", 101500U, 3U)); !added) {
std::cerr << "add failed\n";
return EXIT_FAILURE;
}
const std::string json = core::serializeStationListJson(list);
auto parsed = core::parseStationListJson(json);
if (!parsed || parsed->stations().size() != 1U) {
std::cerr << "round-trip parse failed\n";
return EXIT_FAILURE;
}
if (parsed->stations()[0U].name().value() != "Jazz FM") {
std::cerr << "name mismatch after round-trip\n";
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
[[nodiscard]] int runParseStationJsonTest()
{
const auto station = core::parseStationJson(
R"({"name":"BBC","band":"dab","dab_freq_index":12,"dab_service_id":42,"dab_component_id":1})");
if (!station || station->band() != core::TunerBand::Dab
|| station->dabFreqIndex() != 12U) {
std::cerr << "dab station parse failed\n";
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
} // namespace
int main()
{
if (runDuplicateRejectionTest() != EXIT_SUCCESS) {
return EXIT_FAILURE;
}
if (runJsonRoundTripTest() != EXIT_SUCCESS) {
return EXIT_FAILURE;
}
if (runParseStationJsonTest() != EXIT_SUCCESS) {
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}