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:
@@ -7,6 +7,9 @@ idf_component_register(
|
||||
"src/WifiSsid.cpp"
|
||||
"src/WifiCredentials.cpp"
|
||||
"src/WifiProvisionJson.cpp"
|
||||
"src/EmbeddedBlobReader.cpp"
|
||||
"src/TunerJson.cpp"
|
||||
"src/FrequencyKHz.cpp"
|
||||
INCLUDE_DIRS "include"
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* @file EmbeddedBlobReader.hpp
|
||||
* @brief IFirmwareBlobReader over a contiguous embedded flash region.
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "core/IFirmwareBlobReader.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <span>
|
||||
|
||||
namespace core {
|
||||
|
||||
/**
|
||||
* @brief EmbeddedBlobReader — non-owning view of an embedded binary.
|
||||
*
|
||||
* @dname EmbeddedBlobReader
|
||||
* @return n/a (type)
|
||||
* @pubstate Borrows [data_, data_ + size_) for the reader lifetime.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
class EmbeddedBlobReader final : public IFirmwareBlobReader {
|
||||
public:
|
||||
/**
|
||||
* @brief EmbeddedBlobReader — construct over a flash-backed range.
|
||||
*
|
||||
* @dname EmbeddedBlobReader
|
||||
* @param data Start of embedded blob (e.g. linker symbol).
|
||||
* @param size Length in bytes.
|
||||
* @pubstate stores data_ and size_.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
EmbeddedBlobReader(const std::byte* data, std::size_t size);
|
||||
|
||||
[[nodiscard]] std::size_t size() const override;
|
||||
|
||||
[[nodiscard]] std::size_t read(std::size_t offset,
|
||||
std::span<std::byte> dest) const override;
|
||||
|
||||
private:
|
||||
const std::byte* data_;
|
||||
std::size_t size_;
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* @file FrequencyKHz.hpp
|
||||
* @brief Strong type for FM centre frequency in kilohertz.
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "core/ParseError.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
|
||||
namespace core {
|
||||
|
||||
/**
|
||||
* @brief FrequencyKHz — validated FM centre frequency (European band).
|
||||
*
|
||||
* @dname FrequencyKHz
|
||||
* @return n/a (type)
|
||||
* @pubstate Owns khz_ within 64\,000–108\,000 kHz. Immutable after construction.
|
||||
*
|
||||
* Parsed once at the HTTP boundary; trusted downstream by ITuner and drivers.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
class FrequencyKHz {
|
||||
public:
|
||||
/** Minimum valid FM frequency for DigiRadio (64.0 MHz). */
|
||||
static constexpr std::uint32_t kMinKhz = 64000U;
|
||||
/** Maximum valid FM frequency for DigiRadio (108.0 MHz). */
|
||||
static constexpr std::uint32_t kMaxKhz = 108000U;
|
||||
|
||||
/**
|
||||
* @brief tryFromKhz — validate an FM frequency at the boundary.
|
||||
*
|
||||
* @dname tryFromKhz
|
||||
* @param khz Untrusted frequency in kilohertz from JSON input.
|
||||
* @return FrequencyKHz on success, or ParseError::MissingField.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] static std::expected<FrequencyKHz, ParseError> tryFromKhz(
|
||||
std::uint32_t khz) noexcept;
|
||||
|
||||
/**
|
||||
* @brief value — read the stored frequency in kilohertz.
|
||||
*
|
||||
* @dname value
|
||||
* @return Validated centre frequency in kHz.
|
||||
* @pubstate reads khz_.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] std::uint32_t value() const noexcept;
|
||||
|
||||
private:
|
||||
explicit FrequencyKHz(std::uint32_t khz) noexcept;
|
||||
|
||||
std::uint32_t khz_;
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* @file IFirmwareBlobReader.hpp
|
||||
* @brief Read-only streaming access to a firmware blob in flash.
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <span>
|
||||
|
||||
namespace core {
|
||||
|
||||
/**
|
||||
* @brief IFirmwareBlobReader — streaming firmware image access.
|
||||
*
|
||||
* @dname IFirmwareBlobReader
|
||||
* @return n/a (type)
|
||||
* @pubstate Implementations wrap embedded flash or test buffers. Used by
|
||||
* Si4684Driver to HOST_LOAD in bounded chunks (AGENTS.md §7.1).
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
class IFirmwareBlobReader {
|
||||
public:
|
||||
/**
|
||||
* @brief ~IFirmwareBlobReader — virtual destructor.
|
||||
*
|
||||
* @dname ~IFirmwareBlobReader
|
||||
* @pubstate n/a
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
virtual ~IFirmwareBlobReader() = default;
|
||||
|
||||
/**
|
||||
* @brief size — total blob length in bytes.
|
||||
*
|
||||
* @dname size
|
||||
* @return Blob size.
|
||||
* @pubstate reads backing storage.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] virtual std::size_t size() const = 0;
|
||||
|
||||
/**
|
||||
* @brief read — copy bytes from the blob at an offset.
|
||||
*
|
||||
* @dname read
|
||||
* @param offset Byte offset from the start of the blob.
|
||||
* @param dest Destination buffer; truncated at end of blob.
|
||||
* @return Number of bytes copied into dest.
|
||||
* @pubstate reads backing storage.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] virtual std::size_t read(std::size_t offset,
|
||||
std::span<std::byte> dest) const = 0;
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* @file ITuner.hpp
|
||||
* @brief Abstract tuner driver boundary (host-testable).
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "core/FrequencyKHz.hpp"
|
||||
#include "core/SeekDirection.hpp"
|
||||
#include "core/TunerBand.hpp"
|
||||
#include "core/TunerError.hpp"
|
||||
#include "core/TunerStatus.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <vector>
|
||||
|
||||
namespace core {
|
||||
|
||||
/**
|
||||
* @brief ITuner — hardware abstraction for DAB/FM tuning.
|
||||
*
|
||||
* @dname ITuner
|
||||
* @return n/a (type)
|
||||
* @pubstate Implemented by si4684::Si4684Tuner on device; fakes in host tests.
|
||||
* No public data members.
|
||||
*
|
||||
* Services depend on this interface, not on SPI details. All methods return
|
||||
* std::expected with TunerError on failure.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
class ITuner {
|
||||
public:
|
||||
virtual ~ITuner() = default;
|
||||
|
||||
/**
|
||||
* @brief boot — load the requested band application image.
|
||||
*
|
||||
* @dname boot
|
||||
* @param band DAB or FM image to load.
|
||||
* @return Ok on success, or TunerError::HardwareFailed / NotBooted.
|
||||
* @pubstate none (implementation-defined driver state).
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] virtual std::expected<void, TunerError> boot(
|
||||
TunerBand band) = 0;
|
||||
|
||||
/**
|
||||
* @brief currentBand — read the loaded application band.
|
||||
*
|
||||
* @dname currentBand
|
||||
* @return Active TunerBand, or TunerError::NotBooted.
|
||||
* @pubstate reads driver boot state.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] virtual std::expected<TunerBand, TunerError> currentBand()
|
||||
const = 0;
|
||||
|
||||
/**
|
||||
* @brief readStatus — snapshot lock, metrics, and tune target.
|
||||
*
|
||||
* @dname readStatus
|
||||
* @return TunerStatus on success, or a TunerError.
|
||||
* @pubstate reads driver metrics; may refresh RSQ/DIGRAD.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] virtual std::expected<TunerStatus, TunerError> readStatus() = 0;
|
||||
|
||||
/**
|
||||
* @brief tuneDab — select a Band III ensemble by index.
|
||||
*
|
||||
* @dname tuneDab
|
||||
* @param freqIndex Ensemble index 0–37.
|
||||
* @return Ok on success, or WrongBand / TuneFailed / NotBooted.
|
||||
* @pubstate writes last tune target in the adapter.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] virtual std::expected<void, TunerError> tuneDab(
|
||||
std::uint8_t freqIndex) = 0;
|
||||
|
||||
/**
|
||||
* @brief tuneFm — tune to an FM centre frequency.
|
||||
*
|
||||
* @dname tuneFm
|
||||
* @param frequency Validated FM centre frequency.
|
||||
* @return Ok on success, or WrongBand / TuneFailed / NotBooted.
|
||||
* @pubstate writes last tune target in the adapter.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] virtual std::expected<void, TunerError> tuneFm(
|
||||
FrequencyKHz frequency) = 0;
|
||||
|
||||
/**
|
||||
* @brief seekFm — seek to the next valid FM station.
|
||||
*
|
||||
* @dname seekFm
|
||||
* @param direction Up or Down scan direction.
|
||||
* @return New centre frequency, or a TunerError.
|
||||
* @pubstate updates last tune target on success.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] virtual std::expected<FrequencyKHz, TunerError> seekFm(
|
||||
SeekDirection direction) = 0;
|
||||
|
||||
/**
|
||||
* @brief listDabServices — fetch programmes for the current ensemble.
|
||||
*
|
||||
* @dname listDabServices
|
||||
* @return Service entries, ServiceListEmpty, WrongBand, or NotBooted.
|
||||
* @pubstate reads DAB service list from the driver.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] virtual std::expected<std::vector<TunerServiceEntry>,
|
||||
TunerError>
|
||||
listDabServices() = 0;
|
||||
|
||||
/**
|
||||
* @brief playDabService — start DAB audio for a programme.
|
||||
*
|
||||
* @dname playDabService
|
||||
* @param serviceId Selected service identifier.
|
||||
* @param componentId Audio component within the service.
|
||||
* @return Ok on success, or WrongBand / HardwareFailed / NotBooted.
|
||||
* @pubstate starts digital audio output on the tuner I2S port.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] virtual std::expected<void, TunerError> playDabService(
|
||||
std::uint32_t serviceId, std::uint32_t componentId) = 0;
|
||||
|
||||
/**
|
||||
* @brief setVolume — set tuner output attenuation.
|
||||
*
|
||||
* @dname setVolume
|
||||
* @param level Attenuator 0–63.
|
||||
* @return Ok on success, or HardwareFailed / NotBooted.
|
||||
* @pubstate writes volume property on the driver.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] virtual std::expected<void, TunerError> setVolume(
|
||||
std::uint8_t level) = 0;
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @file SeekDirection.hpp
|
||||
* @brief FM seek direction selector (core domain).
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace core {
|
||||
|
||||
/**
|
||||
* @brief SeekDirection — FM seek scan direction.
|
||||
*
|
||||
* @dname SeekDirection
|
||||
* @return n/a (type)
|
||||
* @pubstate n/a
|
||||
*
|
||||
* Replaces bare bool parameters in public tuner APIs (AGENTS.md §2.1).
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
enum class SeekDirection { Up, Down };
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* @file TunerBand.hpp
|
||||
* @brief Strong band selector for tuner operations (core domain).
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace core {
|
||||
|
||||
/**
|
||||
* @brief TunerBand — active RF application (DAB or FM image).
|
||||
*
|
||||
* @dname TunerBand
|
||||
* @return n/a (type)
|
||||
* @pubstate n/a
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
enum class TunerBand { Dab, Fm };
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* @file TunerError.hpp
|
||||
* @brief Typed errors for tuner service and ITuner adapters.
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace core {
|
||||
|
||||
/**
|
||||
* @brief TunerError — failure causes for tuner operations.
|
||||
*
|
||||
* @dname TunerError
|
||||
* @return n/a (type)
|
||||
* @pubstate Stable error set mapped from driver failures at the service boundary.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
enum class TunerError {
|
||||
NotBooted,
|
||||
WrongBand,
|
||||
HardwareFailed,
|
||||
TuneFailed,
|
||||
ServiceListEmpty,
|
||||
InvalidInput,
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* @file TunerJson.hpp
|
||||
* @brief JSON parse/serialise for tuner API (pure core).
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "core/FrequencyKHz.hpp"
|
||||
#include "core/ParseError.hpp"
|
||||
#include "core/TunerBand.hpp"
|
||||
#include "core/TunerStatus.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace core {
|
||||
|
||||
/**
|
||||
* @brief TunerTuneRequest — parsed POST /api/tuner/tune body.
|
||||
*
|
||||
* @dname TunerTuneRequest
|
||||
* @return n/a (type)
|
||||
* @pubstate Plain DTO filled by parseTunerTuneJson at the HTTP boundary.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
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.
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief TunerPlayRequest — parsed POST /api/tuner/play body.
|
||||
*
|
||||
* @dname TunerPlayRequest
|
||||
* @return n/a (type)
|
||||
* @pubstate Plain DTO filled by parseTunerPlayJson at the HTTP boundary.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
struct TunerPlayRequest {
|
||||
std::uint32_t serviceId; ///< DAB service identifier from the ensemble list.
|
||||
std::uint32_t componentId; ///< Audio component within the service.
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief serializeTunerStatusJson — serialise a tuner snapshot for GET status.
|
||||
*
|
||||
* @dname serializeTunerStatusJson
|
||||
* @param status Domain snapshot from TunerService::refreshStatus().
|
||||
* @return JSON object string for the HTTP response body.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] std::string serializeTunerStatusJson(const TunerStatus& status);
|
||||
|
||||
/**
|
||||
* @brief serializeTunerServicesJson — serialise the DAB service list.
|
||||
*
|
||||
* @dname serializeTunerServicesJson
|
||||
* @param services Programme entries for the current ensemble.
|
||||
* @return JSON object string with a services array.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] std::string serializeTunerServicesJson(
|
||||
const std::vector<TunerServiceEntry>& services);
|
||||
|
||||
/**
|
||||
* @brief serializeTunerErrorJson — serialise a tuner API error response.
|
||||
*
|
||||
* @dname serializeTunerErrorJson
|
||||
* @param reason Short machine-readable cause (never a secret).
|
||||
* @return JSON object string with status error and reason fields.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] std::string serializeTunerErrorJson(const char* reason);
|
||||
|
||||
/**
|
||||
* @brief parseTunerTuneJson — validate POST /api/tuner/tune body.
|
||||
*
|
||||
* @dname parseTunerTuneJson
|
||||
* @param json Untrusted request body from the HTTP handler.
|
||||
* @return TunerTuneRequest on success, or a ParseError.
|
||||
* @pubstate none
|
||||
*
|
||||
* Rejects malformed JSON, missing fields, and out-of-range frequencies
|
||||
* before any driver call.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] std::expected<TunerTuneRequest, ParseError> parseTunerTuneJson(
|
||||
std::string_view json);
|
||||
|
||||
/**
|
||||
* @brief parseTunerPlayJson — validate POST /api/tuner/play body.
|
||||
*
|
||||
* @dname parseTunerPlayJson
|
||||
* @param json Untrusted request body from the HTTP handler.
|
||||
* @return TunerPlayRequest on success, or a ParseError.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] std::expected<TunerPlayRequest, ParseError> parseTunerPlayJson(
|
||||
std::string_view json);
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* @file TunerStatus.hpp
|
||||
* @brief Tuner status DTO for API and UI (pure core).
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "core/FrequencyKHz.hpp"
|
||||
#include "core/TunerBand.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
namespace core {
|
||||
|
||||
/**
|
||||
* @brief TunerServiceEntry — one DAB programme in the current ensemble.
|
||||
*
|
||||
* @dname TunerServiceEntry
|
||||
* @return n/a (type)
|
||||
* @pubstate Plain DTO returned by ITuner::listDabServices().
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
struct TunerServiceEntry {
|
||||
std::uint32_t serviceId; ///< DAB service identifier.
|
||||
std::uint32_t componentId; ///< Audio component within the service.
|
||||
std::array<char, 17> label; ///< UTF-8 programme label, NUL-terminated.
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief TunerStatus — snapshot for GET /api/tuner/status.
|
||||
*
|
||||
* @dname TunerStatus
|
||||
* @return n/a (type)
|
||||
* @pubstate Plain DTO built by ITuner::readStatus(); serialised by
|
||||
* serializeTunerStatusJson(). Band-specific fields are optional.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
struct TunerStatus {
|
||||
bool booted; ///< Companion tuner boot completed.
|
||||
TunerBand band; ///< Active loaded application band.
|
||||
bool locked; ///< RF lock / valid signal.
|
||||
std::uint8_t volume; ///< Attenuator level 0–63.
|
||||
std::optional<std::uint8_t> dabFreqIndex; ///< Current Band III ensemble index.
|
||||
std::optional<std::uint8_t> dabFicQuality; ///< FIC quality 0–100 when DAB.
|
||||
std::optional<std::int8_t> dabCnrDb; ///< CNR in dB when DAB.
|
||||
std::optional<FrequencyKHz> fmFrequency; ///< Tuned FM centre frequency.
|
||||
std::optional<std::int8_t> fmRssiDbuV; ///< FM RSSI in dBµV.
|
||||
std::optional<std::int8_t> fmSnrDb; ///< FM SNR in dB.
|
||||
std::optional<bool> fmStereo; ///< FM stereo pilot detected.
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* @file EmbeddedBlobReader.cpp
|
||||
* @brief EmbeddedBlobReader implementation.
|
||||
*
|
||||
* 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 <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
namespace core {
|
||||
|
||||
EmbeddedBlobReader::EmbeddedBlobReader(const std::byte* data, std::size_t size)
|
||||
: data_(data)
|
||||
, size_(size)
|
||||
{
|
||||
}
|
||||
|
||||
std::size_t EmbeddedBlobReader::size() const
|
||||
{
|
||||
return size_;
|
||||
}
|
||||
|
||||
std::size_t EmbeddedBlobReader::read(std::size_t offset,
|
||||
std::span<std::byte> dest) const
|
||||
{
|
||||
if (offset >= size_ || dest.empty()) {
|
||||
return 0;
|
||||
}
|
||||
const std::size_t available = size_ - offset;
|
||||
const std::size_t toCopy = std::min(available, dest.size());
|
||||
std::memcpy(dest.data(), data_ + offset, toCopy);
|
||||
return toCopy;
|
||||
}
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* @file FrequencyKHz.cpp
|
||||
* @brief FrequencyKHz implementation.
|
||||
*
|
||||
* 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"
|
||||
|
||||
namespace core {
|
||||
|
||||
FrequencyKHz::FrequencyKHz(std::uint32_t khz) noexcept
|
||||
: khz_(khz)
|
||||
{
|
||||
}
|
||||
|
||||
std::expected<FrequencyKHz, ParseError> FrequencyKHz::tryFromKhz(
|
||||
std::uint32_t khz) noexcept
|
||||
{
|
||||
if (khz < kMinKhz || khz > kMaxKhz) {
|
||||
return std::unexpected(ParseError::MissingField);
|
||||
}
|
||||
return FrequencyKHz(khz);
|
||||
}
|
||||
|
||||
std::uint32_t FrequencyKHz::value() const noexcept
|
||||
{
|
||||
return khz_;
|
||||
}
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* @file TunerJson.cpp
|
||||
* @brief TunerJson implementation.
|
||||
*
|
||||
* 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/TunerJson.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <sstream>
|
||||
|
||||
namespace core {
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] std::string_view extractJsonString(std::string_view json,
|
||||
std::string_view key)
|
||||
{
|
||||
const std::string needle =
|
||||
std::string("\"") + std::string(key) + "\":\"";
|
||||
const std::size_t start = json.find(needle);
|
||||
if (start == std::string_view::npos) {
|
||||
return {};
|
||||
}
|
||||
const std::size_t valueStart = start + needle.size();
|
||||
const std::size_t valueEnd = json.find('"', valueStart);
|
||||
if (valueEnd == std::string_view::npos) {
|
||||
return {};
|
||||
}
|
||||
return json.substr(valueStart, valueEnd - valueStart);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool extractJsonUint(std::string_view json,
|
||||
std::string_view key,
|
||||
unsigned long& out)
|
||||
{
|
||||
const std::string needle =
|
||||
std::string("\"") + std::string(key) + "\":";
|
||||
const std::size_t start = json.find(needle);
|
||||
if (start == std::string_view::npos) {
|
||||
return false;
|
||||
}
|
||||
const std::size_t valueStart = start + needle.size();
|
||||
char* end = nullptr;
|
||||
out = std::strtoul(json.data() + valueStart, &end, 10);
|
||||
return end != json.data() + valueStart;
|
||||
}
|
||||
|
||||
[[nodiscard]] const char* bandToken(TunerBand band) noexcept
|
||||
{
|
||||
return band == TunerBand::Fm ? "fm" : "dab";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string serializeTunerStatusJson(const TunerStatus& status)
|
||||
{
|
||||
std::ostringstream out;
|
||||
out << "{\"booted\":" << (status.booted ? "true" : "false")
|
||||
<< ",\"band\":\"" << bandToken(status.band) << "\""
|
||||
<< ",\"locked\":" << (status.locked ? "true" : "false")
|
||||
<< ",\"volume\":" << static_cast<unsigned>(status.volume);
|
||||
|
||||
if (status.band == TunerBand::Dab) {
|
||||
out << ",\"dab\":{";
|
||||
if (status.dabFreqIndex) {
|
||||
out << "\"freq_index\":" << static_cast<unsigned>(*status.dabFreqIndex);
|
||||
}
|
||||
if (status.dabFicQuality) {
|
||||
if (status.dabFreqIndex) {
|
||||
out << ',';
|
||||
}
|
||||
out << "\"fic_quality\":" << static_cast<unsigned>(*status.dabFicQuality);
|
||||
}
|
||||
if (status.dabCnrDb) {
|
||||
out << ",\"cnr_db\":" << static_cast<int>(*status.dabCnrDb);
|
||||
}
|
||||
out << "},\"fm\":null";
|
||||
} else {
|
||||
out << ",\"fm\":{";
|
||||
if (status.fmFrequency) {
|
||||
out << "\"frequency_khz\":" << status.fmFrequency->value();
|
||||
}
|
||||
if (status.fmRssiDbuV) {
|
||||
if (status.fmFrequency) {
|
||||
out << ',';
|
||||
}
|
||||
out << "\"rssi_dbuv\":" << static_cast<int>(*status.fmRssiDbuV);
|
||||
}
|
||||
if (status.fmSnrDb) {
|
||||
out << ",\"snr_db\":" << static_cast<int>(*status.fmSnrDb);
|
||||
}
|
||||
if (status.fmStereo) {
|
||||
out << ",\"stereo\":" << (*status.fmStereo ? "true" : "false");
|
||||
}
|
||||
out << "},\"dab\":null";
|
||||
}
|
||||
out << '}';
|
||||
return out.str();
|
||||
}
|
||||
|
||||
std::string serializeTunerServicesJson(
|
||||
const std::vector<TunerServiceEntry>& services)
|
||||
{
|
||||
std::ostringstream out;
|
||||
out << "{\"services\":[";
|
||||
for (std::size_t i = 0; i < services.size(); ++i) {
|
||||
if (i > 0U) {
|
||||
out << ',';
|
||||
}
|
||||
const auto& s = services[i];
|
||||
out << "{\"service_id\":" << s.serviceId
|
||||
<< ",\"component_id\":" << s.componentId << ",\"label\":\"";
|
||||
for (char c : s.label) {
|
||||
if (c == '\0') {
|
||||
break;
|
||||
}
|
||||
if (c == '"' || c == '\\') {
|
||||
out << '\\';
|
||||
}
|
||||
out << c;
|
||||
}
|
||||
out << "\"}";
|
||||
}
|
||||
out << "]}";
|
||||
return out.str();
|
||||
}
|
||||
|
||||
std::string serializeTunerErrorJson(const char* reason)
|
||||
{
|
||||
return std::string("{\"status\":\"error\",\"reason\":\"") + reason + "\"}";
|
||||
}
|
||||
|
||||
std::expected<TunerTuneRequest, ParseError> parseTunerTuneJson(
|
||||
std::string_view json)
|
||||
{
|
||||
if (json.find('{') == std::string_view::npos) {
|
||||
return std::unexpected(ParseError::InvalidJson);
|
||||
}
|
||||
|
||||
const std::string_view band = extractJsonString(json, "band");
|
||||
if (band.empty()) {
|
||||
return std::unexpected(ParseError::MissingField);
|
||||
}
|
||||
|
||||
TunerTuneRequest req = {};
|
||||
if (band == "dab") {
|
||||
req.band = TunerBand::Dab;
|
||||
unsigned long idx = 0U;
|
||||
if (!extractJsonUint(json, "freq_index", idx) || idx > 37U) {
|
||||
return std::unexpected(ParseError::MissingField);
|
||||
}
|
||||
req.dabFreqIndex = static_cast<std::uint8_t>(idx);
|
||||
} else if (band == "fm") {
|
||||
req.band = TunerBand::Fm;
|
||||
unsigned long khz = 0U;
|
||||
if (!extractJsonUint(json, "frequency_khz", khz)) {
|
||||
return std::unexpected(ParseError::MissingField);
|
||||
}
|
||||
auto freq = FrequencyKHz::tryFromKhz(static_cast<std::uint32_t>(khz));
|
||||
if (!freq) {
|
||||
return std::unexpected(freq.error());
|
||||
}
|
||||
req.fmFrequency = *freq;
|
||||
} else {
|
||||
return std::unexpected(ParseError::InvalidJson);
|
||||
}
|
||||
return req;
|
||||
}
|
||||
|
||||
std::expected<TunerPlayRequest, ParseError> parseTunerPlayJson(
|
||||
std::string_view json)
|
||||
{
|
||||
if (json.find('{') == std::string_view::npos) {
|
||||
return std::unexpected(ParseError::InvalidJson);
|
||||
}
|
||||
|
||||
unsigned long sid = 0U;
|
||||
unsigned long cid = 0U;
|
||||
if (!extractJsonUint(json, "service_id", sid)
|
||||
|| !extractJsonUint(json, "component_id", cid)) {
|
||||
return std::unexpected(ParseError::MissingField);
|
||||
}
|
||||
|
||||
TunerPlayRequest req = {};
|
||||
req.serviceId = static_cast<std::uint32_t>(sid);
|
||||
req.componentId = static_cast<std::uint32_t>(cid);
|
||||
return req;
|
||||
}
|
||||
|
||||
} // namespace core
|
||||
@@ -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