Add Slice 4 tuner stack with AGENTS-compliant core types and HTTP API.
Introduces Si4684/ADAU1701 drivers, TunerService, FrequencyKHz, SeekDirection, /api/tuner routes without file-scope globals, host tests, and firmware blob tooling (binaries remain gitignored). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -19,6 +19,9 @@ add_library(digiradio_core STATIC
|
||||
"${CORE_SRC_DIR}/WifiSsid.cpp"
|
||||
"${CORE_SRC_DIR}/WifiCredentials.cpp"
|
||||
"${CORE_SRC_DIR}/WifiProvisionJson.cpp"
|
||||
"${CORE_SRC_DIR}/EmbeddedBlobReader.cpp"
|
||||
"${CORE_SRC_DIR}/TunerJson.cpp"
|
||||
"${CORE_SRC_DIR}/FrequencyKHz.cpp"
|
||||
)
|
||||
target_include_directories(digiradio_core PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../include"
|
||||
@@ -31,3 +34,15 @@ add_test(NAME health_status_test COMMAND health_status_test)
|
||||
add_executable(wifi_provision_test wifi_provision_test.cpp)
|
||||
target_link_libraries(wifi_provision_test PRIVATE digiradio_core)
|
||||
add_test(NAME wifi_provision_test COMMAND wifi_provision_test)
|
||||
|
||||
add_executable(embedded_blob_reader_test embedded_blob_reader_test.cpp)
|
||||
target_link_libraries(embedded_blob_reader_test PRIVATE digiradio_core)
|
||||
add_test(NAME embedded_blob_reader_test COMMAND embedded_blob_reader_test)
|
||||
|
||||
add_executable(tuner_json_test tuner_json_test.cpp)
|
||||
target_link_libraries(tuner_json_test PRIVATE digiradio_core)
|
||||
add_test(NAME tuner_json_test COMMAND tuner_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,79 @@
|
||||
/**
|
||||
* @file embedded_blob_reader_test.cpp
|
||||
* @brief Host tests for EmbeddedBlobReader streaming reads.
|
||||
*
|
||||
* 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/EmbeddedBlobReader.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
|
||||
namespace {
|
||||
|
||||
bool expectEqual(std::size_t actual, std::size_t expected, const char* label)
|
||||
{
|
||||
if (actual == expected) {
|
||||
return true;
|
||||
}
|
||||
std::cerr << label << ": expected " << expected << ", got " << actual
|
||||
<< '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
* @brief main — run EmbeddedBlobReader host tests.
|
||||
*
|
||||
* @dname main
|
||||
* @return 0 on success, 1 on failure.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
int main()
|
||||
{
|
||||
const std::array<std::byte, 8> source = {
|
||||
std::byte{0x01}, std::byte{0x02}, std::byte{0x03}, std::byte{0x04},
|
||||
std::byte{0x05}, std::byte{0x06}, std::byte{0x07}, std::byte{0x08},
|
||||
};
|
||||
core::EmbeddedBlobReader reader(source.data(), source.size());
|
||||
|
||||
if (!expectEqual(reader.size(), source.size(), "size")) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
std::array<std::byte, 3> chunk = {};
|
||||
if (!expectEqual(reader.read(0U, chunk), 3U, "first read length")) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (chunk[0] != std::byte{0x01} || chunk[2] != std::byte{0x03}) {
|
||||
std::cerr << "first read content mismatch\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
if (!expectEqual(reader.read(6U, chunk), 2U, "tail read length")) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (chunk[0] != std::byte{0x07} || chunk[1] != std::byte{0x08}) {
|
||||
std::cerr << "tail read content mismatch\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
if (!expectEqual(reader.read(source.size(), chunk), 0U, "past end")) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
std::cout << "embedded_blob_reader_test: ok\n";
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* @file frequency_khz_test.cpp
|
||||
* @brief Host tests for FrequencyKHz validation.
|
||||
*
|
||||
* 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/ParseError.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] int runFrequencyAcceptTest()
|
||||
{
|
||||
const auto parsed = core::FrequencyKHz::tryFromKhz(101500U);
|
||||
if (!parsed || parsed->value() != 101500U) {
|
||||
std::cerr << "expected 101500 kHz accepted\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]] int runFrequencyRejectTest()
|
||||
{
|
||||
const auto parsed = core::FrequencyKHz::tryFromKhz(50000U);
|
||||
if (parsed || parsed.error() != core::ParseError::MissingField) {
|
||||
std::cerr << "expected out-of-band frequency rejection\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
if (runFrequencyAcceptTest() != EXIT_SUCCESS) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (runFrequencyRejectTest() != EXIT_SUCCESS) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* @file tuner_json_test.cpp
|
||||
* @brief Host tests for tuner 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-07-06
|
||||
*/
|
||||
|
||||
#include "core/FrequencyKHz.hpp"
|
||||
#include "core/ParseError.hpp"
|
||||
#include "core/TunerBand.hpp"
|
||||
#include "core/TunerJson.hpp"
|
||||
#include "core/TunerStatus.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 runTunerTuneParseDabTest()
|
||||
{
|
||||
const auto parsed =
|
||||
core::parseTunerTuneJson(R"({"band":"dab","freq_index":12})");
|
||||
if (!parsed || parsed->band != core::TunerBand::Dab
|
||||
|| parsed->dabFreqIndex != 12U) {
|
||||
std::cerr << "DAB tune parse failed\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]] int runTunerTuneParseFmTest()
|
||||
{
|
||||
const auto parsed =
|
||||
core::parseTunerTuneJson(R"({"band":"fm","frequency_khz":101500})");
|
||||
if (!parsed || parsed->band != core::TunerBand::Fm
|
||||
|| !parsed->fmFrequency
|
||||
|| parsed->fmFrequency->value() != 101500U) {
|
||||
std::cerr << "FM tune parse failed\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]] int runTunerTuneRejectTest()
|
||||
{
|
||||
const auto parsed =
|
||||
core::parseTunerTuneJson(R"({"band":"fm","frequency_khz":50000})");
|
||||
if (parsed) {
|
||||
std::cerr << "expected FM frequency rejection\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]] int runTunerPlayParseTest()
|
||||
{
|
||||
const auto parsed =
|
||||
core::parseTunerPlayJson(R"({"service_id":42,"component_id":7})");
|
||||
if (!parsed || parsed->serviceId != 42U || parsed->componentId != 7U) {
|
||||
std::cerr << "play parse failed\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]] int runTunerStatusSerialiseTest()
|
||||
{
|
||||
core::TunerStatus status = {};
|
||||
status.booted = true;
|
||||
status.band = core::TunerBand::Fm;
|
||||
status.locked = true;
|
||||
status.volume = 40;
|
||||
status.fmFrequency = *core::FrequencyKHz::tryFromKhz(101500U);
|
||||
status.fmRssiDbuV = -20;
|
||||
status.fmSnrDb = 25;
|
||||
status.fmStereo = true;
|
||||
|
||||
const std::string json = core::serializeTunerStatusJson(status);
|
||||
if (json.find("\"booted\":true") == std::string::npos
|
||||
|| json.find("\"band\":\"fm\"") == std::string::npos
|
||||
|| json.find("\"frequency_khz\":101500") == std::string::npos) {
|
||||
std::cerr << "status serialise missing fields: " << json << '\n';
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]] int runTunerServicesSerialiseTest()
|
||||
{
|
||||
core::TunerServiceEntry entry = {};
|
||||
entry.serviceId = 1U;
|
||||
entry.componentId = 2U;
|
||||
entry.label = {'R', 'A', 'I', '\0'};
|
||||
|
||||
const std::string json =
|
||||
core::serializeTunerServicesJson({entry});
|
||||
if (!expectEqual(json,
|
||||
R"({"services":[{"service_id":1,"component_id":2,"label":"RAI"}]})")) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]] int runTunerErrorSerialiseTest()
|
||||
{
|
||||
if (!expectEqual(core::serializeTunerErrorJson("not_booted"),
|
||||
R"({"status":"error","reason":"not_booted"})")) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
if (runTunerTuneParseDabTest() != EXIT_SUCCESS) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (runTunerTuneParseFmTest() != EXIT_SUCCESS) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (runTunerTuneRejectTest() != EXIT_SUCCESS) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (runTunerPlayParseTest() != EXIT_SUCCESS) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (runTunerStatusSerialiseTest() != EXIT_SUCCESS) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (runTunerServicesSerialiseTest() != EXIT_SUCCESS) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (runTunerErrorSerialiseTest() != EXIT_SUCCESS) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
Reference in New Issue
Block a user