Add BT1035 pairing and station presets (fw 0.7.0).
Expose discoverable mode and A2DP control over REST, add persisted DAB/FM preset list with web UI, and document gaps in docs/TODO.md. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* @file BluetoothJson.cpp
|
||||
* @brief BluetoothJson 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/BluetoothJson.hpp"
|
||||
|
||||
#include <sstream>
|
||||
|
||||
namespace core {
|
||||
|
||||
std::string serializeBluetoothStatusJson(const BluetoothStatus& status)
|
||||
{
|
||||
std::ostringstream out;
|
||||
out << "{\"booted\":" << (status.booted ? "true" : "false")
|
||||
<< ",\"pairing\":" << (status.pairing ? "true" : "false")
|
||||
<< ",\"a2dp\":\"" << a2dpStateToken(status.a2dpState) << "\"}";
|
||||
return out.str();
|
||||
}
|
||||
|
||||
std::string serializeBluetoothErrorJson(const char* reason)
|
||||
{
|
||||
std::ostringstream out;
|
||||
out << "{\"status\":\"error\",\"reason\":\"" << reason << "\"}";
|
||||
return out.str();
|
||||
}
|
||||
|
||||
} // namespace core
|
||||
@@ -13,6 +13,8 @@
|
||||
|
||||
#include "core/Bt1035At.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
namespace core {
|
||||
|
||||
namespace {
|
||||
@@ -32,6 +34,65 @@ namespace {
|
||||
return text;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool lineEqualsOk(std::string_view line) noexcept
|
||||
{
|
||||
return trimAscii(line) == "OK";
|
||||
}
|
||||
|
||||
[[nodiscard]] bool lineIsError(std::string_view line) noexcept
|
||||
{
|
||||
const std::string_view trimmed = trimAscii(line);
|
||||
return trimmed == "ERROR" || trimmed.starts_with("ERROR");
|
||||
}
|
||||
|
||||
[[nodiscard]] bool responseContainsOk(std::string_view response) noexcept
|
||||
{
|
||||
std::size_t start = 0;
|
||||
while (start < response.size()) {
|
||||
const std::size_t end = response.find_first_of("\r\n", start);
|
||||
const std::string_view line =
|
||||
end == std::string_view::npos
|
||||
? response.substr(start)
|
||||
: response.substr(start, end - start);
|
||||
if (lineEqualsOk(line)) {
|
||||
return true;
|
||||
}
|
||||
if (end == std::string_view::npos) {
|
||||
break;
|
||||
}
|
||||
start = end + 1U;
|
||||
while (start < response.size()
|
||||
&& (response[start] == '\r' || response[start] == '\n')) {
|
||||
++start;
|
||||
}
|
||||
}
|
||||
return lineEqualsOk(response);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool responseContainsError(std::string_view response) noexcept
|
||||
{
|
||||
std::size_t start = 0;
|
||||
while (start < response.size()) {
|
||||
const std::size_t end = response.find_first_of("\r\n", start);
|
||||
const std::string_view line =
|
||||
end == std::string_view::npos
|
||||
? response.substr(start)
|
||||
: response.substr(start, end - start);
|
||||
if (lineIsError(line)) {
|
||||
return true;
|
||||
}
|
||||
if (end == std::string_view::npos) {
|
||||
break;
|
||||
}
|
||||
start = end + 1U;
|
||||
while (start < response.size()
|
||||
&& (response[start] == '\r' || response[start] == '\n')) {
|
||||
++start;
|
||||
}
|
||||
}
|
||||
return lineIsError(response);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string buildBt1035AtLine(Bt1035AtCommand command)
|
||||
@@ -41,6 +102,14 @@ std::string buildBt1035AtLine(Bt1035AtCommand command)
|
||||
return "AT\r\n";
|
||||
case Bt1035AtCommand::AuxLineIn:
|
||||
return "AT+AUXCFG=1\r\n";
|
||||
case Bt1035AtCommand::PairDiscoverable:
|
||||
return "AT+PAIR=1\r\n";
|
||||
case Bt1035AtCommand::PairHidden:
|
||||
return "AT+PAIR=0\r\n";
|
||||
case Bt1035AtCommand::A2dpStat:
|
||||
return "AT+A2DPSTAT\r\n";
|
||||
case Bt1035AtCommand::A2dpDisconnect:
|
||||
return "AT+A2DPDISC\r\n";
|
||||
}
|
||||
return "AT\r\n";
|
||||
}
|
||||
@@ -55,6 +124,12 @@ std::array<Bt1035AtCommand, kBt1035BootInitCommandCount> bootInitSequence() noex
|
||||
|
||||
Bt1035AtResponseKind parseBt1035AtResponse(std::string_view line) noexcept
|
||||
{
|
||||
if (responseContainsOk(line)) {
|
||||
return Bt1035AtResponseKind::Ok;
|
||||
}
|
||||
if (responseContainsError(line)) {
|
||||
return Bt1035AtResponseKind::Error;
|
||||
}
|
||||
const std::string_view trimmed = trimAscii(line);
|
||||
if (trimmed == "OK") {
|
||||
return Bt1035AtResponseKind::Ok;
|
||||
@@ -65,4 +140,43 @@ Bt1035AtResponseKind parseBt1035AtResponse(std::string_view line) noexcept
|
||||
return Bt1035AtResponseKind::Unexpected;
|
||||
}
|
||||
|
||||
std::expected<Bt1035A2dpState, ParseError>
|
||||
parseBt1035A2dpStatResponse(std::string_view response)
|
||||
{
|
||||
constexpr std::string_view kPrefix = "+A2DPSTAT=";
|
||||
const std::size_t pos = response.find(kPrefix);
|
||||
if (pos == std::string_view::npos) {
|
||||
return std::unexpected(ParseError::MissingField);
|
||||
}
|
||||
|
||||
const std::size_t valueStart = pos + kPrefix.size();
|
||||
char* end = nullptr;
|
||||
const unsigned long raw =
|
||||
std::strtoul(response.data() + valueStart, &end, 10);
|
||||
if (end == response.data() + valueStart || raw > 5U) {
|
||||
return std::unexpected(ParseError::MissingField);
|
||||
}
|
||||
|
||||
return static_cast<Bt1035A2dpState>(raw);
|
||||
}
|
||||
|
||||
const char* a2dpStateToken(Bt1035A2dpState state) noexcept
|
||||
{
|
||||
switch (state) {
|
||||
case Bt1035A2dpState::Unsupported:
|
||||
return "unsupported";
|
||||
case Bt1035A2dpState::Standby:
|
||||
return "standby";
|
||||
case Bt1035A2dpState::Connecting:
|
||||
return "connecting";
|
||||
case Bt1035A2dpState::Connected:
|
||||
return "connected";
|
||||
case Bt1035A2dpState::Streaming:
|
||||
return "streaming";
|
||||
case Bt1035A2dpState::Paused:
|
||||
return "paused";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
} // namespace core
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* @file PresetSlot.cpp
|
||||
* @brief PresetSlot 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/PresetSlot.hpp"
|
||||
|
||||
namespace core {
|
||||
|
||||
std::expected<PresetSlot, ParseError> PresetSlot::tryFrom(unsigned slot) noexcept
|
||||
{
|
||||
if (slot < 1U || slot > kMaxSlot) {
|
||||
return std::unexpected(ParseError::MissingField);
|
||||
}
|
||||
return PresetSlot(static_cast<std::uint8_t>(slot));
|
||||
}
|
||||
|
||||
PresetSlot::PresetSlot(std::uint8_t slot) noexcept
|
||||
: slot_(slot)
|
||||
{
|
||||
}
|
||||
|
||||
std::uint8_t PresetSlot::value() const noexcept
|
||||
{
|
||||
return slot_;
|
||||
}
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* @file Station.cpp
|
||||
* @brief Station 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/Station.hpp"
|
||||
|
||||
namespace core {
|
||||
|
||||
Station::Station(StationName name,
|
||||
TunerBand band,
|
||||
std::uint8_t dabFreqIndex,
|
||||
std::optional<std::uint32_t> dabServiceId,
|
||||
std::optional<std::uint32_t> dabComponentId,
|
||||
std::optional<FrequencyKHz> fmFrequency,
|
||||
std::optional<PresetSlot> presetSlot)
|
||||
: name_(std::move(name))
|
||||
, band_(band)
|
||||
, dabFreqIndex_(dabFreqIndex)
|
||||
, dabServiceId_(dabServiceId)
|
||||
, dabComponentId_(dabComponentId)
|
||||
, fmFrequency_(std::move(fmFrequency))
|
||||
, presetSlot_(std::move(presetSlot))
|
||||
{
|
||||
}
|
||||
|
||||
const StationName& Station::name() const noexcept
|
||||
{
|
||||
return name_;
|
||||
}
|
||||
|
||||
TunerBand Station::band() const noexcept
|
||||
{
|
||||
return band_;
|
||||
}
|
||||
|
||||
std::uint8_t Station::dabFreqIndex() const noexcept
|
||||
{
|
||||
return dabFreqIndex_;
|
||||
}
|
||||
|
||||
std::optional<std::uint32_t> Station::dabServiceId() const noexcept
|
||||
{
|
||||
return dabServiceId_;
|
||||
}
|
||||
|
||||
std::optional<std::uint32_t> Station::dabComponentId() const noexcept
|
||||
{
|
||||
return dabComponentId_;
|
||||
}
|
||||
|
||||
std::optional<FrequencyKHz> Station::fmFrequency() const noexcept
|
||||
{
|
||||
return fmFrequency_;
|
||||
}
|
||||
|
||||
std::optional<PresetSlot> Station::presetSlot() const noexcept
|
||||
{
|
||||
return presetSlot_;
|
||||
}
|
||||
|
||||
bool Station::sameTuneTarget(const Station& other) const noexcept
|
||||
{
|
||||
if (band_ != other.band_) {
|
||||
return false;
|
||||
}
|
||||
if (band_ == TunerBand::Fm) {
|
||||
return fmFrequency_ && other.fmFrequency_
|
||||
&& fmFrequency_->value() == other.fmFrequency_->value();
|
||||
}
|
||||
if (dabFreqIndex_ != other.dabFreqIndex_) {
|
||||
return false;
|
||||
}
|
||||
return dabServiceId_ == other.dabServiceId_
|
||||
&& dabComponentId_ == other.dabComponentId_;
|
||||
}
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* @file StationList.cpp
|
||||
* @brief StationList 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/StationList.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace core {
|
||||
|
||||
StationList::StationList() = default;
|
||||
|
||||
const std::vector<Station>& StationList::stations() const noexcept
|
||||
{
|
||||
return stations_;
|
||||
}
|
||||
|
||||
void StationList::replaceAll(std::vector<Station> stations)
|
||||
{
|
||||
stations_ = std::move(stations);
|
||||
}
|
||||
|
||||
bool StationList::hasDuplicateTuneTarget(const Station& candidate) const
|
||||
{
|
||||
for (const Station& existing : stations_) {
|
||||
if (existing.sameTuneTarget(candidate)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool StationList::slotTaken(const PresetSlot& slot) const
|
||||
{
|
||||
for (const Station& existing : stations_) {
|
||||
if (existing.presetSlot() && existing.presetSlot()->value() == slot.value()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::expected<void, StationListError> StationList::add(Station station)
|
||||
{
|
||||
if (stations_.size() >= kMaxStations) {
|
||||
return std::unexpected(StationListError::Full);
|
||||
}
|
||||
if (hasDuplicateTuneTarget(station)) {
|
||||
return std::unexpected(StationListError::Duplicate);
|
||||
}
|
||||
if (station.presetSlot() && slotTaken(*station.presetSlot())) {
|
||||
return std::unexpected(StationListError::SlotInUse);
|
||||
}
|
||||
stations_.push_back(std::move(station));
|
||||
return {};
|
||||
}
|
||||
|
||||
std::expected<void, StationListError> StationList::removeAt(std::size_t index)
|
||||
{
|
||||
if (index >= stations_.size()) {
|
||||
return std::unexpected(StationListError::NotFound);
|
||||
}
|
||||
stations_.erase(stations_.begin() + static_cast<std::ptrdiff_t>(index));
|
||||
return {};
|
||||
}
|
||||
|
||||
std::expected<void, StationListError> StationList::move(std::size_t fromIndex,
|
||||
std::size_t toIndex)
|
||||
{
|
||||
if (fromIndex >= stations_.size() || toIndex >= stations_.size()) {
|
||||
return std::unexpected(StationListError::NotFound);
|
||||
}
|
||||
if (fromIndex == toIndex) {
|
||||
return {};
|
||||
}
|
||||
Station moving = std::move(stations_[fromIndex]);
|
||||
stations_.erase(stations_.begin() + static_cast<std::ptrdiff_t>(fromIndex));
|
||||
stations_.insert(stations_.begin() + static_cast<std::ptrdiff_t>(toIndex),
|
||||
std::move(moving));
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,265 @@
|
||||
/**
|
||||
* @file StationListJson.cpp
|
||||
* @brief StationListJson 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/StationListJson.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";
|
||||
}
|
||||
|
||||
[[nodiscard]] std::expected<TunerBand, ParseError>
|
||||
parseBandField(std::string_view json)
|
||||
{
|
||||
const std::string_view band = extractJsonString(json, "band");
|
||||
if (band == "fm") {
|
||||
return TunerBand::Fm;
|
||||
}
|
||||
if (band == "dab") {
|
||||
return TunerBand::Dab;
|
||||
}
|
||||
return std::unexpected(ParseError::MissingField);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::expected<Station, ParseError>
|
||||
parseStationObject(std::string_view json)
|
||||
{
|
||||
const std::string_view nameRaw = extractJsonString(json, "name");
|
||||
auto name = StationName::tryFrom(nameRaw);
|
||||
if (!name) {
|
||||
return std::unexpected(ParseError::MissingField);
|
||||
}
|
||||
|
||||
auto band = parseBandField(json);
|
||||
if (!band) {
|
||||
return std::unexpected(ParseError::MissingField);
|
||||
}
|
||||
|
||||
std::optional<PresetSlot> slot;
|
||||
unsigned long slotRaw = 0;
|
||||
if (extractJsonUint(json, "preset_slot", slotRaw)) {
|
||||
auto parsedSlot = PresetSlot::tryFrom(slotRaw);
|
||||
if (!parsedSlot) {
|
||||
return std::unexpected(ParseError::MissingField);
|
||||
}
|
||||
slot = *parsedSlot;
|
||||
}
|
||||
|
||||
if (*band == TunerBand::Fm) {
|
||||
unsigned long khz = 0;
|
||||
if (!extractJsonUint(json, "fm_frequency_khz", khz)) {
|
||||
return std::unexpected(ParseError::MissingField);
|
||||
}
|
||||
auto frequency = FrequencyKHz::tryFromKhz(static_cast<std::uint32_t>(khz));
|
||||
if (!frequency) {
|
||||
return std::unexpected(ParseError::MissingField);
|
||||
}
|
||||
return Station(*name, TunerBand::Fm, 0U, std::nullopt, std::nullopt,
|
||||
*frequency, slot);
|
||||
}
|
||||
|
||||
unsigned long dabIndex = 0;
|
||||
if (!extractJsonUint(json, "dab_freq_index", dabIndex) || dabIndex > 37U) {
|
||||
return std::unexpected(ParseError::MissingField);
|
||||
}
|
||||
|
||||
std::optional<std::uint32_t> serviceId;
|
||||
unsigned long serviceRaw = 0;
|
||||
if (extractJsonUint(json, "dab_service_id", serviceRaw)) {
|
||||
serviceId = static_cast<std::uint32_t>(serviceRaw);
|
||||
}
|
||||
|
||||
std::optional<std::uint32_t> componentId;
|
||||
unsigned long componentRaw = 0;
|
||||
if (extractJsonUint(json, "dab_component_id", componentRaw)) {
|
||||
componentId = static_cast<std::uint32_t>(componentRaw);
|
||||
}
|
||||
|
||||
return Station(*name, TunerBand::Dab, static_cast<std::uint8_t>(dabIndex),
|
||||
serviceId, componentId, std::nullopt, slot);
|
||||
}
|
||||
|
||||
void appendStationJson(std::ostringstream& out, const Station& station)
|
||||
{
|
||||
out << "{\"name\":\"" << station.name().value() << "\",\"band\":\""
|
||||
<< bandToken(station.band()) << "\"";
|
||||
|
||||
if (station.band() == TunerBand::Fm && station.fmFrequency()) {
|
||||
out << ",\"fm_frequency_khz\":" << station.fmFrequency()->value();
|
||||
} else {
|
||||
out << ",\"dab_freq_index\":"
|
||||
<< static_cast<unsigned>(station.dabFreqIndex());
|
||||
if (station.dabServiceId()) {
|
||||
out << ",\"dab_service_id\":" << *station.dabServiceId();
|
||||
}
|
||||
if (station.dabComponentId()) {
|
||||
out << ",\"dab_component_id\":" << *station.dabComponentId();
|
||||
}
|
||||
}
|
||||
|
||||
if (station.presetSlot()) {
|
||||
out << ",\"preset_slot\":"
|
||||
<< static_cast<unsigned>(station.presetSlot()->value());
|
||||
}
|
||||
out << '}';
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string serializeStationListJson(const StationList& list)
|
||||
{
|
||||
std::ostringstream out;
|
||||
out << "{\"stations\":[";
|
||||
bool first = true;
|
||||
for (const Station& station : list.stations()) {
|
||||
if (!first) {
|
||||
out << ',';
|
||||
}
|
||||
first = false;
|
||||
appendStationJson(out, station);
|
||||
}
|
||||
out << "]}";
|
||||
return out.str();
|
||||
}
|
||||
|
||||
std::expected<StationList, ParseError> parseStationListJson(std::string_view json)
|
||||
{
|
||||
StationList list;
|
||||
const std::size_t arrayStart = json.find("\"stations\"");
|
||||
if (arrayStart == std::string_view::npos) {
|
||||
return list;
|
||||
}
|
||||
|
||||
std::size_t pos = json.find('[', arrayStart);
|
||||
if (pos == std::string_view::npos) {
|
||||
return std::unexpected(ParseError::InvalidJson);
|
||||
}
|
||||
++pos;
|
||||
|
||||
while (pos < json.size()) {
|
||||
while (pos < json.size()
|
||||
&& (json[pos] == ' ' || json[pos] == ',' || json[pos] == '\n'
|
||||
|| json[pos] == '\r')) {
|
||||
++pos;
|
||||
}
|
||||
if (pos >= json.size() || json[pos] == ']') {
|
||||
break;
|
||||
}
|
||||
if (json[pos] != '{') {
|
||||
return std::unexpected(ParseError::InvalidJson);
|
||||
}
|
||||
const std::size_t objectStart = pos;
|
||||
int depth = 0;
|
||||
for (; pos < json.size(); ++pos) {
|
||||
if (json[pos] == '{') {
|
||||
++depth;
|
||||
} else if (json[pos] == '}') {
|
||||
--depth;
|
||||
if (depth == 0) {
|
||||
++pos;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const std::string_view object =
|
||||
json.substr(objectStart, pos - objectStart);
|
||||
auto station = parseStationObject(object);
|
||||
if (!station) {
|
||||
return std::unexpected(station.error());
|
||||
}
|
||||
if (auto added = list.add(std::move(*station)); !added) {
|
||||
return std::unexpected(ParseError::InvalidJson);
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
std::expected<Station, ParseError> parseStationJson(std::string_view json)
|
||||
{
|
||||
return parseStationObject(json);
|
||||
}
|
||||
|
||||
std::expected<StationRemoveRequest, ParseError>
|
||||
parseStationRemoveJson(std::string_view json)
|
||||
{
|
||||
unsigned long index = 0;
|
||||
if (!extractJsonUint(json, "index", index)) {
|
||||
return std::unexpected(ParseError::MissingField);
|
||||
}
|
||||
return StationRemoveRequest{.index = static_cast<std::size_t>(index)};
|
||||
}
|
||||
|
||||
std::string serializeStationListErrorJson(const char* reason)
|
||||
{
|
||||
std::ostringstream out;
|
||||
out << "{\"status\":\"error\",\"reason\":\"" << reason << "\"}";
|
||||
return out.str();
|
||||
}
|
||||
|
||||
const char* stationListErrorToken(StationListError error) noexcept
|
||||
{
|
||||
switch (error) {
|
||||
case StationListError::Duplicate:
|
||||
return "duplicate";
|
||||
case StationListError::Full:
|
||||
return "full";
|
||||
case StationListError::NotFound:
|
||||
return "not_found";
|
||||
case StationListError::SlotInUse:
|
||||
return "slot_in_use";
|
||||
}
|
||||
return "station_error";
|
||||
}
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* @file StationName.cpp
|
||||
* @brief StationName 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/StationName.hpp"
|
||||
|
||||
namespace core {
|
||||
|
||||
std::expected<StationName, ParseError> StationName::tryFrom(std::string_view raw)
|
||||
{
|
||||
if (raw.empty() || raw.size() > kMaxLength) {
|
||||
return std::unexpected(ParseError::MissingField);
|
||||
}
|
||||
return StationName(std::string(raw));
|
||||
}
|
||||
|
||||
StationName::StationName(std::string name)
|
||||
: name_(std::move(name))
|
||||
{
|
||||
}
|
||||
|
||||
std::string_view StationName::value() const noexcept
|
||||
{
|
||||
return name_;
|
||||
}
|
||||
|
||||
} // namespace core
|
||||
Reference in New Issue
Block a user