Этот проект был начат в качестве простого тестирования Ethernet модуля ENC28J60 который преобразовался в широкополосный маршрутизатор монитор и таймер контроля. Он будет отключаться от Интернета на автоматически, когда это требуется.
Этот проект Arduino подключен к запасному гнезду локальной сети на маршрутизаторе. Он постоянно следит за подключением к Интернету, отправив "Ping" запросы на удаленный веб-сервер и ожидает ответа. После того, как заранее установленное количество неудачных ответов - что указывало на то что маршрутизатор потерял подключение к Интернету - монитор пытается найти другой удаленный сервер, чтобы подтвердить, что это не просто отказ сервера. Если тест на второй сервер также не проходит, маршрутизатор будет выключен.
После того , как устройство получает ответы на пинги снова, он посылает "Уведомление" на Android устройства - смартфон или планшет - с использованием службы: Уведомить Мой Android . Бесплатное приложение можно установить на вашем Android устройстве и оно будет получать до пяти уведомлений в день. В течение заданного времени блокировки, красный светодиод на передней панели устройства находится "ON". Период блокировки может регулироваться в пределах от 1,0 до 4,0 часов с шагом 0,5 часа - см ниже.
Приуроченная локаута можно отменить, нажав кнопку RESET , аппарата.
В дополнение к отправки уведомления на ваше Android устройство, служба также отправляет короткое сообщение обратно в Arduino маршрутизатор-монитор. Программное обеспечение Arduino извлекает дату и время из этого сообщения и отображает его на 16x2 ЖК-дисплее маршрутизатора монитора. Дата и время будет оставаться на дисплее, пока вы вручную не нажмете кнопку CLEAR , на передней панели устройства. Когда истек период блокировки, красный светодиод гаснет и устройство свободно питает маршрутизатор снова в случае необходимости. Второе уведомление посылается на ваш Android устройство, как подтверждение того, что Everthing резервное копирование работает. Время отображается в GMT. Android уведомления могут быть отключены, если они не требуются.
Обратите внимание , что из - за ограничений в ATmega328, все коммуникации с Notify My Android сервиса даются в виде простого текста.
ЖК - дисплей
Относительно небольшой 16x2 ЖК-дисплей не дает много места, так что сообщения, которые он отображает немного искажены. Некоторые из возможных проявлений:
Маршрутизатор-монитор не будет инициировать цикл питания до заданного количества последовательных отказов.
Начальная настройка
При нажатии на кнопку SET на задней панели устройства, задается цикл с помощью различных вариантов настройки:
Первое нажатие отображает экран Maximum ошибки. Это число разрешенных неудачных ответов которые посылают сообщение предупреждения в Android службы. Две кнопки на передней панели используются для настройки (вверх и вниз) желаемого предварительно заданного максимального числа ошибок. (Минимум 10, максимум 99). Следующее нажатие кнопки SET отображает "Notify". Нажмите любую кнопку на передней панели для переключения между yes и нет. Если установлено значение Нет, вы не будете получать уведомления на вашем Android устройства и дата и время энергетического цикла не будет отображаться на экране.
При нажатии кнопки SET нажата снова, количество часов , что мощность цикла блокируется для может быть установлено от 1,0 часов до 4,0 часов с шагом 0,5 часа. Верхняя кнопка (LIGHT) увеличивает значение , а кнопка ниже (CLEAR) уменьшает значение.
Следующее нажатие кнопки SET является ручной блокировки? экран. Нажатие любой из передней панели кнопки переключает между yes и no. Если установлено значение Да, пинг будет продолжать отображаться на экране , а количество ошибок будет увеличиваться. Если установлено значение Нет будет работать в обычном режиме. Следующее нажатие кнопки SET показывает Daylight Saving?. Уведомляет Android сервер возвращение время уведомления в GMT. Будет отображаться на второй строке дисплея. Вы можете установить возможность добавить один час (+1) для отображения "времени на часах" во время британского летнего времени (BST). Следующее нажатие позволяет установить " Primary server." Это сервер, который пингуется большую часть времени. Когда происходят два последовательных отказа DNS поискf, маршрутизатор-монитор переключается на вторичный сервер и пытается посылать запросы пинг к нему. Монитор переключится обратно на основной сервер DNS, если поиск восстанавливается или через 10 минут когда время блокировки маршрутизатора истекает. Оба сервера установлены в программном обеспечении как www.google.com и bbc.co.uk - любой из них может быть установлен в качестве основного сервера. Другие серверы также могут быть использованы. Повторное нажатие кнопки SET возвращает экран LCD к обычному состоянию. Установленные значения, будут сохранены в памяти. При отключении питания устройства значения будут восстановлены из памяти. Когда маршрутизатор-монитор работает в обычном режиме (не отображается экран установки), верхняя кнопка на передней панели переключает подсветку ЖК-дисплея. Нижняя кнопка сбрасывает дату и время последнего включения питания и можно повторно вызвать в любой момент повторным нажатием кнопки.
ЖК - дисплей
Ping запросы направляются либо google.com или bbc.co.uk. Один из них установлен в качестве основного сервера в меню настройки. Ping запросы направляются к основному серверу через каждые пять секунд. Ответ должен произойти в течение четырех секунд , иначе увеличивается счетчик ошибок. Каждый пакет начинается с запроса DNS поиска и, когда Интернет теряется, эти DNS - запросы отключаются. Ethercard библиотека имеет встроенный 30 секундный таймер ожидания ответа DNS , и в течение этого времени, маршрутизатор-монитор не реагирует на кнопки. После того, как поиск DNS терпит неудачу, то счетчик ошибок увеличивается в два раза для каждого неудачного ответа пинг , ответы необходимы для подачи питания маршрутизатора -циклом. Это позволяет монитору быть устойчивыми к относительно высоким числом неудачных пингов. Если DNS-поиск на основном сервере не может ответить два раза подряд, монитор переключается на вторичный сервер. Когда DNS поиск неудачен, дисплей показывает имя сервера префиксом с восклицательным знаком. Например www.google.com будет отображаться на дисплее как ! Google. WWW.. Если он используется, то монитор будет отображать его на экране настройки, но не во время сбоя DNS, так как некоторое пространство зарезервировано на дисплее для подсчета ошибок. Желтый светодиод загорается, когда посылается запрос, и гаснет, когда ответ получен - как правило, в течение 1 секунды, хотя иногда ответ занимает три-четыре секунды, в зависимости от того, насколько занят Android-сервер. Если DNS-поиск в www.notifymyandroid.com выходит из строя, и нет никаких указаний на ЖК-дисплее, желтый светодиод горит и выводится информация отладки на монитор последовательного порта Arduino (57600 бод).
Схема
Схема достаточно проста. Нормальное питание маршрутизатора подключено к блоку и регулируется до 5 вольт регулятором переключения 1.5A с OKI-78SR-5. Первоначально я использовал стандартный LM7805 регулятор линейного напряжения, но отказался из-за чрезмерного повышения температуры.
Питание маршрутизатора также проходит через нормально замкнутые контакты реле 5 вольт и обратно. Реле управляется с выхода ATmega328 через транзистор BC547 NPN. Можно использовать любой общего назначения NPN-транзистор, например, 2N2222, или даже полевой МОП-транзистор 2N7000. 1N4002 диод подключаем к катушке реле, для предотврпщения всплесков на супрессорах при подаче питания. 5.1V стабилитрон на самом деле не требуется. Его цель заключается в защите выходного ATmega328 от высокого напряжения, что очень маловероятно (практически невозможно). 12 вольт от контактов реле идут мимо катушки реле и транзистора. Указанное реле Omron имеет хорошее качество. При использовании нормально замкнутых контактов реле для питания маршрутизатора, реле обесточено. Потребляя меньше энергии, Omron имеет запас отказоустойчивости, если прибор не включается, и это позволяет устройству быть сброшеным, не нарушая питание маршрутизатора. Использование реле вместо МОП-транзистора позволяет обеспечить физическую изоляцию между маршрутизатором при питании 12 вольт и ATmega328 без использования оптоизолятора. Указанное реле имеет контакты, рассчитанные на 30 вольт DC 10A. Импульсный стабилизатор напряжения питания рассчитан до 36 вольт постоянного тока на входе. Модуль ENC28J60 Ethernet широко доступны, некоторые из них в состоянии обрабатывать 3,2 вольт. Обратите внимание, что я использовал версию 5 вольт. Для версии 3.2 вольт потребуется регулятор при подключении к Vcc. Модуль подключается к ATmega328 SPI вывод D11, D12 и D13 (MOSI, MISO и SCK соответственно). Обратите внимание, что распиновка отличается от версии 3.2V к тому, что я разработал макет PCB для версии с 5v. 16x2 LCD модуль использует общедоступный I2C серийный дисплейный модуль. Выводы SDA и SCL (контакты 27 и 28) подключаем к нему. Некоторые I2C имеют выбираемый адрес. С помощью перемычки или ламелей, устанавливаем обычный адрес на ATmega328 (по умолчанию 0x27). Есть очень полезный сайт , который дает информацию о различных I2C включая адреса для Arduino, так что вы можете проверить свой I2C в случае , если вам нужно изменить адрес в строке: LiquidCrystal_I2C LCD (0x27, 16, 2 ); в маршрутизаторе-мониторе (смотри ниже). Выводы D2 до D8 (D5 не используется) ATmega328, предназначены для подключения к трем светодиодам на передней панели, два на передней панели и задней. Вторая задняя кнопка подключается к контакту сброса ATmega328. Я использовал красный светодиод на D8, чтобы указать, что маршрутизатор находится в ) периоде блокировки один час; зеленый светодиод на D6 указывает на питание маршрутизатора и желтый светодиод D7 показывает что запрос был отправлен в Notify My Android. Желтый светодиод гаснет практически сразу при ответе от Notify My Android. ATmega328 запрограммирован на Arduino IDE , через Reset, RXD и TXD выводы с использованием USB-RS232 модуля TTL (посмотрите поиск junradio, статья по USB-RS232 есть на сайте). Вывод DTR выполняет автоматический сброс при программировании ATmega328.
Компоновка печатной платы
Основные компоненты
ATmega328 с Arduino загрузчиком |
Кварц 16 МГц |
5 вольт Модуль ENC28J60 Ethernet |
реле 5v Omron G5LA-1 SPDT |
16x2 ЖК-дисплей и последовательный интерфейс I2C |
OKI-78SR-5 / 1,5-W36-C Регулятор 5v |
2 - 12 двухрядные разъемы IDC (для модуля Ethernet) |
134x129x54mm корпус |
Разъем питания для включения питания |
Особенности питания
5 вольтовый регулятор
Схема кушает меньше 180mA но регулятор LM7805 хоть и рассеивает около 1,3 Вт - нагревается даже с радиатором с тепловым сопротивлением около 12 ° C / Вт.
Модуль ENC28J60
Я приспособил небольшой самоклеящийся теплоотвод к ENC28J60, потому что он сильно нагревается, если остался в рабочем состоянии в течение длительного времени. Версия 5v от ENC28J60 имеет регулятор на 3,2 вольта в нижней стороне печатной платы, и ему необходим поток воздуха путем установки пару шайб на 2мм выступающих на 2mm от крепежного винта.
Sockets
Мой маршрутизатор (ZyXel) использует 2.1mm DC штекер, хотя многие из них имеют 2,5 мм (Thomson TG582, например).
Для выходной мощности постоянного тока, чтобы избежать путаницы, после некоторых поисков использовал RCA разъем. Он рассчитан на 2A, которые достаточны для питания большинства маршрутизаторов.
Сборка
Верхняя фотография показывает заполненную печатную плату, при использовании регулятора 5v (LM7805) с радиатором мощностью около 12 ° C / Вт. Как я уже отмечал выше, повышение температуры не позволили питать маршрутизатор с более высоким напряжением, поэтому я заменил LM7805 и теплоотвод регулятором переключения - показано крупным планом на втором фото. Нижняя фотография показывает завершенный блок с взаимосвязями между печатной платой, передней и задней панелей. PCB макет сделан для 12-выводного IDC разъема модуля ENC28J60. Другие модули могут использовать другую компоновку.
Программирование Arduino
Примечание о коде: ATmega328 имеет очень мало "динамической памяти", для хранения переменных. В результате, этот код не является "хорошим" с точки зрения программирования (блоки кода записываются дважды вместо того, чтобы назначать функции, например), но это дает лучшую возможность использовать обильную память программы, чтобы оставить доступным большую часть драгоценной динамической памяти. Код широко использует PROGMEM для хранения постоянных данных, освобождая около 250 байт динамической памяти.
Arduino библиотеки:
При регистрации учетной записи с Notify My Android , вы получите ключ 48 символов
Введите ключ между кавычки ( "-----") в пространстве выделены желтым цветом в коде ниже.
// DSL Router Monitor (c) August 2016 vwlowen.co.uk
// Based on ethercard examples 'ping' and 'notify my android'
// Compiled using Arduino IDE version 1.6.11 (AVR Board version 1.6.13)
// Dynamic memory is very tight in the ATmega328 (Arduino Uno). Expect stability problems if the code is
// modified even slightly.
// https://github.com/jcw/ethercard
// https://github.com/fdebrabander/Arduino-LiquidCrystal-I2C-library
#include <EtherCard.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <EEPROM.h>
LiquidCrystal_I2C lcd(0x27, 16, 2); //This LCD is at I2C address 0x27. Others may differ.
const char charMap[] [8] PROGMEM = { // Data for user-defined LCD characters
{14, 17, 14, 4, 4, 28, 4, 28}, // Key icon
{10, 31, 21, 31, 31, 14, 10, 27}, // Android icon
{4, 4, 4, 4, 4, 21, 14, 4}, // Lower case 'g' with a proper descender!
{0, 0, 14, 18, 18, 14, 2, 14}, // Underlined exclamation (Auto daylight saving - status unknown)
{0, 4, 21, 14, 21, 4, 0, 31}, // Underlined asterisk (Auto daylight saving - BST)
{4, 4, 4, 4, 0, 4, 0, 31} // Underline blank. (Auto daylight saving - GMT)
};
const char apihost[] PROGMEM = "www.notifymyandroid.com"; // NotifyMyAndroid server
const char server1[] PROGMEM = "www.google.com"; // Main server to ping.
const char server2[] PROGMEM = "bbc.co.uk"; // Secondary server to check when DNS lookup on main server fails.
byte offset1 = 0; // Used to omit 'www.' from LCD if present in server url.
byte offset2 = 0;
/* Variables to adjust for *UK* daylight saving time. (BST) */
/* Gets date/time from Notify My Android reply. */
/* Only works for +1 hr UK. */
const char sMon0[] PROGMEM = "Jan";
const char sMon1[] PROGMEM = "Feb";
const char sMon2[] PROGMEM = "Mar";
const char sMon3[] PROGMEM = "Apr";
const char sMon4[] PROGMEM = "May";
const char sMon5[] PROGMEM = "Jun";
const char sMon6[] PROGMEM = "Jul";
const char sMon7[] PROGMEM = "Aug";
const char sMon8[] PROGMEM = "Sep";
const char sMon9[] PROGMEM = "Oct";
const char sMon10[] PROGMEM = "Nov";
const char sMon11[] PROGMEM = "Dec";
const char * const mons_table[] PROGMEM {sMon0, sMon1, sMon2, sMon3, sMon4, sMon5, sMon6, sMon7, sMon8, sMon9, sMon10, sMon11};
const char sDow0[] PROGMEM = "Sun";
const char sDow1[] PROGMEM = "Mon";
const char sDow2[] PROGMEM = "Tue";
const char sDow3[] PROGMEM = "Wed";
const char sDow4[] PROGMEM = "Thu";
const char sDow5[] PROGMEM = "Fri";
const char sDow6[] PROGMEM = "Sat";
const char * const dows_table[] PROGMEM = {sDow0, sDow1, sDow2, sDow3, sDow4, sDow5, sDow6};
const char message0[] PROGMEM = "Router power-cycled."; // Notify My Android messages
const char message1[] PROGMEM = "Router unlocked. ";
const char message2[] PROGMEM = "Test notification. ";
const char * const message_table[] PROGMEM = {message0, message1, message2};
byte datetime[16]; // Used to "copy" data that is printed on LCD 2nd row for recall later.
byte nDay; // Day of the month
byte nDow; // Day of the week
byte nMon; // Month
#define BST 1 // Add 1 hour for British Summer Time (Other positive values may work).
boolean daylightSaving = false; // Show time in GMT or BST?
byte ds = 0; // 0 or 1 or 2 = GMT or BST or Auto ?
// ethernet interface mac address, must be unique on the LAN
static byte mymac[] = { 0x74,0x69,0x69,0x2D,0x30,0x31 };
byte Ethernet::buffer[800];
unsigned long pingTimer;
unsigned long errTimer;
unsigned long lockoutTimer;
#define errTimeout 4000000 // wait this long (microseconds) before reporting no ping response
#define pingTimeout 5000000 // wait this long (microseconds) between pings. Note this time increases
// to about 35 seconds when the DNS lookup fails as the DNS has to time out.
// The DNS timeout is hard-coded at 30000 ms in the ethercard library (dns.cpp)
#define routerDelay 15000 // wait this long (milliseconds) when power-cycling the router. (15 seconds)
byte mins = 60; // lockTimeout will be calculated as minutes * 60000 msec.
unsigned long lockTimeout = 3600000; // wait this long (milliseconds) before allowing another router cycling.
#define serverTimeout 600000 // Swap back to primary server after this time.
unsigned long serverTimer;
boolean useServer1 = true; // Default is to use server1 (google.com)
boolean serverSwapped = false;
byte dnsErrCount = 0;
byte errCount = 0; // Accumulating number of errors (no response to pings) before the next response to pings.
byte errHighCount = 0; // Make a note of the highest number of errors before a response. This
// helps determine the number of errors before triggering the router
byte maxError = 20; // re-cycle (maxError) and Notify My Android (NMA).
byte nmaAction = 0; // Determines message to send to Notify My Android service.
byte op = 0; // Determines currently displayed setup screen on LCD and action up/down buttons will perform.
/* Hardware defines */
#define ethCS 10 // ethercard chip select pin.
#define lockLED 8 // Red LED lights on first power re-cycle and stays on until lockout times out (lockTimeout).
#define nmaLED 7 // Yellow LED to indicate NMA has been called but no response received (yet).
#define PowerLED 6 // Separate normally ON output for green LED which 'mirrors' the power to the router.
#define powerRelay A3 // Normally OFF output to relay. Relay uses a normally closed contact to power the router.
#define clearLCD 4 // Clear 'Status Line' (2nd Row) on LCD which normally shows the time of the last power cycle.
#define lightLCD 3 // Push button to toggle LCD backlight on/off
#define setMax 2 // Set push button
/* Each boolean actually uses a byte so is a "bit" wasteful (!) but is easier to implement and understand! */
boolean led = true; // Is LCD backlight on or off?
boolean timedLockout = false; // Is router power cycle locked out for an hour?
boolean dnsFail = false; // Has DNS lookup failed?
boolean manualLockout = false; // Is router locked 'ON' so it won't power-cycle even after a timed lockout?
boolean isLocked = false;
boolean nma = true; // Notify My Android?
boolean cleared = true; // LCD 2nd row (status line) is clear.
boolean dateLine = false;
Stash stash; // Class to build request to Notify My Android.
static byte session;
char buff[21];
// Routine to send a formatted request to NotifyMyAndroid. As noted in the ethercard example, this data has to be
// sent over the internet as plain text because the ATmega328 is limited in what it can do due to memory constraints.
static void notifyMyAndroid(const byte msg) {
strcpy_P(buff, (char*) pgm_read_word(&(message_table[msg]))); // Retrieve message from Program Memory into buffer.
digitalWrite(nmaLED, HIGH); // Signal that we've called Notify My Android.
if (!ether.dnsLookup(apihost)) {
Serial.println(F("DNS lookup failed for the apihost"));
}
ether.printIp("SRV: ", ether.hisip);
byte sd = stash.create(); // Create ethernet stash class.
stash.print(F("apikey="));
stash.print(F("------------------------------------------------")); // Enter your NMA key when you register an account
stash.print(F("&application="));
stash.print(F("Arduino Watchdog"));
stash.print(F("&event="));
stash.print(F("Router monitor"));
stash.print(F("&description="));
stash.print(buff); // Message retrieved from Program Memory.
stash.print(F("&priority="));
stash.print(F("0")); // Priority 1 would over-ride Android's Do Not Disturb.
stash.save();
int stash_size = stash.size();
// Compose the http POST request, taking the headers below and appending
// previously created stash in the sd holder.
Stash::prepare(PSTR("POST /publicapi/notify HTTP/1.1" "\r\n"
"Host: $F" "\r\n"
"Content-Length: $D" "\r\n"
"Content-Type: application/x-www-form-urlencoded" "\r\n"
"\r\n"
"$H"),
apihost, stash_size, sd);
pingTimer = micros(); // Reset ping and error timers so
errTimer = micros(); // they won't timeout while receiving Notification
// send the packet - this also releases all stash buffers once done
// Save the session ID so we can watch for it in the main loop.
session = ether.tcpSend();
}
/* ====== Function to update 2nd row on LCD ("Status Line") ============ */
void updateStatus() { // Update the 2nd row on the LCD
lcd.setCursor(0,1);
for (byte i=0;i<16;i++)
lcd.print(F(" "));
if (nma && !cleared && dateLine) { // 2nd row is showing date/time
if (timedLockout)
digitalWrite(lockLED, HIGH);
lcd.setCursor(0,1);
for (byte i=0; i<13; i++)
lcd.write(datetime[i]);
if (daylightSaving) {
addHour();
timedLockout ? lcd.print(F("bst")) : lcd.print(F("BST"));
} else {
if (timedLockout) {
lcd.write(3);
lcd.setCursor(14,1);
lcd.print(F("mt"));
}else
lcd.print(F("GMT"));
}
return;
}
if (manualLockout) { // 2nd row is showing normal status line
lcd.setCursor(9,1); // If manual lockout is selected...
lcd.write(0);
lcd.print(F("LOCKED")); // display 'LOCKED'.
digitalWrite(lockLED, HIGH);
} else { // Not manually locked so show normal icons.
lcd.setCursor(0,1); // Display max error and lock time.
lcd.print(F("E:"));
lcd.print(maxError); // Max errors before trigger router power cycle
if (nma && !manualLockout) {
lcd.setCursor(7,1);
lcd.write(1); // Print Android symbol if Notify My Android is set
}
lcd.setCursor(8,1);
if (daylightSaving) { // daylight saving will be known after a NMA response.
ds==1 ? lcd.print(F("*")):lcd.write(4);
} else {
switch (ds) { // If daylight saving state isn't known, show
case 0: lcd.print(F(" ")); // setting instead (No, Yes, Auto).
break;
case 1: lcd.print(F("*")); // Yes
break;
case 2: lcd.write(5); // Auto (state unknown)
break;
}
}
if (timedLockout || manualLockout) {
digitalWrite(lockLED, HIGH);
lcd.setCursor(9,1);
lcd.write(0); // Print a key symbol
} else {
digitalWrite(lockLED, LOW);
}
lcd.setCursor(12,1);
if (mins < 100) lcd.print(F(" "));
lcd.print(mins);
lcd.print(F("m"));
}
}
/* ================ Function to add 1 hour for daylight saving. =============== */
void addHour() { // This is a simple GMT +1 hr. Memory constraints
char hr[3]; // prevent anything more versatile.
hr[0] = datetime[7];
hr[1] = datetime[8];
hr[2] = '\0';
byte h = atoi(hr) + BST; // Add one hour to GMT time
if (h > 23) { // If hour crosses to next day....
h = 0; // increment day number.
byte dom = nDay +1; // nDay is day of month as sent by Notify My Android
// if new day of month is > 30 and month is Apr, Jun, Sep or Nov.
if ((dom > 30) && ((nMon == 4) || (nMon == 6) || (nMon == 9) || (nMon == 11))){
dom = 1; // Set day of month to 1
nMon ++; // Increment month
} else
if (dom > 31) { // if new day of month is more than 31...
dom = 1; // day of month = 1
nMon ++; // next month
}
lcd.setCursor(0,1);
if (dom < 10) lcd.print(F("0")); // Print new day of month
lcd.print(dom);
}
lcd.setCursor(7,1);
if (h < 10) lcd.print(F("0"));
lcd.print(h);
lcd.setCursor(13,1);
}
/* ======== Function to print character array from Program Memory one character at a time ======== */
void printProgStr (const char * str, const char ch, byte offset, byte total) {
char c;
byte b = 0;
if (!str) return;
str = str + offset; // Skip 'www.' if offset > 0.
while ((c = pgm_read_byte_near(str++)) && (c != ch) && (b < total)) { // Stop at ch character or 'total' characters.
c == 'g' ? lcd.write(3) : lcd.print(c); // If character is 'g' print custom g
b++; // with a proper descender!
}
}
/* Function to determine if server url begins with www . and returns appropriate number of characters to skip */
byte isWww(const char * str) {
byte result = 0;
while (pgm_read_byte_near(str++)=='w') result++;
result == 3 ? result = 4 : result = 0; // Also skip the dot after 'www'
return result;
}
/* ======== Setup ====================================== */
void setup () {
pinMode(PowerLED, OUTPUT);
pinMode(lockLED, OUTPUT); // Lock LED (Router is unable to power-cycle when this LED is lit).
pinMode(powerRelay, OUTPUT); // Power to router LED.
pinMode(nmaLED, OUTPUT); // Notify My Android has been notified LED.
pinMode(ethCS, OUTPUT);
pinMode(lightLCD, INPUT_PULLUP); // Backlight on/off
pinMode(clearLCD, INPUT_PULLUP); // Clear date/time from LCD
pinMode(setMax, INPUT_PULLUP); // Set Max error push button
digitalWrite(powerRelay, LOW); // Power on router
digitalWrite(PowerLED, HIGH);
lcd.begin();
byte bb[8]; // Retrieve custom LCD characters from PROGMEM and create characters.
for (byte i=0;i<6;i++) {
for (byte j=0;j<8;j++) bb[j] = pgm_read_byte(&(charMap[i][j]));
lcd.createChar(i, bb);
}
Serial.begin(57600);
Serial.println(F("\n[pings]"));
lcd.clear();
lcd.backlight();
useServer1 = constrain(EEPROM.read(5), 0, 1); // Select which server to use as main server
ds = constrain(EEPROM.read(4), 0, 2); // Is daylight savings set to GMT, BST or Auto?
manualLockout = constrain(EEPROM.read(3), 0, 1); // Is manual lockout in force? no/yes
nma = constrain(EEPROM.read(2), 0, 1); // Get notify my android no/yes.
mins = constrain(EEPROM.read(1), 60, 240); // Get number of minutes router is locked out for.
maxError = constrain(EEPROM.read(0), 10, 99); // Get max errors that are allowed from EEPROM.
offset1 = isWww(server1); // Determine if server urls begin with 'www.'
offset2 = isWww(server2);
lockTimeout = mins * 60000;
for (byte i=0;i<16;i++)
datetime[i]= ' ';
lcd.setCursor(0,0);
lcd.print(F("Initializing.."));
if (ether.begin(sizeof Ethernet::buffer, mymac, ethCS) == 0) { // Initialize ethercard.
Serial.println(F("Failed to access Ethernet controller"));
lcd.print(F("Controller Error"));
}
if (!ether.dhcpSetup()) { // DHCP must be enabled in the router so it can assign
lcd.setCursor(0, 0);
lcd.print(F("DHCP Failed ")); // the ethercard an IP address and get the gateway and
lcd.setCursor(0,1);
lcd.print(F("Check Connection"));
Serial.println(F("DHCP failed")); // DNS server addresses.
}
while(!ether.dhcpSetup()); // Loop here until DHCP is working
lcd.clear();
updateStatus();
ether.printIp("IP: ", ether.myip);
ether.printIp("GW: ", ether.gwip);
ether.printIp("DNS:", ether.dnsip);
pingTimer = micros(); // set the ping timer running
}
void loop () {
/* ======== Check if we're on the secondary server and swap back to primary server after 10 minutes ======= */
if (serverSwapped) {
if (millis() - serverTimer >= serverTimeout) {
useServer1 = constrain(EEPROM.read(5), 0, 1); // Swap back to main server
serverSwapped = false;
dnsErrCount = 0;
}
}
/* ======== Check if the router cycle lockout timer has expired ======================================== */
if (timedLockout) { // If router power-cycle is locked out, check the
if (cleared) { // elapsed time and - if the 2nd row on the LCD
byte left = ((lockTimeout - (millis() - lockoutTimer)) / 60000) + 1; // isn't showing the date/time, show the elapsed
lcd.setCursor(11,1); // time.
if (left < 100) lcd.print(F(" "));
lcd.write(2);
if (left < 10) lcd.print(F("0"));
lcd.print(left);
lcd.print(F("m"));
}
if (millis() - lockoutTimer >= lockTimeout) { // If time has expired...
digitalWrite(lockLED, LOW); // 'Lock' LED OFF.
timedLockout = false; // Release the timed lockout flag.
if (serverSwapped) {
useServer1 = constrain(EEPROM.read(5), 0, 1); // Swap back to main server
serverSwapped = false;
dnsErrCount = 0;
}
isLocked = false;
updateStatus(); // Update the 2nd row of the LCD
if (nma && !manualLockout) {
nmaAction = 2; // Tell Notify my android.
notifyMyAndroid(1); // 'Router unlocked' message
}
}
}
/* =================== Front Panel Switches ==================================================== */
if (digitalRead(clearLCD) == LOW) { // If 'Clear' button is pressed, update
delay(250); // the LCD's 2nd row.
if (digitalRead(lightLCD) != LOW) { // Check 'Light' button isn't pressed also.
cleared = !cleared;
updateStatus();
}
}
if (digitalRead(lightLCD) == LOW) { // Toggle LCD backlight on/off
delay(250);
if (digitalRead(clearLCD) != LOW) { // Chec 'Clear' button isn't pressed also.
led = !led;
led ? lcd.backlight(): lcd.noBacklight();
}
}
if ((digitalRead(clearLCD) == LOW) && (digitalRead(lightLCD) == LOW)) { // Press both buttons for
delay(250); // a test Notification
while ((digitalRead(clearLCD) == LOW) || (digitalRead(lightLCD) == LOW));
nmaAction = 1;
notifyMyAndroid(2);
}
/* =================== Main Settings menu ========================================================= */
if (digitalRead(setMax) == LOW) { // If 'Set' button is pressed, enter main 'Setup' loop
while (digitalRead(setMax) == LOW); // Wait here until button is released.
op = 0; // Reset 'operation' variable.
lcd.clear();
delay(150);
lcd.print(F("Set Max Errors"));
lcd.setCursor(0,1);
lcd.print(maxError);
while (op == 0) {
while (digitalRead(lightLCD) == LOW) { // Use LCD backlight on/off button to increase max error.
if (maxError < 99) maxError += 1;
lcd.setCursor(0,1);
lcd.print(maxError);
lcd.print(F(" "));
delay(250);
}
while (digitalRead(clearLCD) == LOW) { // Use clear button to decrease max error.
if (maxError > 10) maxError -= 1;
lcd.setCursor(0,1);
lcd.print(maxError);
lcd.print(F(" "));
delay(250);
}
if (digitalRead(setMax) == LOW) { // Cycle indefinitely until 'SetMAx' button is pressed again
op = 1;
while(digitalRead(setMax) == LOW);
}
}
delay(50);
lcd.clear();
lcd.print(F("Notify MyAndroid")); // Enable/disable Notify My Android
while (op == 1) {
lcd.setCursor(0,1); // Display current NMA status
if (nma ) {
lcd.print(F("Yes "));
lcd.write(1);
} else {
lcd.print(F("No "));
}
while ((digitalRead(lightLCD) == LOW) || (digitalRead(clearLCD) == LOW)) { // ======== Set Notify My Android ====
nma = !nma;
lcd.setCursor(0,1);
if (nma ) {
lcd.print(F("Yes "));
lcd.write(1);
} else {
lcd.print(F("No "));
}
delay(250);
}
if (digitalRead(setMax) == LOW) { // Press 'SetMax' again to exit NMA loop
op = 2;
while(digitalRead(setMax) == LOW);
}
}
delay(50);
lcd.clear(); // Set the number of minutes (in 30 minute increments)
lcd.print(F("Timed Lockout ")); // that the route rpower-cycle can be locked out for.
lcd.write(0);
while (op == 2) {
lcd.setCursor(0,1);
lcd.print(mins);
lcd.print(F(" min ("));
lcd.print((float)mins/60,1); // Display minutes in hours. (1.0, 1.5, 2.0.... 4.0)
lcd.print(F(" hr) "));
while (digitalRead(lightLCD) == LOW) { // Use LCD backlight on/off button to increase minutes.
if (mins < 240) mins += 30; // Increase minutes by 30 minute intervals.
lcd.setCursor(0,1);
lcd.print(mins);
lcd.print(F(" min ("));
lcd.print((float)mins/60,1); // Display minutes in hours. (1.0, 1.5, 2.0.... 4.0)
lcd.print(F(" hr) "));
delay(250);
}
while (digitalRead(clearLCD) == LOW) { // Use clear error button to decrease minutes.
if (mins > 60) mins -= 30; // Decrease minutes by 30 minute intervals.
lcd.setCursor(0,1);
lcd.print(mins);
lcd.print(F(" min ("));
lcd.print((float)mins/60,1); // Display minutes in hours. (1.0, 1.5, 2.0.... 4.0)
lcd.print(F(" hr) "));
delay(250);
}
if (digitalRead(setMax) == LOW) { // Press 'SetMax' again to exit Set Lockout loop
op = 3;
while(digitalRead(setMax) == LOW);
}
}
delay(50);
lcd.clear();
lcd.print(F("Manual Lockout?")); // Set/unset Manual Lockout
while (op == 3) {
lcd.setCursor(0,1);
if (manualLockout ) { // Display current Manual lockout status
lcd.print(F("Yes "));
} else {
lcd.print(F("No "));
}
while ((digitalRead(lightLCD) == LOW) || (digitalRead(clearLCD) == LOW)) {
manualLockout = !manualLockout;
lcd.setCursor(0,1);
manualLockout ? lcd.print(F("Yes")):lcd.print(F("No "));
delay(250);
}
if (digitalRead(setMax) == LOW) { // Press 'SetMax' again to exit Manual Lockout loop
op = 4;
while(digitalRead(setMax) == LOW);
}
}
delay(50);
lcd.clear();
lcd.print(F("Daylight Saving?")); // Set Daylight saving selector
while(op == 4) {
lcd.setCursor(0,1);
switch (ds) {
case 0: lcd.print(F("No "));
break;
case 1: lcd.print(F("Yes "));
break;
case 2: lcd.print(F("Auto"));
break;
}
while ((digitalRead(lightLCD) == LOW) || (digitalRead(clearLCD) == LOW)) {
ds += 1;
if (ds > 2) ds = 0;
lcd.setCursor(0,1);
switch (ds) {
case 0: lcd.print(F("No "));
break;
case 1: lcd.print(F("Yes "));
break;
case 2: lcd.print(F("Auto"));
break;
}
delay(250);
}
if (digitalRead(setMax) == LOW) { // Press 'SetMax' again to exit Daylight Saving loop
op = 5;
while(digitalRead(setMax) == LOW);
}
}
delay(50);
lcd.clear();
lcd.print(F("Primary server")); // Set the primary server
while (op == 5) {
lcd.setCursor(0,1);
useServer1 ? printProgStr(server1, '!',0, 16) : // Print server name from program memory and
printProgStr(server2, '!', 0, 16); // include 'www' if present in url.
while ((digitalRead(lightLCD) == LOW) || (digitalRead(clearLCD) == LOW)) {
useServer1 = !useServer1; // Swap servers as the primary server
lcd.setCursor(0,1);
lcd.print(F(" "));
lcd.setCursor(0,1);
useServer1 ? printProgStr(server1, '!', 0, 16) : // Print server name from program memory
printProgStr(server2, '!', 0, 16); // include 'www' if present in url.
delay(250);
}
if (digitalRead(setMax) == LOW) { // Press 'SetMax' again to exit settings loop
op = 6;
while(digitalRead(setMax) == LOW);
}
}
lcd.clear();
if (EEPROM.read(5) != useServer1) EEPROM.write(5, useServer1);
if (EEPROM.read(3) != manualLockout) EEPROM.write(3, manualLockout);
if (EEPROM.read(2) != nma) EEPROM.write(2, nma);
if (EEPROM.read(1) != mins) EEPROM.write(1, mins);
if (EEPROM.read(0) != maxError) EEPROM.write(0, maxError); // Save values in EEPROM but don't write unnecessarily.
if (EEPROM.read(4) != ds) {
EEPROM.write(4, ds);
if (dateLine) { // If Daylight Saving selection has changed, and a
while(digitalRead(setMax) == LOW); // date/time has been received previously, re-evaluate
delay(100); // the time.
switch (ds) { // Check setting of Daylight Saving selector
case 0: daylightSaving = false; // No, Yes, Auto
break;
case 1: daylightSaving = true;
break;
case 2: daylightSaving = isSummer();
break;
default: daylightSaving = false;
break;
}
}
}
updateStatus(); // Display (new) settings on 2nd row of LCD
lockTimeout = mins * 60000; // Convert minutes to milliseconds
}
/* =================================== End of Main Settings Menu ======================================= */
/* ping a remote server once every few seconds. I found it necessary to use DNS lookup every time *
* around the loop because the ethercard also looks up the address of notifymyandroid.com */
if (micros() - pingTimer >= pingTimeout){
dnsFail = false;
boolean DNSsuccess;
if (useServer1) { // Default to Google server but try the BBC server
DNSsuccess = ether.dnsLookup(server1); // if the DNS lookup fails. Alternate between
} else { // the two servers until DNS lookup succeeds.
DNSsuccess = ether.dnsLookup(server2);
}
if (!DNSsuccess ) {
dnsErrCount++;
Serial.println(F("DNS failed"));
lcd.setCursor(4,0);
lcd.print(F(" "));
lcd.setCursor(4,0);
if (useServer1 ) { // Show which server DNS lookup has failed.
lcd.print(F("!"));
printProgStr(server1, '.', offset1, 11); // Only show up to first dot and start at 'offset'
} else { // but only show a maximum of 11 characters.
lcd.print(F("!"));
printProgStr(server2, '.', offset2, 11);
}
dnsFail = true; // As DNS is now failing, we can increase the error count rate...
if (errCount < 99) // .. by counting DNS failures as well as ping failures.
errCount++;
if (!serverSwapped && (dnsErrCount >= 2)) {
useServer1 = !useServer1; // Swap to the secondary server after 2 DNS failures.
serverSwapped = true;
serverTimer = millis(); // Start server timer running.
}
} else {
lcd.setCursor(0,0);
if (errCount < 10) lcd.print(F(" "));
lcd.print(errCount);
lcd.print(F("> "));
}
if (cleared) { // If 2nd LCD row is not showing date/time...
lcd.setCursor(5,1);
useServer1 ? lcd.write(toupper(pgm_read_byte_near(server1+offset1))) : // Show 1st letter of server that's being used.
lcd.write(toupper(pgm_read_byte_near(server2+offset2)));
}
ether.printIp("Pinging ", ether.hisip); // show ping address on Serial monitor
if (!dnsFail) // No point sending ping request if DNS has failed.
ether.clientIcmpRequest(ether.hisip); // Send the ping request and..
pingTimer = micros(); // ..set the ping timer running
errTimer = micros(); // start ping error timer
}
word len = ether.packetReceive(); // go receive new packets
word pos = ether.packetLoop(len); // respond to incoming pings
/* ========== Report successful ping response on LCD ================================================== */
if (len > 0 && ether.packetLoopIcmpCheckReply(ether.hisip)) {
float temp = (micros() - pingTimer) * 0.001;
lcd.setCursor(4,0);
lcd.print(F(" ^"));
lcd.setCursor(4,0);
lcd.print(temp, 2);
Serial.print(temp, 3);
useServer1 = constrain(EEPROM.read(5), 0, 1); // Swap back to main server
serverSwapped = false;
dnsErrCount = 0;
Serial.println(F(" ms")); // Cosmetic text on Monitor and LCD.
lcd.print(F("ms "));
lcd.setCursor(0,0);
lcd.print(F(" 0"));
lcd.print(F(">"));
if (errCount > maxError) { // There must have been a router power-cycle
if (nma && !timedLockout && !manualLockout ) {
nmaAction = 1;
notifyMyAndroid(0); // 'Router power-cycled' message.
}
if (!timedLockout)
lockoutTimer = millis(); // Restart the timed lockout timer so lock time starts now
timedLockout = true;
}
errCount = 0; // Reset error count to zero for every successful response
dnsErrCount = 0;
}
/* ======== No reply to ping after 'errTimeout' time so increment error counter. ================ */
if (micros() - errTimer > errTimeout) {
if (errCount < 99) // No response after error timeout so increment error counter.
errCount++;
Serial.print(F("Error "));
Serial.println(errCount);
lcd.setCursor(0,0); // Update the LCD with the error count
if (errCount < 10) lcd.print(F(" "));
lcd.print(errCount);
lcd.print(F(">"));
lcd.setCursor(12,0);
if (!dnsFail) { // If we have a ping response failure but NOT a DNS failure..
if (errCount < maxError) // If the error was not due to a DNS lookup error, keep
errHighCount = max(errHighCount, errCount); // a tally of the highest error count before receiving
// a ping response. This should help setting the value at
// which to trigger the router power-cycle and the subsequent
lcd.print(F(" ^"));
lcd.print(errHighCount);
lcd.print(F(" "));
}
errTimer = micros(); // Re-start ping error timeout timer.
if (!isLocked && (errCount >= maxError)) { // After 'maxError' error timeouts or more...
errCount++; // Increment counter to ensure we only enter this loop once.
if (!timedLockout && !manualLockout) { // If router hasn't been power cycled in the last hour (or more, as set)..
digitalWrite(lockLED, HIGH); // 'Lock' LED ON.
if (serverSwapped) {
useServer1 = constrain(EEPROM.read(5), 0, 1); // Swap back to main server
serverSwapped = false;
dnsErrCount = 0;
}
if (cleared) { // If 2nd row on LCD is not showing date/time, print the key character.
lcd.setCursor(9,1);
lcd.write(0);
}
digitalWrite(powerRelay, HIGH); // Power down router. Router is wired to normally closed contact on
digitalWrite(PowerLED, LOW); // relay so energizing the relay breaks power to the router. This is better
delay(routerDelay); // than having the relay energized most of the time and is fail-safe if the
digitalWrite(powerRelay,LOW); // ATmega328 output fails.
digitalWrite(PowerLED, HIGH);
lockoutTimer = millis(); // Initialize lockout timer so it won't time out next time round the main loop.
isLocked = true;
}
}
}
/* =========== Look for a response from NotifyMyAndroid and extract the Date & Time from the returned reply === */
ether.packetLoop(ether.packetReceive());
char *reply = ether.tcpReply(session); // Look out for session ID which was obtained from tcpSend.
if (reply != 0) { // Response received.
if (nmaAction == 1) { // Only parse date/time for LCD when the notification is from a router
lcd.setCursor(0,1); // power cycle, not when notification for the timed lockout ends.
dateLine = true;
/* The format of the reply is: *
* HTTP/1.1 200 OK<crlf>Server: nginx/0.8.55<crlf>Date: Sun, 07 Aug 2016 13:30:14 GMT<crlf>Content-Type...etc *
* Location of ^D+11=idx--^ idx+12--^ */
char *p = strstr_P(reply, PSTR("Date")); // Look for "Date" in reply and set pointer to start of actual date, 11
byte idx = (p - reply) + 11; // characters later. (Date/time is extracted from HTTP response header.)
byte idy = 0;
for (byte i = idx; i < (idx+7); i++) { // Extract the actual date characters from the NMA reply.
lcd.write(reply[i]); // and write to the LCD
datetime[idy] = reply[i]; // Copy date into byte array for recall later
idy++;
}
for (byte i = (idx+12); i < (idx+17); i++) { // Extract hours and mins from NMA's reply for LCD (skip the year).
lcd.write(reply[i]); // Write the time to the LCD
datetime[idy] = reply[i]; // Copy time into byte array for recall later
idy++;
}
char dow[4]; // Extract day of week text from NMA reply
dow[0] = reply[idx-5];
dow[1] = reply[idx-4];
dow[2] = reply[idx-3];
dow[3] = '\0';
for (byte i=0;i<7;i++) { // Convert day of week text to numerical value
if (strcmp_P(dow, pgm_read_word (&(dows_table[i] )))==0){
nDow = i+1; // result = numerical Day Of Week.
break;
}
}
char day[3];
day[0] = reply[idx]; // Numerical day of month
day[1] = reply[idx+1];
day[2] = '\0';
nDay = atoi(day);
char mon[4];
mon[0] = reply[idx+3]; // Month (text)
mon[1] = reply[idx+4];
mon[2] = reply[idx+5];
mon[3] = '\0';
for (byte i = 0; i<12;i++) { // Convert Month in NMA reply to numerical value.
if (strcmp_P(mon, pgm_read_word (&(mons_table[i])) )==0){
nMon = i+1;
break;
}
}
switch (ds) { // Check setting of Daylight Saving selector
case 0: daylightSaving = false; // No, Yes, Auto
break;
case 1: daylightSaving = true;
break;
case 2: daylightSaving = isSummer();
break;
default: daylightSaving = false;
break;
}
lcd.setCursor(12,1);
lcd.print(F(" "));
datetime[12] = ' ';
if (daylightSaving) { // During Daylight Saving, add an hour and print 'bst'...
addHour();
lcd.print(F("bst"));
} else {
lcd.write(3);
lcd.setCursor(14,1);
lcd.print(F("mt")); // ... otherwise, print 'gmt'
}
}
if (nmaAction == 2) { // This is the second Notification (Router unlocked)
lcd.setCursor(0,1);
for (byte i=0;i<16;i++)
lcd.print(F(" ")); // Clear the LCD 2nd row
lcd.setCursor(0,1);
for (byte i=0; i<13; i++)
lcd.write(datetime[i]); // Recall date/time
if (daylightSaving) // Add daylight saving if required.
addHour();
lcd.setCursor(13, 1);
daylightSaving ? lcd.print(F("BST")) : lcd.print(F("GMT")); // Change gmt or bst to Uppercase.
}
nmaAction = 0;
cleared = false; // Flag that 2nd row on LCD shows date/time.
Serial.print(reply); // Show full response from Notify My Android on Monitor.
digitalWrite(nmaLED, LOW); // NMA has responded so turn off LED indication that NMA was called.
}
}
// stackoverflow.com/questions/5590429/calculating-daylight-saving-time-from-only-date
static bool isSummer() {
if (nMon < 3 || nMon > 10) return false;
if (nMon > 3 && nMon < 10) return true;
int previousSunday = nDay - nDow;
if (nMon == 3) return previousSunday >= 25;
if (nMon == 10) return previousSunday < 25;
return false;
}