Arduino + ESP-01, работаем с API

Соединить Arduino и EPS-01 само по себе проблема, а взаимодействие с API находится на грани возможностей платформы.

Подключение

Если вы работаете с Arduino Mega, лучше использовать подключение по Serial2. C Arduino Uno я использовал Software Serial, что дает менее стабильное, но вполне рабочее соединение.

Многие, при подключении RX/TX, рекомендуют использовать конвертеры логических уровней с 5В на 3.3В. Я пробовал с конвертером и без, разницы не увидел. В процессе экспериментов от конвертора уровней отказался совсем. У меня ничего не сгорело и вполне себе работает.

Очень важно питание 3.3В для ESP-01. Без мощного источника питания, добиться стабильной работы платы очень сложно. При необходимости, конечно, можно использовать разъем 3.3В на плате Arduino, но его мощности не хватает на прожорливый чип ESP32. В этом случае, выправить ситуацию помогает параллельное подключение конденсатора. Однако, лучше использовать отдельный источник питания, например, на основе LM2596.

Прошивка

Как правило, из Китая платы EPS-01 приходят с очень древними прошивками. Поэтому их точно придется перепрошивать. Тут есть тонкость. Если вы будете использовать прошивку до версии 1.7, то для работы с платой лучше взять библиотеку bportaluri/WiFiEsp. К сожалению, она давно не обновлялась, и автор остановил ее поддержку. Для прошивок, начиная с версии 1.7, используется библиотека jandrassy/WiFiEspAT.

Удобнее всего для перепрошивки ESP-01 использовать переходник USB-UART: втыкаете в него модуль, подключаете к компьютеру. Для перепрошивки я использовал Flash Download Tool. Саму прошивку ставил версии 1.7:

11:06:35.843 -> AT+GMR
11:06:35.843 -> AT version:1.7.1.0(Jul 15 2019 16:58:04)
11:06:35.877 -> SDK version:3.0.1(78a3e33)
11:06:35.911 -> compile time:Feb 14 2020 09:19:42

На всякий случай, прикрепляю архив с прошивкой. Найти его не так просто, как хотелось бы. Инструкцию по перепрошивке использовал на английском, но сейчас появилось много статей на русском. После перепрошивки, для более стабильной работы, лучше выставить скорость обмена на 9600 бод (команда AT+UART_DEF=9600,8,1,0,0).

Скетч

При работе с веб API, главная сложность – малый объем памяти как на ESP, так и на Ардуино. Старые прошивки и библиотека bportaluri/WiFiEsp плохо обрабатывают объемные входные данные. Поэтому, дальше будет идти речь о прошивке AT 1.7 и библиотеке jandrassy/WiFiEspAT. 

Для экономии памяти постарайтесь не использовать глобальные переменные и сократить количество используемых библиотек. Желательно отказаться от использования типа данных String и работать со строками как массивом char.

Работа с API, как правило, подразумевает работу с JSON. Т.е. мы делаем запрос серверу, а он нам возвращает текст в формате JSON. JSON обычно начинается с фигурных скобок. Поэтому мы можем ограничить запоминание ответа сервера фигурными скобками. Кроме того, нам не обязательно брать весь JSON, в большинстве случаев необходима лишь его часть. Для экономии памяти допустимо пропустить первые 100 символов, а затем взять текст не до конца, а только следующие 300 символов.

Для тестирования API на компьютере удобно использовать бесплатную программу Postman. В качестве примера мы будем делать запрос на API получения текущего времени: GET http://worldtimeapi.org/api/timezone/Europe/Moscow

Это простое API, которое не требует дополнительных параметров в запросе. Сделав запрос мы получим примерно такой ответ:


{
    "abbreviation": "MSK",
    "client_ip": "000.000.000.000",
    "datetime": "2022-05-09T22:08:31.713676+03:00",
    "day_of_week": 1,
    "day_of_year": 129,
    "dst": false,
    "dst_from": null,
    "dst_offset": 0,
    "dst_until": null,
    "raw_offset": 10800,
    "timezone": "Europe/Moscow",
    "unixtime": 1652123311,
    "utc_datetime": "2022-05-09T19:08:31.713676+00:00",
    "utc_offset": "+03:00",
    "week_number": 19
}

В нем нам интересно только поле utc_datetime, которое содержит время по Гринвичу в строго заданном формате. Разберем пример скетча, который по API определяет текущее время. 


#include "SoftwareSerial.h"
#include "WiFiEspAT.h"
#define JSON_SIZE 300 // Определяем количество байт, которое мы берем из ответа сервера
int response_offset = 100; // Сколько байт необходимо пропустить из ответа сервера
WiFiClient client;

char ssid[] = "xxx"; // имя сети WiFi
char pass[] = "xxx"; // пароль сети WiFi
int status = WL_IDLE_STATUS; // статус сети WiFi

SoftwareSerial WiFiSerial(7, 6); // Подключаем ESP01. Соответственно: RX, TX


void setup() {
  Serial.begin(9600);
  WiFiSerial.begin(9600);
  WiFi.init(WiFiSerial);

  // проверяем, что ESP01 подключилась
  if (WiFi.status() == WL_NO_MODULE) {
    Serial.println("WiFi shield not present");
    while (true);
  }
  connectToWiFi();
  getNetworkTime(); // Вызываем функцию определения времени
}

void loop() {
  getServerResponse(); // Читаем ответ сервера
}

// Функция обращения к API
void getNetworkTime() { 
  // Используем API http://worldtimeapi.org/api/timezone/Europe/Moscow
  char* server = "worldtimeapi.org";
  response_offset = 100;

  if (client.connect(server, 80)) {
    Serial.println("Connected to server");
    // Делаем HTTP запрос
    client.println("GET /api/timezone/Europe/Moscow HTTP/1.1");
    client.print("Host: ");
    client.println(server);
    client.println("Connection: close");
    client.println();
    client.flush();
  }
}

// Функция парсит время из массива JSON
void parseTimeFromJson(char json[JSON_SIZE])
{
  int start = 0;
  char text[5]; // служебная переменная, для преобразования символов в числа
  int seconds, minutes, hour, day, month, year = 0;

  // посимвольно обрабатываем массив с ответом сервера
  for (int i = 0; i <= JSON_SIZE-10; i++) {
    char need[] = "utc_d"; // Ищем поле utc_datetime
    char substr[5] = {json[i], json[i+1], json[i+2], json[i+3], json[i+4]};
    // Если встречаем нужную нам последовательность символов
    // сохраняем позицию в массиве и прерываем цикл
    if (strcmp(need, substr) == 0) {
      start = i+15;
      break;
    }
  }

  Serial.print("Start: ");
  Serial.println(start);

  text[0] = json[start+11];
  text[1] = json[start+12];
  hour = atoi(text) + 3;

  text[0] = json[start+14];
  text[1] = json[start+15];
  minutes = atoi(text);

  text[0] = json[start+17];
  text[1] = json[start+18];
  seconds = atoi(text) + 1;

  text[0] = json[start+8];
  text[1] = json[start+9];
  day = atoi(text);

  text[0] = json[start+5];
  text[1] = json[start+6];
  month = atoi(text);

  text[0] = json[start];
  text[1] = json[start+1];
  text[2] = json[start+2];
  text[3] = json[start+3];
  year = atoi(text);

  Serial.print("Parse: ");
  Serial.print(hour);
  Serial.print(minutes);
  Serial.print(seconds);
  Serial.print(" - ");
  Serial.print(day);
  Serial.print(month);
  Serial.println(year);
}

// Функция считывает ответ сервера
void getServerResponse() {
  int pos = 0; // Номер текущего символа из ответа сервера
  bool json_flag = false; // Флаг, что ответ содержит данные в формате json
  char json[JSON_SIZE]; // Массив c частью ответа сервера

  while (client.available()) {
    // посимвольно читаем ответ сервера
    char c = client.read();
    // Нужная нам часть ответа сервера начинается с фигурных скобок
    if (c == '{') { 
      json_flag = true;
    }
    if (json_flag) {     
      pos++;
    }

    // Сохраняем часть ответа сервера
    if (json_flag && (pos > response_offset)) {
      json[pos - response_offset] = c;
    }

    // После того, как нужная нам часть ответа была сохранена - прерываем считывание
    if (pos == (JSON_SIZE + response_offset - 1)) {
      json_flag = false;
      break;
    }
  }

  // Если ответ сервера был удачно считан
  if (pos > 0) {
    client.stop();

    Serial.print("JSON: ");
    Serial.println(json);

    parseTimeFromJson(json);
  }
}

При экспериментах с API, важно ограничивать количество запросов к серверу. В противном случае, вам могут автоматически отказать в доступе. Ограничения по запросам для каждого API индивидуально. Но исходите из того, что лучше не делать более 1 запроса в секунду.

Работа с API не ограничивается получением текущего времени. Через API можно получать практически любую информацию: погоду, новости, курсы валют, информацию о состоянии серверов и удаленных устройств и т.п.

09.05.2022