Табло курсов для офисной кухни было моим самым обсуждаемым проектом: три человека в день проходили мимо и спорили про доллар. Первая версия печатала курс как 92.39850044 — я отдал на показ сырое значение float и никто не заметил, что после запятой восемь знаков. Вторая версия упала тише: я сменил источник API, новый присылал котировку строкой — "RUB":"92.4" — и моё чтение во float молча вернуло ноль. Табло неделю показывало нули, пока бухгалтерия не спросила, почему у меня доллар бесплатный.
Обе поломки — из одного корня: я не задумывался, в каком виде приходит число и сколько знаков у него имеет право быть на экране. С погодой мы такой разбор уже делали в прошлом уроке — там была матрёшка из вложенных объектов и кнопка «обновить». У курсов своя геометрия: словарь rates с десятками котировок, у каждой — свои знаки после точки, и вопрос «сколько печатать» решается не в API, а в вашем скетче.
Разберём три вещи: откуда брать курсы без ключа и регистрации, как читать вложенный rates и не ловить нули на строковых котировках, и сколько знаков после запятой показывать — с арифметикой float, где у неё граница, и одним правилом про деньги, которое экономит нервы крепче любого API. План урока намеренно повторяет маршрут погодной панели — там мы оттачивали стыки, здесь то же самое на другом пакете: чем меньше новых механизмов, тем лучше видно, что общего у всех API.
Откуда брать курсы: API без ключа
Хорошие новости для настольного проекта: есть источники без регистрации и ключей. Открытый open.er-api.com/v6/latest/USD отдаёт котировки к доллару сразу во все валюты, обновление раз в сутки; курсы Центробанка в JSON отдаёт зеркало cbr-xml-daily.ru — структура повторяет официальную: имя валюты, номинал, курс. Ключ не нужен ни там, ни там.
Геометрия ответа: {"result":"success","base_code":"USD","rates":{"RUB":92.404,"EUR":0.921,"CNY":7.24,...}} — котировки лежат во вложенном объекте rates, по ключу-коду валюты. На плате чтение цепочкой: doc["rates"]["RUB"] — тот же приём, что с main.temp в погодном уроке. У зеркала ЦБ есть своя особенность: рядом с курсом приходит номинал — для большинства валют он равен единице, но для турецкой лиры и южноафриканского рэнда номинал будет десять, и делить надо самому. Валют в словаре больше полутора сотен, но документ тянет только то, что вы прочитали: размер ответа около десятка килобайт, поэтому для ESP32 это прогулка, а для Uno с её 2 КБ SRAM — уже не по зубам, тут честно нужен ESP32 или фильтр.
Грабля номер один — протокол. У большинства источников только HTTPS: у ESP32 это решается клиентом с шифрованием из урока про WiFiClientSecure, у классической Uno без сетевого модуля — никак. er-api — из тех, кто отвечает и по HTTP: удобный первый кандидат для табло, но проверяйте это в момент написания, источник вправе закрыть открытый протокол в любой день.
Грабля номер два — тип значения. Код валюты приходит строкой, курс — числом, но части API шлют курс строкой в кавычках: "RUB":"92.4". Чтение такой строки во float даст ноль без единой жалобы — тип решает, как значение читается, и это мы разбирали в уроке про синтаксис JSON. Страховка простая: распечатайте сырой ответ в Serial при первом запуске и посмотрите глазами, в кавычках ли ваши котировки.
Сколько знаков печатать: арифметика float
float хранит около семи значащих цифр. Курс 92.4045 — семь цифр, помещается; курс 92.40452618 — десять, хвост float уже переврал, и перевранный хвост вычислений не спасёт никакая печать. Для показа этого хватает с запасом: табло живёт на двух-четырёх знаках после точки. Для арифметики с деньгами — нет, и об этом ниже в жёлтой рамке. Отсюда же правило выбора платы: чем больше валют вы тянете в переменные, тем быстрее упираетесь в границу типа — четырёх байтов float хватает курсу, но не их сумме.
Печать числа знаков — второй аргумент Serial.print: Serial.print(rate, 2) даст 92.40, Serial.print(rate, 4) — 92.4045. По умолчанию печатается два знака: Serial.print(92.4) — это 92.40. Отдельная тонкость с сериализацией: ArduinoJson при сборке своего JSON печатает дробные в кратчайшей форме — 92.4 останется 92.4 без хвостовых нулей, библиотека подбирает число знаков сама. Поэтому один и тот же курс в логе Serial выглядит как 92.40, а в собранном документе — как 92.4: оба верные, просто это две разные печати с разными правилами.
Практическое правило для табло: два знака для больших денег (доллар, евро), четыре для «дешёвых» валют, где копейки значимы — сто йен меньше двух рублей, и там четвёртый знак живой. Если же из курсов считается что-то дальше — цена пересчёта, суммы — переводите в целые: храните копейки как int, делите на сто только в момент печати. Так работают кассы, и это правило из мира «настоящих» денег, а не Arduino-игрушек.
И про частоту: курсы ЦБ меняются раз в сутки, er-api обновляется раз в сутки, значит опрашивать раз в час — уже роскошь с запасом. Между опросами табло показывает последние известные значения: сорванный пакет — не повод обнулять экран, это повод молча держать старые курсы. Таймер ставится на millis() без delay, как в уроке про millis вместо delay, — delay на час в loop похоронит всю остальную логику скетча.
#include <WiFi.h>#include <HTTPClient.h>#include <ArduinoJson.h> const char* WIFI_SSID = "HomeWiFi";const char* WIFI_PASS = "parol";// er-api отвечает и по HTTP, ключ не нужен; у большинства// других источников только HTTPS — см. урок про WiFiClientSecure:String url = "http://open.er-api.com/v6/latest/USD"; JsonDocument doc;unsigned long lastFetch = 0;const unsigned long PERIOD = 3600000UL; // час: курсы меняются раз в сутки void fetchRates() { HTTPClient http; http.begin(url); int code = http.GET(); http.end(); if (code != 200) return; // пакета нет — держим прежние значения DeserializationError err = deserializeJson(doc, http.getString()); if (err) return; float rub = doc["rates"]["RUB"] | 0.0; // | — запасное значение, если float eur = doc["rates"]["EUR"] | 0.0; // ключа нет: увидим 0.0 сознательно float cny = doc["rates"]["CNY"] | 0.0; Serial.print("RUB="); Serial.println(rub, 4); Serial.print("EUR="); Serial.println(eur, 4); Serial.print("CNY="); Serial.println(cny, 4);} void setup() { Serial.begin(115200); WiFi.begin(WIFI_SSID, WIFI_PASS); while (WiFi.status() != WL_CONNECTED) { delay(400); Serial.print("."); } fetchRates(); lastFetch = millis();} void loop() { if (millis() - lastFetch >= PERIOD) { lastFetch = millis(); fetchRates(); }}| Печать | Что увидите | Где применяю |
|---|---|---|
| Serial.print(rate) | 92.40 — по умолчанию два знака | быстрый лог при отладке |
| Serial.print(rate, 4) | 92.4040 — четыре знака | дешёвые валюты, где копейки значимы |
| serializeJson(doc, Serial) | 92.4 — кратчайшая форма, знаки подбирает библиотека | передача курса дальше по сети |
| int k = rate * 100 | 9240 — целые копейки | любая арифметика с деньгами |
Между опросами: что показывает табло, пока сервер молчит
Табло живёт по простому циклу: прочитали — показали — держим. Значения курсов сидят в обычных переменных, и сорванный пакет их не трогает: экран не обнуляется, просто показываются те же цифры, что и час назад. Правило «сорванный пакет молчит» я вывел после недели нулей из вступления: любой сбой приёма остаётся событием Serial-лога, а не событием экрана — посетитель табло не должен видеть его провалы. Заметьте, как это отличается от погодной панели из прошлого урока: там при сбое экран продолжал показывать старую погоду, и это тоже было правильным поведением, просто проверка кода стояла раньше.
Если табло обязано переживать перезагрузку — последний набор курсов пишется в EEPROM раз в сутки: update запишет байты только при изменении, и ресурс ячеек не съестся. Как float раскладывается по байтам и читается обратно — разобрано в уроке про float и байты EEPROM. Для ESP32 путь короче — последний ответ можно писать в файл, но идея та же: показания суточной давности честнее нулей.
Вывод на дисплей — отдельная дисциплина: если звать lcd.clear() при каждом обновлении, экран моргает; лечение разбирали в уроке про моргание и тик таймера. Обновляйте только изменившиеся позиции и только по событию «пришли новые данные». На две строки по 16 символов помещаются четыре курса в формате USD 92.40 — моя разметка влезала в 32 ячейки впритык, и это ещё один аргумент за два знака вместо четырёх.
И кнопка по требованию: рядом с таймером на плате живёт кнопка «обновить сейчас» — та же, что на погодной панели. Она не заменяет интервал, а дополняет: сервер ждёт ваш опрос, но визитор, пришедший к табло, ждать час не обязан.
- Порог в действии. Поменяйте условие на usd > 90.0 и запустите: светодиод на D12 загорится — сигнализация перешла из мёртвой ветки в живую.
- Строка вместо числа. Замените в пакете 92.4 на "92.4" (в кавычках): usd= станет 0.00 — тип решает, как читается значение, и это та самая грабля с серверной стороны.
- Молчаливый ключ. Вырежьте из пакета пару с cny вместе с запятой: печать cny не изменится? Нет — код читает только usd и eur; добавьте чтение doc.cny и увидите ноль по отсутствующему ключу.
- На настоящей плате. Возьмите code-блок с doc["rates"]["RUB"]: вложенный словарь котировок движок не читает — на плате цепочка работает, и запасное значение через | подставится само.
- Курсы без ключа: open.er-api.com/v6/latest/USD и зеркало ЦБ cbr-xml-daily.ru; у большинства источников только HTTPS — для ESP32 это WiFiClientSecure
- Котировки лежат во вложенном объекте rates: doc["rates"]["RUB"]; если курс пришёл строкой "92.4", чтение во float молча даст ноль
- float держит около семи значащих цифр: для показа двух-четырёх знаков хватает, для арифметики с деньгами — нет
- Serial.print(rate, 2) печатает два знака; ArduinoJson при сериализации подбирает кратчайшую форму сам — 92.4 останется 92.4
- Курсы меняются раз в сутки: опрос раз в час с запасом; сорванный пакет — держим прежние значения, а не обнуляем табло
- 1. API прислал "RUB":"92.4" — курс строкой в кавычках. Что даст float rub = doc["rates"]["RUB"]?
- 2. Что напечатает Serial.print(92.4) без второго аргумента?
- 3. Курс доллара лежит во вложенном объекте rates. Как читается на плате?
- 4. Пакет с курсами не пришёл: GET вернул 500. Что показывает грамотное табло?
- 5. Почему для показа курса хватает float, а для сумм конвертации — нет?
Табло готово: пакет, словарь котировок, печать с нужным числом знаков и сигнализация на пороге. Третий проект раздела разворачивает стрелку: теперь не мы читаем чужой JSON, а чужой сервер читает наш. Соберём тело POST-запроса через документ — с Content-Length по замеру, правильным Content-Type и разбором ответа, который сервер пришлёт на нашу телеметрию. Если ваши узлы дорастут до десятков сообщений в минуту — собранный здесь JSON уезжает в MQTT-топик без изменений, поменяется только транспорт.
Частые вопросы по теме урока
- API прислал "RUB":"92.4" — курс строкой в кавычках. Что даст float rub = doc["rates"]["RUB"]?
- Тип решает, как читается значение: строка в кавычках при чтении во float даёт ноль без единого сообщения. Смотрите сырой ответ глазами при первом запуске. Ноль — молча, тип не совпал
- Что напечатает Serial.print(92.4) без второго аргумента?
- По умолчанию печатаются два знака после точки: 92.40. Число знаков задаётся вторым аргументом — print(rate, 4) даст 92.4000. 92.40
- Курс доллара лежит во вложенном объекте rates. Как читается на плате?
- Котировки живут внутри объекта rates, поэтому цепочка двух обращений: сначала rates, потом код валюты. Тот же приём, что с main.temp в погодном API. doc["rates"]["RUB"]
- Пакет с курсами не пришёл: GET вернул 500. Что показывает грамотное табло?
- Значения читаются в переменные только после успешного разбора; при сбое старые показания остаются на месте. Обнуление экрана на живом источнике — путь к неделе нулей, как у меня. Последние известные значения — сорванный пакет их не трогает
- Почему для показа курса хватает float, а для сумм конвертации — нет?
- Семь значащих цифр покрывают курс целиком, но накопления и умножения в деньгах накапливают расхождение в дальних знаках. Суммы считают в целых копейках. float держит около семи значащих цифр — показ живёт на двух-четырёх, а копейки в суммах требуют целых