Add preset polish and broadcast metadata (fw 0.8.0).

Ship station reorder, DAB playing ids in presets, RDS PS/RT and DAB DLS in tuner status, Si4684 data-service read, host tests, and UI now-playing lines.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-07 00:01:49 +02:00
co-authored by Cursor
parent 9f76ab9e74
commit 0fc714e431
34 changed files with 1379 additions and 65 deletions
@@ -21,6 +21,7 @@
#include <cstdint>
#include <expected>
#include <optional>
#include <span>
#include <vector>
@@ -236,6 +237,21 @@ public:
*/
[[nodiscard]] std::expected<Si4684FmRdsStatus, Si4684Error> readFmRds();
/**
* @brief readDabServiceData — read one queued DAB data-service block.
*
* @dname readDabServiceData
* @param statusOnly Poll queue depth without consuming payload.
* @param ack Acknowledge DSRVINT when reading payload.
* @return Data block on success, nullopt when queue empty, or Si4684Error.
* @pubstate reads GET_DIGITAL_SERVICE_DATA response bytes.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<std::optional<Si4684DabServiceData>, Si4684Error>
readDabServiceData(bool statusOnly, bool ack);
/**
* @brief installDefaultDabFrequencyPlan — load Band III frequency list.
*
@@ -12,10 +12,14 @@
*/
#pragma once
#include "core/DabDynamicLabelAccumulator.hpp"
#include "core/FrequencyKHz.hpp"
#include "core/ITuner.hpp"
#include "core/RdsMetadataAccumulator.hpp"
#include "si4684/Si4684Driver.hpp"
#include <optional>
namespace si4684 {
/**
@@ -190,6 +194,10 @@ private:
std::uint8_t dabIndex_;
core::FrequencyKHz fmFrequency_;
std::uint8_t volume_;
core::RdsMetadataAccumulator rdsMetadata_;
core::DabDynamicLabelAccumulator dabDynamicLabel_;
std::optional<std::uint32_t> lastDabServiceId_;
std::optional<std::uint32_t> lastDabComponentId_;
};
} // namespace si4684
@@ -18,6 +18,7 @@
#include <array>
#include <cstddef>
#include <cstdint>
#include <vector>
namespace si4684 {
@@ -113,6 +114,27 @@ struct Si4684FmRdsStatus {
std::uint16_t blockC; ///< RDS block C.
std::uint16_t blockD; ///< RDS block D.
bool received; ///< Group received flag.
std::uint8_t fifoUsed; ///< Remaining groups in the RDS FIFO.
};
/**
* @brief Si4684DabServiceData — one GET_DIGITAL_SERVICE_DATA block.
*
* @dname Si4684DabServiceData
* @return n/a (type)
* @pubstate Plain DTO from GET_DIGITAL_SERVICE_DATA response bytes.
*
* @author Michele Bigi
* @date 2026-07-06
*/
struct Si4684DabServiceData {
std::uint32_t serviceId; ///< Associated DAB service identifier.
std::uint32_t componentId; ///< Associated component identifier.
std::uint8_t dataSrc; ///< Payload source (2 = DLS PAD).
std::uint16_t byteCount; ///< Payload byte count.
std::uint16_t segmentIndex; ///< Zero-based segment index.
std::uint16_t segmentCount; ///< Total segments for this label object.
std::vector<std::uint8_t> payload; ///< Raw payload bytes.
};
/**
@@ -587,6 +587,7 @@ std::expected<Si4684FmRdsStatus, Si4684Error> Si4684Driver::readFmRds()
Si4684FmRdsStatus rds = {};
rds.received = (raw[4] & 0x01U) != 0U;
rds.fifoUsed = raw[10];
rds.blockA = readLe16(raw.data() + 12);
rds.blockB = readLe16(raw.data() + 14);
rds.blockC = readLe16(raw.data() + 16);
@@ -594,6 +595,60 @@ std::expected<Si4684FmRdsStatus, Si4684Error> Si4684Driver::readFmRds()
return rds;
}
std::expected<std::optional<Si4684DabServiceData>, Si4684Error>
Si4684Driver::readDabServiceData(bool statusOnly, bool ack)
{
if (auto band = ensureBand(Si4684Band::Dab); !band) {
return std::unexpected(band.error());
}
const std::uint8_t arg1 =
static_cast<std::uint8_t>((statusOnly ? 0x08U : 0x00U)
| (ack ? 0x01U : 0x00U));
const std::uint8_t args[] = {arg1};
if (auto cmd =
writeCommand(Command::GetDigitalServiceData, args, sizeof(args));
!cmd) {
return std::unexpected(Si4684Error::CommandFailed);
}
std::array<std::uint8_t, 24> header = {};
if (auto rd = readRaw(header); !rd) {
return std::unexpected(rd.error());
}
if (statusOnly) {
if (header[5] == 0U) {
return std::optional<Si4684DabServiceData>{};
}
}
const std::uint16_t byteCount = readLe16(header.data() + 18);
if (byteCount == 0U) {
return std::optional<Si4684DabServiceData>{};
}
if (byteCount + 24U > kSpiBufferSize) {
return std::unexpected(Si4684Error::ReplyTooShort);
}
std::vector<std::uint8_t> body(byteCount, 0U);
if (byteCount > 0U) {
if (auto rd = readRaw(body); !rd) {
return std::unexpected(rd.error());
}
}
Si4684DabServiceData data = {};
data.dataSrc = static_cast<std::uint8_t>((header[7] >> 6U) & 0x03U);
data.serviceId = readLe32(header.data() + 8);
data.componentId = readLe32(header.data() + 12);
data.byteCount = byteCount;
data.segmentIndex = readLe16(header.data() + 20);
data.segmentCount = readLe16(header.data() + 22);
data.payload = std::move(body);
return data;
}
std::expected<void, Si4684Error> Si4684Driver::installDefaultDabFrequencyPlan()
{
if (auto band = ensureBand(Si4684Band::Dab); !band) {
@@ -89,6 +89,19 @@ std::expected<core::TunerStatus, core::TunerError> Si4684Tuner::readStatus()
} else {
return std::unexpected(mapError(dig.error()));
}
if (lastDabServiceId_) {
if (auto data = driver_.readDabServiceData(false, true); data) {
if (*data && (*data)->dataSrc == 2U) {
dabDynamicLabel_.applySegment((*data)->segmentIndex,
(*data)->segmentCount,
(*data)->payload);
status.dabDynamicLabel = dabDynamicLabel_.label();
}
} else {
return std::unexpected(mapError(data.error()));
}
}
} else {
status.fmFrequency = fmFrequency_;
if (auto rsq = driver_.readFmRsq(); rsq) {
@@ -101,6 +114,25 @@ std::expected<core::TunerStatus, core::TunerError> Si4684Tuner::readStatus()
} else {
return std::unexpected(mapError(rsq.error()));
}
for (int attempt = 0; attempt < 8; ++attempt) {
auto rds = driver_.readFmRds();
if (!rds) {
return std::unexpected(mapError(rds.error()));
}
if (!rds->received) {
break;
}
rdsMetadata_.applyGroup(rds->blockA,
rds->blockB,
rds->blockC,
rds->blockD);
if (rds->fifoUsed <= 1U) {
break;
}
}
status.fmStationName = rdsMetadata_.programName();
status.fmRadiotext = rdsMetadata_.radiotext();
}
return status;
}
@@ -112,6 +144,9 @@ std::expected<void, core::TunerError> Si4684Tuner::tuneDab(
return std::unexpected(mapError(result.error()));
}
dabIndex_ = freqIndex;
dabDynamicLabel_.reset();
lastDabServiceId_.reset();
lastDabComponentId_.reset();
return {};
}
@@ -122,6 +157,7 @@ std::expected<void, core::TunerError> Si4684Tuner::tuneFm(
return std::unexpected(mapError(result.error()));
}
fmFrequency_ = frequency;
rdsMetadata_.reset();
return {};
}
@@ -132,6 +168,7 @@ std::expected<core::FrequencyKHz, core::TunerError> Si4684Tuner::seekFm(
return std::unexpected(mapError(result.error()));
}
fmFrequency_ = *result;
rdsMetadata_.reset();
return *result;
}
@@ -171,6 +208,9 @@ std::expected<void, core::TunerError> Si4684Tuner::playDabService(
if (auto result = driver_.startDabService(serviceId, componentId); !result) {
return std::unexpected(mapError(result.error()));
}
lastDabServiceId_ = serviceId;
lastDabComponentId_ = componentId;
dabDynamicLabel_.reset();
return {};
}