ENC28J60 и REST-управление реле
ENC28J60 — почему-то, именно с этим Ethernet-модулем возникают проблемы у всех, кто пытался его использовать, отойдя в сторону от одно-двухстраничного WEB-сервера. Ответ в принципе работы ENC28J60; его работа с TCP/IP организована программно, а не аппаратно. Оттого, ожидать совместимости с другими похожими Ethernet-модулями несколько необдуманно.
Кратко. Нужно реализовать Web-сервер на Arduino, в качестве Ethernet-модуля которого будет использоваться ENC28J60. Сервер должен получать REST-команды с клиента и возвращать JSON-строку в качестве ответа. Также, в качестве управляемого устройства должен выступать реле-модуль.
Клиентом же может выступать некий абстрактный скрипт, написанный на NodeJS.
План решения этой задачи можно разбить на следующие этапы:
- Подключение модуля ENC28J60 к Arduino,
- Подключение реле-модуля на 2 реле,
- Реализация обработки запросов модулем ENC28J60,
- Соединение всего вместе,
- Написание „клиента“ на NodeJS.
Примечание: Рассматривается только Arduino UNO. Arduino Nano нельзя использовать с модулем ENC28J60. Проблема кроется где-то в особенности работы 13-го GPIO-разъёма. Выглядеть это будет как вечно зависший Arduino. Про остальные платы Arduino не знаю. Может, в чём-то я и не прав, так как видел такие вот девайсы в продаже: Arduino NANO v3 Ethernet Shield.
Подключение модуля ENC28J60 к Arduino
Есть 3 способа подключения Ethernet-модуля 28J60.
- Используя библиотеку UIPEthernet,
- Используя библиотеку ETHER_28j60,
- Используя библиотеку EtherCard.
Долгий поиск по интернету и опыты приводят к следующим выводам:
- ETHER_28J60 не работает как полагается и даже не „собирается“;
- Из оставшихся двух (Arduino_UIP и EtherCard), наиболее удобной в использовании является UIP. Однако, он жрёт просто чудовищный объём свободной памяти (сравнивая с Arduino UNO и её 2 килобайтами).
- EtherCard является довольно сложным в обращении — никаких print() прямо в качестве ответа „сервера“ и т.п.
Получается, что наиболее грамотным вариантом (не „простым“, не „удобным“ а именно „грамотным“) является 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 будет аналогично. Сразу следуют разочарования:
- aREST не поддерживает EtherCard,
- aREST вообще не поддерживает вышеописанные модули в целом.
Есть два способа решения этой проблемы:
Изменить поведение aREST, из них:
- Реализовать rest.handle() вручную прямо в
void loop() {};
- Расширить aREST.h из скетча Arduino;
- Исправить aREST.h, дописав нужное.
- Написать свою собственную реализацию аналогичной библиотеки.
Сперва я кинулся писать собственную реализацию, но, непосредственно перед выкладкой на 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.