Fix audio-profile NVS persistence and whitespace-intolerant JSON parsers

- NvsAudioProfileStore used the key "audio_profile_json" (18 chars),
  exceeding NVS's 15-char key-name limit. Every nvs_set_str call failed
  silently with ESP_ERR_NVS_KEY_TOO_LONG (0x1109): applyProfile() always
  updated live DSP audio correctly, so the bug was invisible except as
  "store_failed" in the HTTP response and settings never surviving a
  reboot. Renamed to "audio_profile" (13 chars); confirmed live, EQ/mixer
  changes now persist across a reset.

- The hand-rolled JSON extractJsonString()/extractJsonBool() helpers
  (duplicated per-file: TunerJson, DspParamJson, StationListJson,
  WebRadioJson, WifiProvisionJson, AudioProfileJson, plus inline mac/name/
  save parsing in BluetoothJson) matched only the exact literal
  `"key":"value"` / `"key":true`, with no tolerance for a space after the
  colon. Standard JSON encoders (e.g. Swift's JSONEncoder in its default,
  non-compact mode) emit `"key": "value"`, which silently failed to parse
  as invalid_json/missing_field. Numeric fields were already fine
  (strtoul/strtof skip leading whitespace per the C standard); fixed only
  the string/bool extractors to skip whitespace after the colon before
  matching the value.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-24 14:55:11 +02:00
co-authored by Claude Sonnet 5
parent 38e768edbb
commit 7cb25be3f3
8 changed files with 157 additions and 53 deletions
@@ -58,15 +58,24 @@ namespace {
std::string_view key,
bool& out)
{
const std::string trueNeedle =
std::string("\"") + std::string(key) + "\":true";
const std::string falseNeedle =
std::string("\"") + std::string(key) + "\":false";
if (json.find(trueNeedle) != std::string_view::npos) {
const std::string needle =
std::string("\"") + std::string(key) + "\":";
const std::size_t needlePos = json.find(needle);
if (needlePos == std::string_view::npos) {
return false;
}
std::size_t start = needlePos + needle.size();
while (start < json.size()
&& (json[start] == ' ' || json[start] == '\t'
|| json[start] == '\r' || json[start] == '\n')) {
++start;
}
const std::string_view rest = json.substr(start);
if (rest.starts_with("true")) {
out = true;
return true;
}
if (json.find(falseNeedle) != std::string_view::npos) {
if (rest.starts_with("false")) {
out = false;
return true;
}
+52 -16
View File
@@ -150,12 +150,21 @@ parseBluetoothConnectJson(std::string_view json)
if (json.find('{') == std::string_view::npos) {
return std::unexpected(ParseError::InvalidJson);
}
const std::string needle = "\"mac\":\"";
const std::size_t start = json.find(needle);
if (start == std::string_view::npos) {
const std::string needle = "\"mac\":";
const std::size_t needlePos = json.find(needle);
if (needlePos == std::string_view::npos) {
return std::unexpected(ParseError::MissingField);
}
const std::size_t valueStart = start + needle.size();
std::size_t start = needlePos + needle.size();
while (start < json.size()
&& (json[start] == ' ' || json[start] == '\t'
|| json[start] == '\r' || json[start] == '\n')) {
++start;
}
if (start >= json.size() || json[start] != '"') {
return std::unexpected(ParseError::InvalidJson);
}
const std::size_t valueStart = start + 1U;
const std::size_t valueEnd = json.find('"', valueStart);
if (valueEnd == std::string_view::npos) {
return std::unexpected(ParseError::InvalidJson);
@@ -183,17 +192,35 @@ BluetoothConnectRequest parseBluetoothConnectRequest(std::string_view json)
if (auto mac = parseBluetoothConnectJson(json); mac) {
request.mac = std::move(*mac);
}
const std::string nameNeedle = "\"name\":\"";
const std::size_t nameStart = json.find(nameNeedle);
if (nameStart != std::string_view::npos) {
const std::size_t valueStart = nameStart + nameNeedle.size();
const std::string nameNeedle = "\"name\":";
const std::size_t nameNeedlePos = json.find(nameNeedle);
if (nameNeedlePos != std::string_view::npos) {
std::size_t nameStart = nameNeedlePos + nameNeedle.size();
while (nameStart < json.size()
&& (json[nameStart] == ' ' || json[nameStart] == '\t'
|| json[nameStart] == '\r' || json[nameStart] == '\n')) {
++nameStart;
}
if (nameStart < json.size() && json[nameStart] == '"') {
const std::size_t valueStart = nameStart + 1U;
const std::size_t valueEnd = json.find('"', valueStart);
if (valueEnd != std::string_view::npos) {
request.name.assign(json.substr(valueStart, valueEnd - valueStart));
request.name.assign(
json.substr(valueStart, valueEnd - valueStart));
}
}
request.save = json.find("\"save\":true") != std::string_view::npos
|| json.find("\"save\": true") != std::string_view::npos;
}
const std::string saveNeedle = "\"save\":";
const std::size_t saveNeedlePos = json.find(saveNeedle);
if (saveNeedlePos != std::string_view::npos) {
std::size_t saveStart = saveNeedlePos + saveNeedle.size();
while (saveStart < json.size()
&& (json[saveStart] == ' ' || json[saveStart] == '\t'
|| json[saveStart] == '\r' || json[saveStart] == '\n')) {
++saveStart;
}
request.save = json.substr(saveStart).starts_with("true");
}
return request;
}
@@ -219,13 +246,22 @@ parseBluetoothSpeakerJson(std::string_view json)
return std::unexpected(mac.error());
}
BtSpeakerTarget target{.mac = *mac, .name = {}};
const std::string nameNeedle = "\"name\":\"";
const std::size_t nameStart = json.find(nameNeedle);
if (nameStart != std::string_view::npos) {
const std::size_t valueStart = nameStart + nameNeedle.size();
const std::string nameNeedle = "\"name\":";
const std::size_t nameNeedlePos = json.find(nameNeedle);
if (nameNeedlePos != std::string_view::npos) {
std::size_t nameStart = nameNeedlePos + nameNeedle.size();
while (nameStart < json.size()
&& (json[nameStart] == ' ' || json[nameStart] == '\t'
|| json[nameStart] == '\r' || json[nameStart] == '\n')) {
++nameStart;
}
if (nameStart < json.size() && json[nameStart] == '"') {
const std::size_t valueStart = nameStart + 1U;
const std::size_t valueEnd = json.find('"', valueStart);
if (valueEnd != std::string_view::npos) {
target.name.assign(json.substr(valueStart, valueEnd - valueStart));
target.name.assign(
json.substr(valueStart, valueEnd - valueStart));
}
}
}
return target;
+13 -4
View File
@@ -24,12 +24,21 @@ namespace {
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) {
std::string("\"") + std::string(key) + "\":";
const std::size_t needlePos = json.find(needle);
if (needlePos == std::string_view::npos) {
return {};
}
const std::size_t valueStart = start + needle.size();
std::size_t start = needlePos + needle.size();
while (start < json.size()
&& (json[start] == ' ' || json[start] == '\t'
|| json[start] == '\r' || json[start] == '\n')) {
++start;
}
if (start >= json.size() || json[start] != '"') {
return {};
}
const std::size_t valueStart = start + 1U;
const std::size_t valueEnd = json.find('"', valueStart);
if (valueEnd == std::string_view::npos) {
return {};
@@ -24,12 +24,21 @@ namespace {
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) {
std::string("\"") + std::string(key) + "\":";
const std::size_t needlePos = json.find(needle);
if (needlePos == std::string_view::npos) {
return {};
}
const std::size_t valueStart = start + needle.size();
std::size_t start = needlePos + needle.size();
while (start < json.size()
&& (json[start] == ' ' || json[start] == '\t'
|| json[start] == '\r' || json[start] == '\n')) {
++start;
}
if (start >= json.size() || json[start] != '"') {
return {};
}
const std::size_t valueStart = start + 1U;
const std::size_t valueEnd = json.find('"', valueStart);
if (valueEnd == std::string_view::npos) {
return {};
+13 -4
View File
@@ -24,12 +24,21 @@ namespace {
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) {
std::string("\"") + std::string(key) + "\":";
const std::size_t needlePos = json.find(needle);
if (needlePos == std::string_view::npos) {
return {};
}
const std::size_t valueStart = start + needle.size();
std::size_t start = needlePos + needle.size();
while (start < json.size()
&& (json[start] == ' ' || json[start] == '\t'
|| json[start] == '\r' || json[start] == '\n')) {
++start;
}
if (start >= json.size() || json[start] != '"') {
return {};
}
const std::size_t valueStart = start + 1U;
const std::size_t valueEnd = json.find('"', valueStart);
if (valueEnd == std::string_view::npos) {
return {};
+28 -10
View File
@@ -24,12 +24,21 @@ constexpr std::string_view kHttpPrefix = "http://";
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) {
std::string("\"") + std::string(key) + "\":";
const std::size_t needlePos = json.find(needle);
if (needlePos == std::string_view::npos) {
return {};
}
const std::size_t valueStart = start + needle.size();
std::size_t start = needlePos + needle.size();
while (start < json.size()
&& (json[start] == ' ' || json[start] == '\t'
|| json[start] == '\r' || json[start] == '\n')) {
++start;
}
if (start >= json.size() || json[start] != '"') {
return {};
}
const std::size_t valueStart = start + 1U;
const std::size_t valueEnd = json.find('"', valueStart);
if (valueEnd == std::string_view::npos) {
return {};
@@ -40,15 +49,24 @@ constexpr std::string_view kHttpPrefix = "http://";
[[nodiscard]] bool extractJsonBool(std::string_view json,
std::string_view key, bool& out)
{
const std::string trueNeedle =
std::string("\"") + std::string(key) + "\":true";
const std::string falseNeedle =
std::string("\"") + std::string(key) + "\":false";
if (json.find(trueNeedle) != std::string_view::npos) {
const std::string needle =
std::string("\"") + std::string(key) + "\":";
const std::size_t needlePos = json.find(needle);
if (needlePos == std::string_view::npos) {
return false;
}
std::size_t start = needlePos + needle.size();
while (start < json.size()
&& (json[start] == ' ' || json[start] == '\t'
|| json[start] == '\r' || json[start] == '\n')) {
++start;
}
const std::string_view rest = json.substr(start);
if (rest.starts_with("true")) {
out = true;
return true;
}
if (json.find(falseNeedle) != std::string_view::npos) {
if (rest.starts_with("false")) {
out = false;
return true;
}
@@ -37,12 +37,21 @@ 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) {
const std::string needle = std::string("\"") + std::string(key) + "\":";
const std::size_t needlePos = json.find(needle);
if (needlePos == std::string_view::npos) {
return {};
}
const std::size_t valueStart = start + needle.size();
std::size_t start = needlePos + needle.size();
while (start < json.size()
&& (json[start] == ' ' || json[start] == '\t'
|| json[start] == '\r' || json[start] == '\n')) {
++start;
}
if (start >= json.size() || json[start] != '"') {
return {};
}
const std::size_t valueStart = start + 1U;
const std::size_t valueEnd = json.find('"', valueStart);
if (valueEnd == std::string_view::npos) {
return {};
@@ -26,7 +26,12 @@ namespace secure_store {
namespace {
constexpr char kTag[] = "NvsAudioProfileStore";
constexpr char kNamespace[] = "digiradio";
constexpr char kProfileKey[] = "audio_profile_json";
// NVS key names are capped at 15 chars (NVS_KEY_NAME_MAX_SIZE=16 incl. NUL);
// the previous "audio_profile_json" (18 chars) made every nvs_set_str call
// fail with ESP_ERR_NVS_KEY_TOO_LONG (0x1109), silently -- applyProfile()
// always updated live audio correctly but persistProfile() never actually
// wrote anything, so nothing survived a reboot.
constexpr char kProfileKey[] = "audio_profile";
} // namespace
bool NvsAudioProfileStore::hasProfile() const