Варианты протокола для обмена данными между Arduino и ESP8266

У меня есть устройство, построенное на базе Arduino uno:

  • Программное обеспечение Arduino, установленное на Arduino uno

  • можно управлять с помощью последовательных команд

  • можно управлять с помощью физических кнопок и датчиков

  • при изменении состояния любой кнопки/датчика текущее состояние записывается в последовательный порт

  • если в течение 5 секунд не было отправлено ни одного сообщения, будет отправлено последовательное сообщение об отсутствии изменений


Что нужно:

  • Используйте ESP8266, чтобы обеспечить мост между текущим программным обеспечением Arduino и MQTT/web

  • Я могу запрограммировать ESP8266 в качестве веб-сервера, клиента MQTT и т. д. с помощью Arduino IDE или Lua (но я предпочитаю Arduino IDE, поскольку я могу повторно использовать части кода для генерации/интерпретации связи).

  • ESP8266 справится со всем, что требуется для Wi-Fi/Интернета/MQTT; без модуля MQTT часть Arduino будет работать автономно, не будет только пульта дистанционного управления.

  • Я хотел бы внести минимальные изменения в код Arduino (или вообще ничего, если возможно). Любые изменения потребуют повторного тестирования, чего я стараюсь избегать.

  • в некоторых установках может отсутствовать ESP8266.


Какие варианты я нашел:

  1. Серийный

ESP8266 может считывать последовательный вывод и быть мостом между веб/MQTT и последовательным, будет хранить текущее состояние в памяти для отправки по запросу, чтобы избежать опроса устройства каждый раз.

Одним из преимуществ является то, что для части Arduino не требуется никаких изменений кода или тестирования.

  1. I2C

Назначьте Arduino ведущим устройством I2C, а ESP8266 — подчиненным (или наоборот) и реализуйте двунаправленную связь. Идея появилась после прочтения этой темы.


Некоторая другая информация о последовательных командах:

Пакет данных (команда или описание состояния) содержит от 1 до 20 символов с возможным пиковым значением 20 пакетов за 5 секунд и в среднем одним пакетом каждые 3 секунды. При необходимости я могу заставить его отправлять 5 целых чисел без знака вместо буквенно-цифровых символов.

Если требуется больше контактов I2C/serial, я могу перейти на Arduino Mega (поэтому количество свободных контактов не является проблемой).

Есть ли другие варианты для этого? (протоколы, готовые библиотеки для последовательной связи и т. д.). Я стараюсь не изобретать велосипед..

Спасибо за ваше время!

, 👍5

Обсуждение

*Firmata* (https://github.com/firmata/protocol/blob/master/protocol.md) — один из таких существующих протоколов связи. Однако неясно, чего вы хотите - вы уже определили и построили последовательный протокол, и вы говорите, что не хотели бы изменять этот код... Если это так, просто закодируйте свой ESP, чтобы получить последовательные данные, проанализировать их и отправить в веб/MQTT-сервер., @KennetRunner


2 ответа


4

В большинстве учебных пособий по I2C каждая плата Arduino рассматривается как подчиненная и главная, но этот подход лучше, потому что каждый Arduino является либо ведущим, либо ведомым (но не обоими), и переключение не требуется. Это упрощает задачу.

I2C лучше, чем последовательный, потому что вы можете добавить больше Arduino на одну шину.

Я реализовал I2C между двумя Arduino, и это не сложнее, чем чтение/запись в последовательный порт (что вы уже сделали). И я уверен, что вы можете обобщить свой код последовательного порта для работы как с последовательными, так и с I2C-коммуникациями.

Это мой пример (просто доказательство концепции). Ведомый Arduino управляет некоторыми контактами, темп. датчик и сторожевой таймер по приказу хозяина Arduino. Если ведомое устройство не получает такт вовремя, оно сбрасывает ведущее устройство Arduino.

Мастер-код

#include <Wire.h>

#define CMD_SENSOR    1
#define CMD_PIN_ON    2
#define CMD_PIN_OFF   3
#define CMD_LUMEN     4
#define CMD_BEAT      5

const byte SLAVE_ADDRESS = 42;
const byte LED = 13;

char buffer[32];

void setup ()
{
  Serial.begin(9600);
  Serial.println("Master");

  Wire.begin ();

  pinMode (LED, OUTPUT);
  digitalWrite(LED, HIGH);
  delay(1000);
  digitalWrite(LED, LOW);

  Wire.beginTransmission (SLAVE_ADDRESS);
  Serial.println("Send LED on");
  Wire.write (CMD_PIN_ON);
  Wire.write (2);
  Wire.write (10);
  Wire.endTransmission();
  int x = Wire.requestFrom(SLAVE_ADDRESS, 1);
  Serial.print("status=");
  Serial.println(x);
}  // конец настройки

void loop ()
{
  Serial.println(".");
  Wire.beginTransmission (SLAVE_ADDRESS);
  Wire.write (CMD_SENSOR);
  Wire.endTransmission();
  int x = Wire.requestFrom(SLAVE_ADDRESS, 1);
  Serial.print("Disponibles = ");
  Serial.println(x);
  int temp = (int) Wire.read();
  Serial.println(temp);

  Wire.beginTransmission (SLAVE_ADDRESS);
  Wire.write (CMD_LUMEN);
  Wire.endTransmission();
  Wire.requestFrom(SLAVE_ADDRESS, 2);
  int light = Wire.read() << 8 | Wire.read();  
  Serial.print("Light=");
  Serial.println(light);

  Wire.beginTransmission (SLAVE_ADDRESS);
  Wire.write (CMD_BEAT);
  Wire.endTransmission();
  Wire.requestFrom(SLAVE_ADDRESS, 1);

  delay (5000);
}  // конец цикла

Подчиненный код

подчиненный I2C

Получите следующие команды
<- 1-й байт -> <- 2-й байт -> <- 3-й байт ->
CMD_SENSOR
CMD_PIN_ON продолжительность номера контакта в секундах
CMD_PIN_OFF номер вывода.
CMD_LUMEN
CMD_BEAT

Каждая команда получает ответ, либо запрошенное значение
или статус.
*/
#include <max6675.h>
#include <Wire.h>

typedef struct {
  int pin;
  unsigned long off;
} PIN_PGMA;

/*
Список сосен, который можно активировать с помощью CMD_PIN_ON.
*/
#define PIN_LUMEN   A0
#define PIN_LED      2
#define PIN_RESET    3
PIN_PGMA pgma[] = {
  {PIN_LED, 0},
  {PIN_RESET, 0}
};
const int pgmaSize = sizeof(pgma) / sizeof(PIN_PGMA);

#define CMD_SENSOR    1
#define CMD_PIN_ON    2
#define CMD_PIN_OFF   3
#define CMD_LUMEN     4
#define CMD_BEAT      5

#define ST_OK         0
#define ST_BAD_PIN    1
#define ST_TIME_0     2
#define ST_BAD_LEN    3

#define MY_ADDRESS  42

// Максимальное время работы между командами CMD_BEAT. Пасадо
// Это время, активируйте PIN_RESET.
// En milisegundos.
#define BEAT_INTERVAL 10000
unsigned long lastBeat;
// Ларго дель сбросить в milisegundos.
#define RESET_LENGTH  250
byte cmd = 0;
byte status = 0;

int thermoDO = 11;
int thermoCS = 12;
int thermoCLK = 13;

MAX6675 thermocouple(thermoCLK, thermoCS, thermoDO);

void setup ()
{
  Serial.begin(9600);

  pinMode(PIN_LUMEN, INPUT);
  analogRead(PIN_LUMEN);

  for (int i = 0; i < pgmaSize; i++) {
    pinMode(pgma[i].pin, OUTPUT);
    digitalWrite(pgma[i].pin, LOW);
  }
  lastBeat = millis();

  Wire.begin (MY_ADDRESS);
  Wire.onReceive (receiveCommand);
  Wire.onRequest (sendAnswer);
} 

void loop()
{
  unsigned long now = millis();

  // Baja la linea de RESET si no ha recibido un beat ultimamente.
  unsigned long diff = now - lastBeat;
  if (diff > BEAT_INTERVAL) {
    resetPin();
  }

  // Recorre la lista de pines y apaga aquellos cuyo tiempo termino.
  for (int i = 0; i < pgmaSize; i++) {
    if (pgma[i].off > 0 && pgma[i].off <= now) {
      Serial.print("off pin="); Serial.println(pgma[i].pin);
      pgma[i].off = 0;
      digitalWrite(pgma[i].pin, LOW);
    }
  }
}

// вызывается процедурой обслуживания прерывания при запросе исходящих данных
void sendAnswer() {
  byte temp;
  int  lightReading;

  switch (cmd) {
    case CMD_SENSOR:
      temp = thermocouple.readCelsius();
      Wire.write(temp);
      break;
    case CMD_LUMEN:
      lightReading  = analogRead(PIN_LUMEN);
      Wire.write(lightReading >> 8);
      Wire.write(lightReading % 0xFF);
      break;
    case CMD_PIN_ON:
    case CMD_PIN_OFF:
    case CMD_BEAT:
      Wire.write(status);
      status = ST_OK;
      break;
  }
  cmd = 0;
}

// вызывается подпрограммой обслуживания прерывания при поступлении входящих данных
void receiveCommand (int howMany) {
  cmd = Wire.read ();
  status = ST_OK;
  switch (cmd) {
    case CMD_PIN_ON:
      cmdPinOn();;
      break;
    case CMD_PIN_OFF:
      cmdPinOff();
      break;
    case CMD_BEAT:
      lastBeat = millis();
      break;
  }
}  // конец приема события

void cmdPinOff() {
  if (Wire.available() != 1) {
    status = ST_BAD_LEN;
  } else {
    int pin = Wire.read();
    int i = searchPin(pin);
    if (i < 0) {
      status = ST_BAD_PIN;
    }
    else {
      pgma[i].off = 0;
      digitalWrite(pin, LOW);
    }
  }
}

int searchPin(int pin) {
  int i = pgmaSize - 1;
  while (i >= 0 && pgma[i].pin != pin) {
    i--;
  }
  return i;
}

/*
 * Programa el encendido y duracion del RESET.
 */
void resetPin() {  
  if (digitalRead(PIN_RESET) == LOW) {
    unsigned long now = millis();

    int i = searchPin(PIN_RESET);

    pgma[i].off = now + RESET_LENGTH;
    lastBeat = now;
    digitalWrite(PIN_RESET, HIGH);
  }
}

void cmdPinOn() {
  if (Wire.available() != 2) {
    status = ST_BAD_LEN;
  } else {
    int pin = Wire.read();
    int len = Wire.read();

    int i = searchPin(pin);
    Serial.print("pin="); Serial.print(pin); Serial.print(",index="); Serial.println(i);
    if (i < 0) {
      status = ST_BAD_PIN;
      Serial.println("bad pin");
    } else {
      if (len == 0) {
        status = ST_TIME_0;
        Serial.println("ban len");
      }
      else {
        pgma[i].off = millis() + len * 1000;
        digitalWrite(pin, HIGH);
        Serial.println("ok");
      }
    }
  }
}
,

исправление: Arduino может быть ведущим ИЛИ ведомым. NodeMCU ESP8266 может быть только ведущим., @tony gil


1

Используя I2C (который я рекомендую вместо последовательного), вы не можете использовать NodeMCU ESP8266 в качестве ведомого. Он может работать только как мастер. С другой стороны, Arduino может быть и тем, и другим. Поэтому Arduino всегда будет пассивным, а NodeMCU будет ведущим.

Что вы можете сделать, так это реализовать семафор (как описано Majenko здесь), где Arduino сигнализирует, что ему есть что сказать мастер, заставляя мастер запрашивать у Arduino. В этих обстоятельствах его можно было бы назвать «услужливым хозяином» (неологизм).

НИКОГДА не печатайте в последовательный порт из requestEvent или receiveEvent.

,