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:
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* @file BroadcastLabel.cpp
|
||||
* @brief BroadcastLabel 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/BroadcastLabel.hpp"
|
||||
|
||||
namespace core {
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] std::string trimChipPadding(std::string_view raw)
|
||||
{
|
||||
std::size_t end = raw.size();
|
||||
if (end > BroadcastLabel::kMaxLength) {
|
||||
end = BroadcastLabel::kMaxLength;
|
||||
}
|
||||
while (end > 0U && (raw[end - 1U] == '\0' || raw[end - 1U] == ' ')) {
|
||||
--end;
|
||||
}
|
||||
std::size_t start = 0U;
|
||||
while (start < end && raw[start] == ' ') {
|
||||
++start;
|
||||
}
|
||||
return std::string(raw.substr(start, end - start));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::optional<BroadcastLabel> BroadcastLabel::tryFromChipBytes(
|
||||
std::string_view raw)
|
||||
{
|
||||
const std::string trimmed = trimChipPadding(raw);
|
||||
if (trimmed.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return BroadcastLabel(trimmed);
|
||||
}
|
||||
|
||||
BroadcastLabel::BroadcastLabel(std::string label)
|
||||
: label_(std::move(label))
|
||||
{
|
||||
}
|
||||
|
||||
std::string_view BroadcastLabel::value() const noexcept
|
||||
{
|
||||
return label_;
|
||||
}
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @file DabDynamicLabelAccumulator.cpp
|
||||
* @brief DabDynamicLabelAccumulator 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/DabDynamicLabelAccumulator.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
namespace core {
|
||||
|
||||
void DabDynamicLabelAccumulator::reset() noexcept
|
||||
{
|
||||
buffer_.fill('\0');
|
||||
segmentValid_.fill(false);
|
||||
expectedSegments_ = 0U;
|
||||
}
|
||||
|
||||
void DabDynamicLabelAccumulator::applySegment(
|
||||
std::uint16_t segmentIndex,
|
||||
std::uint16_t segmentCount,
|
||||
std::span<const std::uint8_t> payload) noexcept
|
||||
{
|
||||
if (segmentCount == 0U || segmentCount > kMaxSegments) {
|
||||
return;
|
||||
}
|
||||
if (segmentIndex >= segmentCount) {
|
||||
return;
|
||||
}
|
||||
if (expectedSegments_ != segmentCount) {
|
||||
reset();
|
||||
expectedSegments_ = segmentCount;
|
||||
}
|
||||
|
||||
const std::size_t offset =
|
||||
static_cast<std::size_t>(segmentIndex)
|
||||
* static_cast<std::size_t>(payload.size());
|
||||
if (offset >= buffer_.size()) {
|
||||
return;
|
||||
}
|
||||
const std::size_t copyLen =
|
||||
std::min(payload.size(), buffer_.size() - offset);
|
||||
std::memcpy(buffer_.data() + offset, payload.data(), copyLen);
|
||||
segmentValid_[segmentIndex] = true;
|
||||
}
|
||||
|
||||
std::optional<BroadcastLabel> DabDynamicLabelAccumulator::label() const
|
||||
{
|
||||
if (expectedSegments_ == 0U) {
|
||||
return std::nullopt;
|
||||
}
|
||||
for (std::uint16_t i = 0U; i < expectedSegments_; ++i) {
|
||||
if (!segmentValid_[i]) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
return BroadcastLabel::tryFromChipBytes(
|
||||
std::string_view(buffer_.data(), buffer_.size()));
|
||||
}
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* @file RdsMetadataAccumulator.cpp
|
||||
* @brief RdsMetadataAccumulator 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/RdsMetadataAccumulator.hpp"
|
||||
|
||||
namespace core {
|
||||
|
||||
void RdsMetadataAccumulator::reset() noexcept
|
||||
{
|
||||
psBuffer_.fill('\0');
|
||||
psSegmentValid_.fill(false);
|
||||
rtBuffer_.fill('\0');
|
||||
rtSegmentValid_.fill(false);
|
||||
rtAbFlag_ = false;
|
||||
rtAbInitialized_ = false;
|
||||
}
|
||||
|
||||
void RdsMetadataAccumulator::applyGroup(std::uint16_t blockA,
|
||||
std::uint16_t blockB,
|
||||
std::uint16_t blockC,
|
||||
std::uint16_t blockD) noexcept
|
||||
{
|
||||
(void)blockA;
|
||||
const std::uint8_t groupType =
|
||||
static_cast<std::uint8_t>((blockB >> 12U) & 0x0FU);
|
||||
|
||||
if (groupType == 0U) {
|
||||
const std::size_t index =
|
||||
static_cast<std::size_t>((blockB >> 1U) & 0x03U);
|
||||
if (index < kPsSegments) {
|
||||
psBuffer_[index * 2U] =
|
||||
static_cast<char>((blockC >> 8U) & 0xFFU);
|
||||
psBuffer_[index * 2U + 1U] = static_cast<char>(blockC & 0xFFU);
|
||||
psSegmentValid_[index] = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (groupType == 2U) {
|
||||
const bool abFlag = (blockB & 0x10U) != 0U;
|
||||
if (!rtAbInitialized_ || abFlag != rtAbFlag_) {
|
||||
rtBuffer_.fill('\0');
|
||||
rtSegmentValid_.fill(false);
|
||||
rtAbFlag_ = abFlag;
|
||||
rtAbInitialized_ = true;
|
||||
}
|
||||
const std::size_t index =
|
||||
static_cast<std::size_t>(blockB & 0x0FU);
|
||||
if (index < kRtSegments) {
|
||||
rtBuffer_[index * 4U] =
|
||||
static_cast<char>((blockC >> 8U) & 0xFFU);
|
||||
rtBuffer_[index * 4U + 1U] =
|
||||
static_cast<char>(blockC & 0xFFU);
|
||||
rtBuffer_[index * 4U + 2U] =
|
||||
static_cast<char>((blockD >> 8U) & 0xFFU);
|
||||
rtBuffer_[index * 4U + 3U] =
|
||||
static_cast<char>(blockD & 0xFFU);
|
||||
rtSegmentValid_[index] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<BroadcastLabel> RdsMetadataAccumulator::programName() const
|
||||
{
|
||||
for (bool valid : psSegmentValid_) {
|
||||
if (!valid) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
return BroadcastLabel::tryFromChipBytes(
|
||||
std::string_view(psBuffer_.data(), psBuffer_.size()));
|
||||
}
|
||||
|
||||
std::optional<BroadcastLabel> RdsMetadataAccumulator::radiotext() const
|
||||
{
|
||||
bool any = false;
|
||||
for (bool valid : rtSegmentValid_) {
|
||||
if (valid) {
|
||||
any = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!any) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return BroadcastLabel::tryFromChipBytes(
|
||||
std::string_view(rtBuffer_.data(), rtBuffer_.size()));
|
||||
}
|
||||
|
||||
} // namespace core
|
||||
@@ -240,6 +240,21 @@ parseStationRemoveJson(std::string_view json)
|
||||
return StationRemoveRequest{.index = static_cast<std::size_t>(index)};
|
||||
}
|
||||
|
||||
std::expected<StationReorderRequest, ParseError>
|
||||
parseStationReorderJson(std::string_view json)
|
||||
{
|
||||
unsigned long fromIndex = 0;
|
||||
unsigned long toIndex = 0;
|
||||
if (!extractJsonUint(json, "from", fromIndex)
|
||||
|| !extractJsonUint(json, "to", toIndex)) {
|
||||
return std::unexpected(ParseError::MissingField);
|
||||
}
|
||||
return StationReorderRequest{
|
||||
.fromIndex = static_cast<std::size_t>(fromIndex),
|
||||
.toIndex = static_cast<std::size_t>(toIndex),
|
||||
};
|
||||
}
|
||||
|
||||
std::string serializeStationListErrorJson(const char* reason)
|
||||
{
|
||||
std::ostringstream out;
|
||||
@@ -258,6 +273,8 @@ const char* stationListErrorToken(StationListError error) noexcept
|
||||
return "not_found";
|
||||
case StationListError::SlotInUse:
|
||||
return "slot_in_use";
|
||||
case StationListError::PersistFailed:
|
||||
return "persist_failed";
|
||||
}
|
||||
return "station_error";
|
||||
}
|
||||
|
||||
@@ -58,6 +58,29 @@ namespace {
|
||||
return band == TunerBand::Fm ? "fm" : "dab";
|
||||
}
|
||||
|
||||
void appendJsonString(std::ostringstream& out, std::string_view value)
|
||||
{
|
||||
out << '"';
|
||||
for (char c : value) {
|
||||
if (c == '"' || c == '\\') {
|
||||
out << '\\';
|
||||
}
|
||||
out << c;
|
||||
}
|
||||
out << '"';
|
||||
}
|
||||
|
||||
void appendOptionalLabel(std::ostringstream& out,
|
||||
std::string_view key,
|
||||
const std::optional<BroadcastLabel>& label)
|
||||
{
|
||||
if (!label) {
|
||||
return;
|
||||
}
|
||||
out << ",\"" << key << "\":";
|
||||
appendJsonString(out, label->value());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string serializeTunerStatusJson(const TunerStatus& status)
|
||||
@@ -82,6 +105,15 @@ std::string serializeTunerStatusJson(const TunerStatus& status)
|
||||
if (status.dabCnrDb) {
|
||||
out << ",\"cnr_db\":" << static_cast<int>(*status.dabCnrDb);
|
||||
}
|
||||
if (status.dabPlayingServiceId) {
|
||||
out << ",\"playing_service_id\":"
|
||||
<< *status.dabPlayingServiceId;
|
||||
}
|
||||
if (status.dabPlayingComponentId) {
|
||||
out << ",\"playing_component_id\":"
|
||||
<< *status.dabPlayingComponentId;
|
||||
}
|
||||
appendOptionalLabel(out, "dynamic_label", status.dabDynamicLabel);
|
||||
out << "},\"fm\":null";
|
||||
} else {
|
||||
out << ",\"fm\":{";
|
||||
@@ -100,6 +132,8 @@ std::string serializeTunerStatusJson(const TunerStatus& status)
|
||||
if (status.fmStereo) {
|
||||
out << ",\"stereo\":" << (*status.fmStereo ? "true" : "false");
|
||||
}
|
||||
appendOptionalLabel(out, "station_name", status.fmStationName);
|
||||
appendOptionalLabel(out, "radiotext", status.fmRadiotext);
|
||||
out << "},\"dab\":null";
|
||||
}
|
||||
out << '}';
|
||||
@@ -116,18 +150,15 @@ std::string serializeTunerServicesJson(
|
||||
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;
|
||||
std::string_view labelText(s.label.data(), s.label.size());
|
||||
const std::size_t nul = labelText.find('\0');
|
||||
if (nul != std::string_view::npos) {
|
||||
labelText = labelText.substr(0U, nul);
|
||||
}
|
||||
out << "\"}";
|
||||
out << "{\"service_id\":" << s.serviceId
|
||||
<< ",\"component_id\":" << s.componentId << ",\"label\":";
|
||||
appendJsonString(out, labelText);
|
||||
out << "}";
|
||||
}
|
||||
out << "]}";
|
||||
return out.str();
|
||||
|
||||
Reference in New Issue
Block a user