Add internet radio streaming with runtime API, modernize web UI, remove auto-tune/beep at boot
Streaming (main feature this session): - New WebRadioConfig/WebRadioJson core types, ISecureStore-backed persistence - New webradio::WebRadioService (thread-safe live config) + GET/POST /api/streaming - web_radio_stream task now runtime-toggleable (no reboot), no hardcoded URL - Content-Type diagnostic: warns clearly when a URL is a webpage, not an audio stream Boot cleanup: - Removed boot-time auto FM/DAB tune, auto-beep, and the (now-concluded) Si4684 crystal IBIAS/CTUN empirical sweep from main.cpp — tuning/beep are on-demand via the existing REST API only Web UI: - Modernized styling (cards, gradients, toggle switches, light/dark theme) - New Stream tab wired to /api/streaming Fixes found via real idf.py build (not just clangd): - Restored wrongly-removed si4684/Si4684Tuner.hpp include in main.cpp - Fixed MP3Decode() argument types in web_radio_stream.cpp (unsigned char**/int*) Quality-gate fixes: - Host-test stub headers (esp_log.h, freertos/*) so TunerService.cpp's scanForStation logging/pacing compiles for station_service_test / integration_service_test instead of running stale binaries - Added WifiScanner and WebRadioService manual sections; filled in missing Doxygen docs on BluetoothService, i2s_sdata_probe, test_firmware, Bt1035At - Ignore clangd's .cache/ index directory Also includes prior uncommitted work carried in the tree: Wi-Fi/Bluetooth device scan REST API and UI (WifiScanner, BT scan), SigmaStudio TCP bridge, and the current ADAU1701 SigmaStudio DSP program export. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,7 @@ add_library(digiradio_core STATIC
|
||||
"${CORE_SRC_DIR}/WifiSsid.cpp"
|
||||
"${CORE_SRC_DIR}/WifiCredentials.cpp"
|
||||
"${CORE_SRC_DIR}/WifiProvisionJson.cpp"
|
||||
"${CORE_SRC_DIR}/WifiScanJson.cpp"
|
||||
"${CORE_SRC_DIR}/EmbeddedBlobReader.cpp"
|
||||
"${CORE_SRC_DIR}/TunerJson.cpp"
|
||||
"${CORE_SRC_DIR}/FrequencyKHz.cpp"
|
||||
@@ -50,6 +51,7 @@ add_library(digiradio_core STATIC
|
||||
"${CORE_SRC_DIR}/RegisterWrite.cpp"
|
||||
"${CORE_SRC_DIR}/DspProgramBlob.cpp"
|
||||
"${CORE_SRC_DIR}/OtaAppDescriptor.cpp"
|
||||
"${CORE_SRC_DIR}/WebRadioJson.cpp"
|
||||
)
|
||||
target_include_directories(digiradio_core PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../include"
|
||||
@@ -75,6 +77,10 @@ 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(wifi_scan_test wifi_scan_test.cpp)
|
||||
target_link_libraries(wifi_scan_test PRIVATE digiradio_core)
|
||||
add_test(NAME wifi_scan_test COMMAND wifi_scan_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)
|
||||
@@ -101,6 +107,7 @@ add_test(NAME enhancements_design_test COMMAND enhancements_design_test)
|
||||
|
||||
add_executable(station_service_test station_service_test.cpp)
|
||||
target_include_directories(station_service_test PRIVATE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/host_stubs"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../../services/station/include"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../../services/tuner/include"
|
||||
)
|
||||
@@ -123,8 +130,13 @@ add_executable(broadcast_metadata_test broadcast_metadata_test.cpp)
|
||||
target_link_libraries(broadcast_metadata_test PRIVATE digiradio_core)
|
||||
add_test(NAME broadcast_metadata_test COMMAND broadcast_metadata_test)
|
||||
|
||||
add_executable(web_radio_json_test web_radio_json_test.cpp)
|
||||
target_link_libraries(web_radio_json_test PRIVATE digiradio_core)
|
||||
add_test(NAME web_radio_json_test COMMAND web_radio_json_test)
|
||||
|
||||
add_executable(integration_service_test integration_service_test.cpp)
|
||||
target_include_directories(integration_service_test PRIVATE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/host_stubs"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../../services/integration/include"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../../services/station/include"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../../services/tuner/include"
|
||||
|
||||
@@ -72,6 +72,31 @@ namespace {
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]] int runBeepEnabledJsonTest()
|
||||
{
|
||||
const auto onParsed = core::parseBeepEnabledJson(R"({"enabled":true})");
|
||||
if (!onParsed || *onParsed != true) {
|
||||
std::cerr << "beep enabled=true parse mismatch\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
const auto offParsed = core::parseBeepEnabledJson(R"({"enabled":false})");
|
||||
if (!offParsed || *offParsed != false) {
|
||||
std::cerr << "beep enabled=false parse mismatch\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
const auto missing = core::parseBeepEnabledJson(R"({})");
|
||||
if (missing) {
|
||||
std::cerr << "beep missing field should fail\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
const auto malformed = core::parseBeepEnabledJson("not json");
|
||||
if (malformed) {
|
||||
std::cerr << "beep malformed body should fail\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
@@ -79,5 +104,8 @@ int main()
|
||||
if (runRoundTripTest() != EXIT_SUCCESS) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (runBeepEnabledJsonTest() != EXIT_SUCCESS) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#include "core/Bt1035At.hpp"
|
||||
#include "core/Bt1035PairedDevice.hpp"
|
||||
#include "core/Bt1035ScannedDevice.hpp"
|
||||
#include "core/BluetoothJson.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
@@ -32,7 +33,7 @@ namespace {
|
||||
std::cerr << "I2S mode must be in init sequence\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (sequence[2U] != core::Bt1035AtCommand::I2sSlave48k32) {
|
||||
if (sequence[2U] != core::Bt1035AtCommand::I2sSlave48k24) {
|
||||
std::cerr << "I2SCFG must be in init sequence\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
@@ -43,9 +44,15 @@ namespace {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
const std::string i2sCfg =
|
||||
core::buildBt1035AtLine(core::Bt1035AtCommand::I2sSlave48k32);
|
||||
if (i2sCfg != "AT+I2SCFG=67\r\n") {
|
||||
std::cerr << "I2SCFG=67 command line mismatch\n";
|
||||
core::buildBt1035AtLine(core::Bt1035AtCommand::I2sSlave48k24);
|
||||
if (i2sCfg != "AT+I2SCFG=35\r\n") {
|
||||
std::cerr << "I2SCFG=35 command line mismatch\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
const std::string reset =
|
||||
core::buildBt1035AtLine(core::Bt1035AtCommand::Reset);
|
||||
if (reset != "AT+RESET\r\n") {
|
||||
std::cerr << "AT+RESET command line mismatch\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
@@ -136,6 +143,115 @@ namespace {
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]] int runScanConnectTest()
|
||||
{
|
||||
if (core::buildBt1035StartScanLine(1U, 20U) != "AT+SCAN=1,20\r\n") {
|
||||
std::cerr << "SCAN start line mismatch\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (core::buildBt1035StartScanLine(1U, 0U) != "AT+SCAN=1\r\n") {
|
||||
std::cerr << "SCAN default line mismatch\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (core::buildBt1035StartScanLine(2U, 15U) != "AT+SCAN=2,15\r\n") {
|
||||
std::cerr << "BLE SCAN line mismatch\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (core::buildBt1035EnablePrintLine() != "AT+PRINT=1\r\n") {
|
||||
std::cerr << "PRINT line mismatch\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (core::buildBt1035DisableAutoLinkLine() != "AT+LINKCFG=0,0\r\n") {
|
||||
std::cerr << "LINKCFG line mismatch\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (core::buildBt1035ClearPairedListLine() != "AT+PLIST=0\r\n") {
|
||||
std::cerr << "PLIST clear line mismatch\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (core::buildBt1035DisconnectAllLine() != "AT+DSCA\r\n") {
|
||||
std::cerr << "DSCA line mismatch\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (core::buildBt1035StopScanLine() != "AT+SCAN=0\r\n") {
|
||||
std::cerr << "SCAN stop line mismatch\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (!core::isValidBt1035Mac("AABBCCDDEEFF")
|
||||
|| core::isValidBt1035Mac("bad")) {
|
||||
std::cerr << "MAC validation failed\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (core::buildBt1035A2dpConnectLine("aabbccddeeff")
|
||||
!= "AT+A2DPCONN=aabbccddeeff\r\n") {
|
||||
std::cerr << "A2DPCONN line mismatch\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (core::buildBt1035A2dpAudioLine(true) != "AT+A2DPAUDIO=1\r\n") {
|
||||
std::cerr << "A2DPAUDIO=1 line mismatch\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (core::buildBt1035A2dpAudioLine(false) != "AT+A2DPAUDIO=0\r\n") {
|
||||
std::cerr << "A2DPAUDIO=0 line mismatch\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
const auto scanned = core::parseBt1035ScanResponse(
|
||||
"+SCAN=1,0,112233445566,-55,7,MyPhone,240404\r\n"
|
||||
"+SCAN=2,0,FFEEDDCCBBAA,-70,0,,240404\r\n"
|
||||
"+SCAN=E\r\n"
|
||||
"OK\r\n");
|
||||
if (!scanned || scanned->size() != 2U || (*scanned)[0U].mac != "112233445566"
|
||||
|| (*scanned)[0U].name != "MyPhone" || (*scanned)[0U].rssiDbm != -55
|
||||
|| !(*scanned)[1U].name.empty()) {
|
||||
std::cerr << "SCAN parse failed\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
const auto colonMac = core::parseBt1035ScanResponse(
|
||||
"+SCAN=1,2,AA:BB:CC:DD:EE:FF,-60,4,Test,240404\r\n+SCAN=E\r\n");
|
||||
if (!colonMac || colonMac->size() != 1U
|
||||
|| (*colonMac)[0U].mac != "AABBCCDDEEFF") {
|
||||
std::cerr << "SCAN colon MAC parse failed\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
const auto userLogScan = core::parseBt1035ScanResponse(
|
||||
"+DEVSTAT=9\r\n\r\nOK\r\n\r\n"
|
||||
"+SCAN=1,2,6960BB7EC5AA,-71,8,HY300PRO,1A0114\r\n\r\n"
|
||||
"+SCAN=2,2,BC87FAE69D6E,-49,0\r\n\r\n"
|
||||
"+DEVSTAT=1\r\n");
|
||||
if (!userLogScan || userLogScan->size() != 2U
|
||||
|| (*userLogScan)[0U].name != "HY300PRO"
|
||||
|| (*userLogScan)[0U].mac != "6960BB7EC5AA"
|
||||
|| (*userLogScan)[1U].mac != "BC87FAE69D6E"
|
||||
|| !(*userLogScan)[1U].name.empty()
|
||||
|| (*userLogScan)[1U].rssiDbm != -49) {
|
||||
std::cerr << "SCAN user-log payload parse failed\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
const auto bareEmptyName = core::parseBt1035ScanResponse(
|
||||
"+SCAN=2,2,BC87FAE69D6E,-49,0\r\n");
|
||||
if (!bareEmptyName || bareEmptyName->size() != 1U
|
||||
|| (*bareEmptyName)[0U].mac != "BC87FAE69D6E"
|
||||
|| !(*bareEmptyName)[0U].name.empty()
|
||||
|| !(*bareEmptyName)[0U].deviceClass.empty()) {
|
||||
std::cerr << "SCAN bare empty-name line parse failed\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
const std::string scanJson =
|
||||
core::serializeBluetoothScanJson(*scanned);
|
||||
if (scanJson.find("\"mac\":\"112233445566\"") == std::string::npos
|
||||
|| scanJson.find("\"rssi_dbm\":-55") == std::string::npos) {
|
||||
std::cerr << "scan JSON serialise failed: " << scanJson << '\n';
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
const auto mac =
|
||||
core::parseBluetoothConnectJson(R"({"mac":"112233445566"})");
|
||||
if (!mac || *mac != "112233445566") {
|
||||
std::cerr << "connect JSON parse failed\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
@@ -149,5 +265,8 @@ int main()
|
||||
if (runNameAutoConnPairedParseTest() != EXIT_SUCCESS) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (runScanConnectTest() != EXIT_SUCCESS) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @file esp_log.h
|
||||
* @brief Host-test stand-in for ESP-IDF logging macros.
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Not shipped to device firmware: idf.py builds resolve the real ESP-IDF
|
||||
* esp_log.h first. Only used so services/tuner/src/TunerService.cpp — which
|
||||
* logs scan progress on real hardware — can also compile against
|
||||
* components/core/test host tests without pulling in ESP-IDF.
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#define ESP_LOGE(tag, fmt, ...) ((void)0)
|
||||
#define ESP_LOGW(tag, fmt, ...) ((void)0)
|
||||
#define ESP_LOGI(tag, fmt, ...) ((void)0)
|
||||
#define ESP_LOGD(tag, fmt, ...) ((void)0)
|
||||
#define ESP_LOGV(tag, fmt, ...) ((void)0)
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* @file FreeRTOS.h
|
||||
* @brief Host-test stand-in for the FreeRTOS core header.
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Not shipped to device firmware: idf.py builds resolve the real FreeRTOS
|
||||
* headers first. Only used so services/tuner/src/TunerService.cpp — which
|
||||
* paces scan steps with vTaskDelay on real hardware — can also compile
|
||||
* against components/core/test host tests without pulling in FreeRTOS.
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
using TickType_t = unsigned int;
|
||||
|
||||
#define pdMS_TO_TICKS(ms) (static_cast<TickType_t>(ms))
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @file task.h
|
||||
* @brief Host-test stand-in for the FreeRTOS task API.
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Not shipped to device firmware: idf.py builds resolve the real FreeRTOS
|
||||
* headers first. vTaskDelay is a no-op here — host tests assert scan logic
|
||||
* against a FakeTuner, not real hardware settle timing.
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
|
||||
inline void vTaskDelay(TickType_t) {}
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "core/FrequencyKHz.hpp"
|
||||
#include "core/IDsp.hpp"
|
||||
#include "core/ISecureStore.hpp"
|
||||
#include "core/BtSpeakerTarget.hpp"
|
||||
#include "core/StationName.hpp"
|
||||
#include "core/TunerBand.hpp"
|
||||
#include "integration/IntegrationService.hpp"
|
||||
@@ -68,6 +69,12 @@ public:
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] std::expected<void, core::DspError> setBeepEnabled(
|
||||
bool) override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
class TrackingTuner final : public core::ITuner {
|
||||
@@ -217,9 +224,60 @@ public:
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] bool hasBtSpeakerTarget() const override
|
||||
{
|
||||
return btSpeaker_.has_value();
|
||||
}
|
||||
|
||||
[[nodiscard]] std::expected<void, core::StoreError> saveBtSpeakerTarget(
|
||||
const core::BtSpeakerTarget& target) override
|
||||
{
|
||||
btSpeaker_ = target;
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] std::expected<core::BtSpeakerTarget, core::StoreError>
|
||||
loadBtSpeakerTarget() const override
|
||||
{
|
||||
if (!btSpeaker_) {
|
||||
return std::unexpected(core::StoreError::NotFound);
|
||||
}
|
||||
return *btSpeaker_;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::expected<void, core::StoreError> clearBtSpeakerTarget()
|
||||
override
|
||||
{
|
||||
btSpeaker_.reset();
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] bool hasWebRadioConfig() const override
|
||||
{
|
||||
return webRadioJson_.has_value();
|
||||
}
|
||||
|
||||
[[nodiscard]] std::expected<void, core::StoreError>
|
||||
saveWebRadioConfigJson(std::string_view json) override
|
||||
{
|
||||
webRadioJson_ = std::string(json);
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] std::expected<std::string, core::StoreError>
|
||||
loadWebRadioConfigJson() const override
|
||||
{
|
||||
if (!webRadioJson_) {
|
||||
return std::unexpected(core::StoreError::NotFound);
|
||||
}
|
||||
return *webRadioJson_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::optional<std::string> stationJson_;
|
||||
std::optional<std::uint8_t> lastPresetIndex_;
|
||||
std::optional<core::BtSpeakerTarget> btSpeaker_;
|
||||
std::optional<std::string> webRadioJson_;
|
||||
};
|
||||
|
||||
[[nodiscard]] core::Station makeFmStation(const char* name, std::uint32_t khz)
|
||||
|
||||
@@ -171,9 +171,54 @@ public:
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] bool hasBtSpeakerTarget() const override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::expected<void, core::StoreError> saveBtSpeakerTarget(
|
||||
const core::BtSpeakerTarget&) override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] std::expected<core::BtSpeakerTarget, core::StoreError>
|
||||
loadBtSpeakerTarget() const override
|
||||
{
|
||||
return std::unexpected(core::StoreError::NotFound);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::expected<void, core::StoreError> clearBtSpeakerTarget()
|
||||
override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] bool hasWebRadioConfig() const override
|
||||
{
|
||||
return webRadioJson_.has_value();
|
||||
}
|
||||
|
||||
[[nodiscard]] std::expected<void, core::StoreError>
|
||||
saveWebRadioConfigJson(std::string_view json) override
|
||||
{
|
||||
webRadioJson_ = std::string(json);
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] std::expected<std::string, core::StoreError>
|
||||
loadWebRadioConfigJson() const override
|
||||
{
|
||||
if (!webRadioJson_) {
|
||||
return std::unexpected(core::StoreError::NotFound);
|
||||
}
|
||||
return *webRadioJson_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::optional<std::string> stationJson_;
|
||||
std::optional<std::uint8_t> lastPresetIndex_;
|
||||
std::optional<std::string> webRadioJson_;
|
||||
};
|
||||
|
||||
[[nodiscard]] core::Station makeFmStation(const char* name, std::uint32_t khz)
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* @file web_radio_json_test.cpp
|
||||
* @brief Host tests for WebRadioConfig JSON round-trip.
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-08-08
|
||||
*/
|
||||
|
||||
#include "core/WebRadioJson.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] int runRoundTripTest()
|
||||
{
|
||||
const core::WebRadioConfig config{
|
||||
.enabled = true, .url = "http://edge.radiomontecarlo.net/RMC.mp3"};
|
||||
const std::string json = core::serializeWebRadioConfigJson(config);
|
||||
const auto parsed = core::parseWebRadioConfigJson(json);
|
||||
if (!parsed || parsed->enabled != true || parsed->url != config.url) {
|
||||
std::cerr << "round-trip mismatch\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]] int runValidationTest()
|
||||
{
|
||||
const auto missingUrl =
|
||||
core::parseWebRadioConfigJson(R"({"enabled":false})");
|
||||
if (missingUrl) {
|
||||
std::cerr << "missing url should fail\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
const auto badScheme = core::parseWebRadioConfigJson(
|
||||
R"({"enabled":true,"url":"https://example.com/x.mp3"})");
|
||||
if (badScheme) {
|
||||
std::cerr << "non-http scheme should fail\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
const auto malformed = core::parseWebRadioConfigJson("not json");
|
||||
if (malformed) {
|
||||
std::cerr << "malformed body should fail\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
const auto ok = core::parseWebRadioConfigJson(
|
||||
R"({"enabled":false,"url":"http://example.com/x.mp3"})");
|
||||
if (!ok || ok->enabled != false) {
|
||||
std::cerr << "valid disabled config should parse\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
if (runRoundTripTest() != EXIT_SUCCESS) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (runValidationTest() != EXIT_SUCCESS) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* @file wifi_scan_test.cpp
|
||||
* @brief Host tests for Wi-Fi scan JSON serialisation.
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-08-05
|
||||
*/
|
||||
|
||||
#include "core/WifiScanJson.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* @brief expectEqual — assert two strings match.
|
||||
*
|
||||
* @dname expectEqual
|
||||
* @param actual Observed value.
|
||||
* @param expected Expected value.
|
||||
* @return true when equal.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-08-05
|
||||
*/
|
||||
[[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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief runWifiScanSerialiseTest — verify scan list JSON shape.
|
||||
*
|
||||
* @dname runWifiScanSerialiseTest
|
||||
* @return EXIT_SUCCESS or EXIT_FAILURE.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-08-05
|
||||
*/
|
||||
[[nodiscard]] int runWifiScanSerialiseTest()
|
||||
{
|
||||
const std::vector<core::WifiScannedNetwork> networks = {
|
||||
{
|
||||
.ssid = "HomeNet",
|
||||
.rssiDbm = -42,
|
||||
.auth = "wpa2",
|
||||
.channel = 6,
|
||||
},
|
||||
{
|
||||
.ssid = "Guest\"WiFi",
|
||||
.rssiDbm = -71,
|
||||
.auth = "open",
|
||||
.channel = 11,
|
||||
},
|
||||
};
|
||||
|
||||
const std::string json = core::serializeWifiScanJson(networks);
|
||||
const std::string expected =
|
||||
R"({"networks":[{"ssid":"HomeNet","rssi_dbm":-42,"auth":"wpa2","channel":6},{"ssid":"Guest\"WiFi","rssi_dbm":-71,"auth":"open","channel":11}]})";
|
||||
if (!expectEqual(json, expected)) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief runWifiScanErrorSerialiseTest — verify error JSON.
|
||||
*
|
||||
* @dname runWifiScanErrorSerialiseTest
|
||||
* @return EXIT_SUCCESS or EXIT_FAILURE.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-08-05
|
||||
*/
|
||||
[[nodiscard]] int runWifiScanErrorSerialiseTest()
|
||||
{
|
||||
if (!expectEqual(core::serializeWifiScanErrorJson("scan_failed"),
|
||||
R"({"status":"error","reason":"scan_failed"})")) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
* @brief main — host test entry point.
|
||||
*
|
||||
* @dname main
|
||||
* @return EXIT_SUCCESS when all tests pass.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-08-05
|
||||
*/
|
||||
int main()
|
||||
{
|
||||
if (runWifiScanSerialiseTest() != EXIT_SUCCESS) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (runWifiScanErrorSerialiseTest() != EXIT_SUCCESS) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
Reference in New Issue
Block a user