Сервис влажности для теплицы отдавал три значения за утро: {"city":"Omsk","h24":[54,62,71]}. Я разложил их тремя строками — int h0 = doc["h24"][0]; int h1 = doc["h24"][1]; int h2 = doc["h24"][2]; — и на месяц успокоился: график строился по индексам 0, 1 и 2 — ровно как в коде. В сентябре сервис добавил четвёртый час: скетч молча продолжал строить по трём, а четвёртое значение ложилось на пол.
Через неделю ударило вторым: ночью сервис начал слать и два значения вместо трёх. Чтение doc["h24"][2] за пределами длины вернуло ноль — а ноль для влажности значение легитимное, график честно нарисовал сухой час в два часа ночи. Две недели журнал показывал росу при влажности ноль процентов, и я искал подвох в датчике, пока не открыл сырой ответ сервера: длина массива — не моя константа, а решение сервера. Правильная версия скетча, как я понял позже, вообще не должна знать длину заранее: границу цикла даёт сам документ.
Разберём устройство JSON-массива и нумерацию с нуля, как узнать длину на плате через size() и чем полезен range-for, что вернёт чтение за границей массива и почему ноль за границей опаснее всего там, где он легитимен. В симуляторе итерация настоящая — цикл переберёт значения из разобранного ответа, а монитор порта позволит спросить конкретный час. Порядок такой: сначала механика ряда на плате, потом те же движения в симуляторе — совпадают они до последнего символа, кроме одного места, где движок короток, и это место я покажу честно.
Массив: ряд значений с нумерацией
Массив JSON — значения в квадратных скобках через запятую: [54,62,71]. Нумерация с нуля: [0] — первое, [2] — третье. Массив сам — значение ключа: doc["h24"] — ряд целиком, а doc["h24"][0] — выбор места в ряду. Запятых внутри значений не бывает — разделитель ряда виден сразу, и при чтении ответа глазами это единственная пунктуация, о которой стоит помнить. Элементом может быть что угодно из синтаксиса: число, строка, булево, а на следующем уровне — объект; общую структуру формата мы рисовали в уроке про JSON без боли.
Длина — вопрос к документу, не к памяти. На плате её даёт doc["h24"].size(): сколько значений сервер положил в этот раз. Второй способ обхода — range-for: for (JsonVariant v : doc["h24"].as<JsonArray>()) — элемент за элементом без индексов и без ручной границы. Когда нужны только значения — сумма, максимум, среднее — он короче; когда нужен номер часа — остаётся индексный цикл. В шестой версии библиотеки обход выглядел так же, только документ был StaticJsonDocument с ручной ёмкостью — переезд на седьмую ничего не ломает, мы его разбирали в уроке про JsonDocument v7.
Граница — главная грабля ряда. Индекс за длиной возвращает null-прокси: молчаливый ноль для числа, пустоту для строки. Для температуры за окном это сразу заметно, для влажности, осадков и счётчиков — нет: ноль легитимен, и отличить «сервер прислал два значения» от «третье значение равно нулю» по чтению невозможно. Поэтому цикл я всегда веду до size(), а не до числа, которое помню со вчерашнего запуска; сравнение соседних элементов — doc["h24"][i] с doc["h24"][i-1] — живёт в том же цикле, так ищут рост и падение между часами. Правила запасных значений — в уроке про оператор |.
Индексы, сумма и деление с точкой
Типовой обход складывается из трёх частей: граница от size(), чтение элемента в переменную с понятным типом и накопление итогов. int sum копится в цикле, максимум ищется сравнением — if (v > mx) mx = v; — и всё это живёт на локальных переменных итерации, как в любом цикле из урока про for и while. Максимум до цикла я ставлю не нулём, а первым элементом ряда: ряд отрицательных температур с максимумом-нулём даёт неверный ответ, старт с первого элемента снимает вопрос.
Среднее считаю с точкой: float avg = sum / 3.0 — литерал с точкой делает деление вещественным. Записать sum / 3 — значит отбросить хвост: 187 / 3 на целых даст 62 вместо 62.33, и график уедет на треть процента. Правила приведения — в уроке про типы переменных, здесь достаточно точки. На плате это проверяется одной строкой: Serial.println(sum / 3) против Serial.println(sum / 3.0) — 62 и 62.33 на одном и том же ряду значений.
Порядок печати тоже часть отладки: пары имя=значение (v=63) читаются в логе лучше голых чисел — графики и фильтры Serial-монитора цепляются за имена. На 9600 бод строка в двадцать символов стоит около двадцати миллисекунд, поэтому в горячем цикле печать каждого элемента я оставляю только на отладке: обход без печатей молчит, зато с печатями видно и порядок, и пропуски.
#include <ArduinoJson.h> JsonDocument doc; void setup() { Serial.begin(9600); String resp = "{\"city\":\"Omsk\",\"h24\":[54,62,71]}"; if (deserializeJson(doc, resp)) return; JsonArray h = doc["h24"]; // ссылка на массив; данные живут в doc int n = h.size(); // длина от сервера, не от моей памяти Serial.print("Elementov: "); Serial.println(n); int sum = 0; int mx = 0; for (int i = 0; i < n; i++) { // индексный обход: нужны и номер, и значение int v = h[i]; sum += v; if (v > mx) mx = v; Serial.print("chas "); Serial.print(i); Serial.print(": "); Serial.println(v); } Serial.print("Srednee: "); Serial.println(sum / (float)n); // точка — хвост не отбрасывается Serial.print("Maksimum: "); Serial.println(mx); // Тот же ряд вторым способом — range-for, когда индекс не нужен: for (JsonVariant v : h) { // v — очередной элемент; здесь его печатают или копят в итоги }} void loop() {}Итерация в симуляторе
В симуляторе итерация по массиву настоящая: doc.h24[i] с переменной i внутри for перебирает значения разобранного ответа, сумма и сравнения считаются честно. Скобочная форма чтения в движке не эмулируется — doc["h24"][i] вернёт ноль; точечная doc.h24[i] соответствует плате, где чаще пишут скобки, — оговорка стоит в комментарии скетча. Размер массива движок не замеряет, поэтому граница цикла задана константой: на плате её место занимает size(), и это место отмечено.
Данные для цикла берутся из настоящего разбора: строка ответа лежит прямо в скетче, deserializeJson наполняет документ, и doc.h24 после этого — честный ряд из трёх значений. Поменяли строку — поменялся ряд; в этом вся сила работы через документ: цикл не знает, откуда пришёл ряд, ему важна граница и значения.
Скетч интерактивен: после печати обхода и среднего пошлите в монитор порта 0, 1 или 2 — скетч ответит влажностью конкретного часа и не полезет за границу: на 5 ответит списком допустимых часов. Проверка границы стоит до чтения — тот же порядок, что на плате. Поменяйте значения в строке ответа — среднее и вердикты пересоберутся; добавьте четвёртое значение и поправьте границу — так вручную повторяется работа size(). И второе: запрос несуществующего часа — лучший способ убедиться, что проверка границы стоит до чтения, а не после.
Приём «спросить элемент из порта» собирается из двух навыков: чтение строки по ограничителю — из урока про команды по Serial, перевод в число — из урока про parseInt. Движок понимает оба, поэтому интерактив в симуляторе настоящий: строка из поля ввода приходит с переводом строки, чтение и сравнение отрабатывают как на плате. Команды можно слать подряд — чтение до ограничителя съедает и терминатор, фантомных повторных чтений нет.
- Четвёртый час. Допишите в массив значение через запятую: [54,62,71,58] — цикл останется на трёх, четвёртое молча не попадёт: так выглядит граница-константа. Поменяйте i < 3 на i < 4 и сравните среднее.
- Росли к полудню. Поставьте значения [48,55,70] — вердикты «выше среднего» сместятся к последнему часу; сравнения пересчитываются от строки ответа.
- Спросить час. Пошлите в монитор 1, потом 5: первый ответ вернёт значение, второй — список допустимых часов. Граница проверяется до чтения, ноль из-за предела не пролезет.
- На настоящей плате. Замените константу 3 на int n = doc["h24"].size(); — и скетч переживёт и четвёртый час, и ночной ряд из двух значений без правки кода.
| Способ обхода | Когда беру | Подвох |
|---|---|---|
| индексный for до size() | нужны и номер, и значение: график, сравнение соседних | граница вручную: size() вместо константы, иначе хвост теряется |
| range-for по JsonArray | нужны только значения: сумма, максимум | индекса нет — «к какому часу» уже не ответить |
| ручная раскладка [0], [1], [2] | одноразовый скетч под стабильный ответ | сервер меняет длину молча — мой сентябрьский график по трём часам |
| doc["arr"].size() | диагностика: сервер шлёт разную длину | в симуляторе не эмулируется — там константа с честной пометкой |
- Массив JSON — значения в квадратных скобках с нумерацией от нуля; сам массив — значение ключа: doc["h24"][0]
- Длину даёт doc["arr"].size(): сервер меняет число элементов молча, константа из прошлого запуска теряет хвост
- Второй способ — range-for по JsonArray: элемент за элементом без индексов; когда нужен номер часа — индексный цикл
- Индекс за границей молча даёт ноль: опасно там, где ноль легитимен — влажность, осадки, счётчики
- Среднее с хвостом — вещественное деление: sum / 3.0 вместо sum / 3
- В симуляторе итерация настоящая: doc.h24[i] в цикле; длина — константа с пометкой, на плате — size()
- 1. Как на плате узнать, сколько значений сервер положил в массив h24?
- 2. В массиве три значения. Что вернёт doc["h24"][5] на плате?
- 3. Что делает range-for: for (JsonVariant v : doc["h24"].as<JsonArray>())?
- 4. Почему среднее считается как s / 3.0, а не s / 3?
- 5. Что в симуляторе этого урока настоящее, а что — упрощение?
Ряд значений перебирается осознанно: длина — из документа, граница — до чтения, среднее — с точкой. Дальше по разделу — самый частый формат списков у API: массив объектов, где каждый элемент несёт свой набор полей, — прогноз на три дня раскладывается по переменным за один цикл. Приём из этого урока — вопрос конкретного элемента через порт — собирается из навыков командных скетчей и пригодится и там: спрошу день, получу вердикт.
Частые вопросы по теме урока
- Как на плате узнать, сколько значений сервер положил в массив h24?
- size() возвращает длину массива в документе. Сервер меняет длину без уведомлений — читать её надо из документа, а не держать константой: константа из прошлого запуска теряет хвост ряда. doc["h24"].size()
- В массиве три значения. Что вернёт doc["h24"][5] на плате?
- Индекс за границей даёт null-прокси: ноль для числа, пустота для строки. Для полей, где ноль легитимен, это незаметная подмена данных — влажность, осадки, счётчики. Ноль — молча
- Что делает range-for: for (JsonVariant v : doc["h24"].as<JsonArray>())?
- Range-for перебирает значения; индекса нет, поэтому «к какому часу относится значение» внутри него не ответить — для этого индексный цикл до size(). Отдаёт элементы по одному без индексов
- Почему среднее считается как s / 3.0, а не s / 3?
- Деление двух целых отбрасывает хвост. Литерал с точкой делает выражение вещественным — дробная часть сохраняется, и график не уезжает на треть процента; проверяется двумя println на одном ряду. Точка делает деление вещественным: 187/3 даёт 62.33, а не 62
- Что в симуляторе этого урока настоящее, а что — упрощение?
- doc.h24[i] с переменной i в цикле движок выполняет по-настоящему; размер массива не замеряется, поэтому граница — константа с пометкой, где на плате стоит size(). Итерация по массиву настоящая; длину задаёт константа — size() не эмулируется