ENC28J60 и REST-управление реле

ENC28J60 — почему-то, именно с этим Ethernet-модулем возникают проблемы у всех, кто пытался его использовать, отойдя в сторону от одно-двухстраничного WEB-сервера. Ответ в принципе работы ENC28J60; его работа с TCP/IP организована программно, а не аппаратно. Оттого, ожидать совместимости с другими похожими Ethernet-модулями несколько необдуманно.

Кратко. Нужно реализовать Web-сервер на Arduino, в качестве Ethernet-модуля которого будет использоваться ENC28J60. Сервер должен получать REST-команды с клиента и возвращать JSON-строку в качестве ответа. Также, в качестве управляемого устройства должен выступать реле-модуль.

Клиентом же может выступать некий абстрактный скрипт, написанный на NodeJS.

План решения этой задачи можно разбить на следующие этапы:

  1. Подключение модуля ENC28J60 к Arduino,
  2. Подключение реле-модуля на 2 реле,
  3. Реализация обработки запросов модулем ENC28J60,
  4. Соединение всего вместе,
  5. Написание „клиента“ на NodeJS.

Примечание: Рассматривается только Arduino UNO. Arduino Nano нельзя использовать с модулем ENC28J60. Проблема кроется где-то в особенности работы 13-го GPIO-разъёма. Выглядеть это будет как вечно зависший Arduino. Про остальные платы Arduino не знаю. Может, в чём-то я и не прав, так как видел такие вот девайсы в продаже: Arduino NANO v3 Ethernet Shield.

Подключение модуля ENC28J60 к Arduino

Есть 3 способа подключения Ethernet-модуля 28J60.

Долгий поиск по интернету и опыты приводят к следующим выводам:

Получается, что наиболее грамотным вариантом (не „простым“, не „удобным“ а именно „грамотным“) является EtherCard.

Внимание! EtherCard использует 8-ой GPIO-разъём, а не 10-ый при подключении контакта „Chip Select“. В EtherCard можно использовать и десятый разъём, но для этого нужно явно указать в void setup() {}:

ether.begin(sizeof Ethernet::buffer, mymac, 10);
//------------------------------------------^

Таким образом, GPIO-разъёмы, необходимые для корректного подключения следующие:

ENC28J60 pin - Arduino pin VCC - 3,3V GND - GND (самоочевидно) SCK - Pin 13 SO - Pin 12 SI - Pin 11 CS - Pin 8 (или 10-ый, см. выше)

Характеристики модуля ENC28J60:

Напряжение - 3,3В Ток в режиме передачи - 180мА Ток в режиме простоя - 120мА

Имейте это в виду при использовании нескольких разных модулей с этим. Согласно „Arduino current limitations“, Arduino сможет отдавать только 200мА тока для питания (или 400мА, если у неё 2 GND-разъёма). После этого порога наступает его неисправность.

Подключение реле-модуля на 2 реле

С документацией на эти релюшки туго. Очень.

еглый осмотр дорожек на плате позволяет заключить, что имеется 2 способа соединения Arduino и этого модуля: используя питания только от Arduino (используя перемычку VCC—JD-VCC) и убрав перемычку и запитав катушки реле отдельно. Замер тока показывает, что обе включенные реле используют для катушки около 100мА. Итого, нужна следующая схема подключения, где перемычку VCC—JD-VCC нужно снять:

Контакты модуля реле - Контакты других устройств JD-VCC - +5V DC (Блок питания 5В) GND - GND (Блок питания) In1 - Любой GPIO (Arduino) In2 - Любой GPIO (Arduino) VCC - +5V (Arduino)

Примечание: Для управления реле можно использовать и 3,3-вольтовую логику. К примеру, Wi-Fi-модуль ESP8266 вполне может управлять этим реле напрямую. Но это не относится к обсуждаемой теме.

В данном случае получается, что, для того, чтобы добиться разности фаз на управлении реле, нужно не подавать логический сигнал с Arduino на соответствующий GPIO. Иными словами, digitalWrite(2, LOW); включит реле, если тот висит на GPIO2, а digitalWrite(2, HIGH); отключит реле.

Т.о., в этом реле-модуле всё наоборот, по сравнению с банальным светодиодом. Это следует иметь в виду при использовании реле.

Реализация обработки запросов модулем ENC28J60

Для удалённого управления Arduino удобнее всего использовать библиотеку „Arduino REST“. В замечательных Wi-Fi модулях ESP-8266 (которые сами себе „Arduino“) и для Serial-соединения вида „Arduino-компьютер“ эта библиотека проявила себя просто потрясающе, и поэтому сложилось заблуждение, что в случае Ethernet будет аналогично. Сразу следуют разочарования:

  1. aREST не поддерживает EtherCard,
  2. aREST вообще не поддерживает вышеописанные модули в целом.

Есть два способа решения этой проблемы:

Изменить поведение aREST, из них:

  1. Реализовать rest.handle() вручную прямо в void loop() {};
  2. Расширить aREST.h из скетча Arduino;
  3. Исправить aREST.h, дописав нужное.
  4. Написать свою собственную реализацию аналогичной библиотеки.

Сперва я кинулся писать собственную реализацию, но, непосредственно перед выкладкой на GitHub при написаний примеров использования я наткнулся на ошибку (где-то ошибки с указателем и поведение вывода программы стало зависеть от фазы луны), поэтому проще было приручить надёжную aREST, чем пользоваться своей нестабильной поделкой (автор aREST пошёл по правильному пути, он резервирует буфер вывода программы в 350 байт заранее, чего я не делал, пытаясь объегорить контроллер с помощью „ехал указатель через указатель…“). I.e. пошёл по варианту 1.1.

Однако, и тут были грабли. Связка двух библиотек (aREST и EtherCard) оставляет около 200 байт памяти для работы Arduino из её двух килобайт.

Пришлось чуть-чуть подумать и максимально ужать взаимодействие между этими библиотеками. Код получился следующим (под результат оставляет около 500 байт свободными, а это четверть памяти Arduino, что неплохо):

/**
 * This a simple example of the aREST Library interaction with ENC28J60 shield.
 * Written in 2016 by Lex ( http://numidium.ru/ ) under a GPL license.
 */
#include <EtherCard.h>

// Declaring aREST
#include <aREST.h>
aREST rest = aREST();

// Ethernet interface ip address
static byte myip[] = { 192, 168, 0, 200 };
// Gateway ip address
static byte gwip[] = { 192, 168, 0, 1 };

// Ethernet mac address - must be unique on your network
static byte mymac[] = { 0x74, 0x69, 0x69, 0x2D, 0x30, 0x31 };

// TCP/IP send and receive buffer.
// 350 selected due to aREST output buffer size is the same,
// and for memory economy.
byte Ethernet::buffer[350]; // tcp/ip send and receive buffer
BufferFiller bfill;

int pinRelay1 = 2;
int pinRelay2 = 3;
void setup() {
  ether.begin(sizeof Ethernet::buffer, mymac, 10); // GPIO 10 for CS!
  ether.staticSetup(myip, gwip);

  // Assigning functions to urls:
  rest.function("relay1", relay1);
  rest.function("relay2", relay2);

  // Set up the relays (2, 3 pins):
  pinMode(pinRelay1, OUTPUT);
  pinMode(pinRelay2, OUTPUT);

  // Relays are off by default:
  digitalWrite(pinRelay1, 1);
  digitalWrite(pinRelay2, 1);
}

// Cutom functions for relays
int relay1(String params) {
  if (params.length() != 0) {
    int newVal = params.toInt();
    newVal == 0 ? newVal = 1 : newVal = 0;
    digitalWrite(pinRelay1, newVal);
  }
  return digitalRead(pinRelay1) == 0 ? 1 : 0;
}
int relay2(String params) {
  if (params.length() != 0) {
    int newVal = params.toInt();
    newVal == 0 ? newVal = 1 : newVal = 0;
    digitalWrite(pinRelay2, newVal);
  }
  return digitalRead(pinRelay2) == 0 ? 1 : 0;
}

void loop() {
  word len = ether.packetReceive();
  word pos = ether.packetLoop(len);
  if (pos) {
    bfill = ether.tcpOffset();
    char *data = (char *) Ethernet::buffer + pos;

    if (strncmp("GET /", data, 5) != 0) {
      // Unsupported request method (POST or other).
      bfill.emit_p(PSTR("Unsupported HTTP request."));
    } else {
      // vvv--- Handle REST manually ---vvv
      rest.handle_proto(data);
      char *res = rest.getBuffer();
      // Send HTTP header:
      bfill.emit_p(
        PSTR("HTTP/1.0 200 OK\r\nContent-Type: application/json\r\nPragma: no-cache\r\n\r\n")
      );
      // Send result:
      bfill.emit_raw(res, strlen(res));
      rest.resetBuffer();
      rest.reset_status();
      // ^^^--- Handle REST manually ---^^^
    }

    ether.httpServerReply(bfill.position());    // send http response
  }
}

Итого, при запросе к нашему серверу http://192.168.0.200/relay1 или http://192.168.0.200/relay2 мы получим значение реле (1 или 0 в ключе return_value JSON-ответа); а при запросе http://192.168.0.200/relay1?params=1 мы включим первое реле, запросив http://192.168.0.200/relay1?params=0 — выключим.

Соединяем всё воедино (ток)

Итак, у нас есть ардуина, способная выдать на любой свой GPIO до 40 мА и на два VCC-разъёма до 200 мА. При превышении этих значений наступит деградация микроконтроллера.

Релюшки жрут около 100 мА во включенном состоянии, модуль Ethernet — 180 мА. Отсюда следует, что питать что-то из этого нужно отдельно. Проще всего разделить JD-VCC и GND реле.

Запитать их можно через блок питания. Используя — например — такую вот плату с регуляторами напряжения:

Сам блок питания должен выдавать: 500мА (Arduino и ENC28J60, который питается через Arduino) + 100мА (питание реле) + 25% (от суммы) = 600мА + 25% = 750мА. Чаще всего в продаже встречаются блоки питания на 1А, скорее всего, именно он и нужен.

Написание „клиента“ на NodeJS

Всё просто. Пишем обычный web-клиент:

/**
 * This is an example of the interaction with aREST library for Arduino/ESP8266.
 * Written in 2016 by Lex ( http://numidium.ru/ ) under a GPL license.
 */
var http = require('http');

/**
 * Gets URL and parsing it.
 * @param {string} req URL address to request.
 * @param {function} cb Callback (optional).
 *
 * Callback function have to take one object with fields:
 * {
 *   'message': string, // Error message
 *   'error': int,      // If error is happened, this number will be 1 otherwise 0
 *   'value': int,      // 'return_value' field of the JSON response from server.
 * }
 */
var getInfo = function(req, cb) {
  // If callback is specified:
  var ret = function(d) {
    if (typeof(cb) !== 'undefined') {
      cb(d);
    }
  };

  // Typical HTTP GET request.
  http.get(req, (res) => {
    var result = '';

    // Save incoming data chunk by chunk
    res.on('data', (chunk) => {
      result += chunk;
    });

    // At the end of process
    res.on('end', () => {
      // Trying to parse a result
      try {
        var obj = JSON.parse(result);
        ret({
          'error': 0,
          'value': obj.return_value
        });
      } catch (e) {
        // If we unable to parse result
        // (Arduino hangs, wrong IP running another server, et.c.)
        ret({
          'error': 1,
          'message': 'Error while parsing result.'
        });
      }
    });
  }).on('error', (e) => {
    // On connection error
    ret({
      'error': 1,
      'message': 'Connection error.'
    });
  });
};

// Main
getInfo('http://192.168.0.200/relay1', (result) => {
  if (result.error != 0) {
    console.log('Error: ' + result.message);
    return;
  }
  var res = (result.value == 1)?'on':'off';
  console.log('Relay 1 is ' + res);

  if (result.value == 1) {
    // It's on! Let's turn it off now.
    console.log('Turning off.');
    getInfo('http://192.168.0.200/relay1?params=0');
  } else {
    // It's off! Let's turn it on now.
    console.log('Turning on.');
    getInfo('http://192.168.0.200/relay1?params=1');
  }
});

Проверяем его работу:

$ node enc28j60-arduino-rest.js 
Relay 1 is off
Turning on.
$ node enc28j60-arduino-rest.js 
Relay 1 is on
Turning off.

Вместо заключения

Осталось только спаять всё вместе и подобрать для результата подходящий корпус, просверлив в нём необходимые отверстия. Также нужно написать нормальную программу-клиент вместо теста из предыдущего пункта. Всего-навсего.

Для NodeJS уже есть модуль aRest (также на гитхабе). Беглый просмотр его кода позволяет сделать вывод о том, что эту библиотеку удобно использовать при наличии более одного-двух устройств (Arduino/ESP8266) прошитых с aREST.h.

На случай зависания Arduino можно использовать watchdog. Однако, следует предварительно проверить Arduino в плане его правильной работы и — в случае необходимости — перепрошить загрузчик таким образом, чтобы он обеспечивал корректную работу watchdog.

Modified: 2016-01-20 00:00:00