Files
DigiRadio/Software/components/core/src/Bt1035At.cpp
T
micheleandCursor 0a1188ad66 Release fw 0.8.5: BT1035 I2S boot init and hardware doc alignment.
Switch BT1035 bring-up from Line-In to I2S slave (AT+AUXCFG=3, AT+I2SCFG=67) to match the ADAU1701 PCM routing, confirm 2 kΩ I2C pull-ups on R1/R16, and sync firmware docs, AGENTS rules, and the DATASHEET bundle.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 14:41:31 +02:00

290 lines
8.9 KiB
C++

/**
* @file Bt1035At.cpp
* @brief Bt1035At 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/Bt1035At.hpp"
#include <cstdlib>
#include <vector>
namespace core {
namespace {
[[nodiscard]] std::string_view trimAscii(std::string_view text) noexcept
{
while (!text.empty()
&& (text.front() == ' ' || text.front() == '\r'
|| text.front() == '\n' || text.front() == '\t')) {
text.remove_prefix(1U);
}
while (!text.empty()
&& (text.back() == ' ' || text.back() == '\r' || text.back() == '\n'
|| text.back() == '\t')) {
text.remove_suffix(1U);
}
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)
{
switch (command) {
case Bt1035AtCommand::Ping:
return "AT\r\n";
case Bt1035AtCommand::I2sMode:
return "AT+AUXCFG=3\r\n";
case Bt1035AtCommand::I2sSlave48k32:
return "AT+I2SCFG=67\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";
case Bt1035AtCommand::QueryName:
return "AT+NAME\r\n";
case Bt1035AtCommand::QueryAutoConn:
return "AT+AUTOCONN\r\n";
case Bt1035AtCommand::QueryPairedList:
return "AT+PLIST\r\n";
}
return "AT\r\n";
}
std::string buildBt1035SetAutoConnLine(std::uint8_t times)
{
if (times > 15U) {
times = 15U;
}
return "AT+AUTOCONN=" + std::to_string(times) + "\r\n";
}
std::string buildBt1035SetNameLine(std::string_view name, bool enableMacSuffix)
{
if (name.empty() || name.size() > 31U) {
return {};
}
return std::string("AT+NAME=") + std::string(name) + ","
+ (enableMacSuffix ? "1" : "0") + "\r\n";
}
std::array<Bt1035AtCommand, kBt1035BootInitCommandCount> bootInitSequence() noexcept
{
return std::array<Bt1035AtCommand, kBt1035BootInitCommandCount>{
Bt1035AtCommand::Ping,
Bt1035AtCommand::I2sMode,
Bt1035AtCommand::I2sSlave48k32,
};
}
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;
}
if (trimmed == "ERROR" || trimmed.starts_with("ERROR")) {
return Bt1035AtResponseKind::Error;
}
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";
}
std::expected<std::string, ParseError>
parseBt1035NameResponse(std::string_view response)
{
constexpr std::string_view kPrefix = "+NAME=";
const std::size_t pos = response.find(kPrefix);
if (pos == std::string_view::npos) {
return std::unexpected(ParseError::MissingField);
}
const std::size_t start = pos + kPrefix.size();
const std::size_t end = response.find_first_of("\r\n", start);
const std::string_view value =
end == std::string_view::npos ? response.substr(start)
: response.substr(start, end - start);
if (value.empty()) {
return std::unexpected(ParseError::MissingField);
}
return std::string(value);
}
std::expected<std::uint8_t, ParseError>
parseBt1035AutoConnResponse(std::string_view response)
{
constexpr std::string_view kPrefix = "+AUTOCONN=";
const std::size_t pos = response.find(kPrefix);
if (pos == std::string_view::npos) {
return std::unexpected(ParseError::MissingField);
}
const std::size_t start = pos + kPrefix.size();
char* end = nullptr;
const unsigned long raw =
std::strtoul(response.data() + start, &end, 10);
if (end == response.data() + start || raw > 15U) {
return std::unexpected(ParseError::MissingField);
}
return static_cast<std::uint8_t>(raw);
}
std::expected<std::vector<Bt1035PairedDevice>, ParseError>
parseBt1035PairedListResponse(std::string_view response)
{
std::vector<Bt1035PairedDevice> devices;
constexpr std::string_view kPrefix = "+PLIST=";
std::size_t pos = 0;
while ((pos = response.find(kPrefix, pos)) != std::string_view::npos) {
const std::size_t start = pos + kPrefix.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);
pos = end == std::string_view::npos ? response.size() : end + 1U;
if (line.empty() || line.front() == 'E') {
continue;
}
const std::size_t comma1 = line.find(',');
if (comma1 == std::string_view::npos) {
continue;
}
const std::size_t comma2 = line.find(',', comma1 + 1U);
char* endIdx = nullptr;
const unsigned long index =
std::strtoul(line.data(), &endIdx, 10);
if (endIdx == line.data() || index == 0U || index > 8U) {
continue;
}
Bt1035PairedDevice entry = {};
entry.index = static_cast<std::uint8_t>(index);
entry.mac = std::string(line.substr(comma1 + 1U,
(comma2 == std::string_view::npos
? line.size()
: comma2)
- comma1
- 1U));
if (comma2 != std::string_view::npos) {
entry.name = std::string(line.substr(comma2 + 1U));
}
devices.push_back(std::move(entry));
}
return devices;
}
} // namespace core