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:
2026-07-06 17:42:11 +02:00
co-authored by Cursor
parent a08a9c19ee
commit 82c9f401da
46 changed files with 2819 additions and 33 deletions
+114
View File
@@ -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