Arduino a celý ekosystém s ním spojený je v současné době velkým fenoménem. Jedni ho oslavují, druzí nenávidí. Já jsem názorově asi tak někde uprostřed, neboť pro mě je Arduino v podstatě jen AVRko a C++ překladač v takovém stavu, že ho stačí nainstalovat a vše funguje. Bonusem je řada knihoven, HW periferií a velká komunita lidí a tím i živá diskuzní fóra. Arduino je v zásadě hračka, ale docela fajn hračka, protože je možné si začít hrát hned a není třeba trávit spoustu času tím, že k rozblikání LEDky potřebuji ještě nějaký programátor, avrdude, avrgcc, makefile a prostudovat datasheet MCU. Souhlasím s názorem, že to vede k jisté otupělosti, kdy člověk dostane vše předžvýkáno až pod nos a vše funguje bez nějaké větší snahy - ale na druhou stranu - řídí snad auta jen automechanici? :-)
Takže jsem si pořídil Arduino Uno a Ethernet Shield. Jedná se o čínské klony a jelikož čínský soudruh je tvor pragmaticky spořivý, má UNO o několik součástek méně a okolo USB konektoru převodník CH340 místo FTDI, resp. Megy16.
V podstatě je to jednoduchá blbost :-) Situace je taková, že v serverovně (racku se switchi apod.) jsou dva okruhy napájení - dejme tomu dvě samostatně jištěné fáze. Na tyto okruhy jsou připojeny UPS, klimatizace apod. Cílem tohoto hlídače je dát vědět dohledovému systému v situaci, kdz dojde k výpadku nějakého napájecího okruhu. Důvodem je, že provoz serverovny bez klimatizace je zpravidla nemožný a samotné UPS vydrží většinou na šetrný shutdown. Dohled samozřejmě mohou dělat samotné UPSky, ale jsou situace, kdy to prostě nějde, neboť v racku není UPSka s ethernetovým rozhraním a zároveň není po ruce nějaké železo schopné si s UPS povídat přes USB nebo RS232.
Schéma sice řekne nejvíc, ale několik poznámek: Základem je Arduino Uno s ethernet shieldem. Tzn. komunikace s dohledovým systémem je po ethernetu tak, že dohledový systém se periodicky Arduina doptává, zda-li je všechno v pořádku. Detekce, zda-li je v jednotlivých okruzích napětí je řešeno tak, že do každého okruhu je zapojen malý adaptér 5V/1A (cca. 110,-Kč v GME). Toto řešení je zvoleno proto, že je tak zajištěno napájení i pokud je živý pouze jeden okruh. Zároveň se v zařízení nepracuje se silnoproudem - ten končí na certifikovaném adaptéru, z něhož vede pouze bezpečné napětí. Dále je k dispozici jeden externí vstup (např. pro nějaké čídlo zaplavení, otevření racku apod.) a vstup pro one-wire teploměr DS18B20. Propojení s Arduinem je 5-ti dráty do piny A0-A4 a napájení.
Ovládací "program" je složen z 5-ti souborů:
Komunikace je primitivní. Stačí se připojit telnetem, Arduino vrátí jeden řádek se stavy okruhů, stavu externího vstupu a teploty. Následně spojení ukončí.
telnet 10.210.100.1 Arduino-Square-Box:LineUPS-ON|LineA-ON|LineB-ON|ExtIn-OPEN|Thrm-1249# Připojení k hostiteli bylo ztraceno.
#include <SPI.h> #include <Ethernet.h> #include "inkey.h" #include "OneWire.h" #include <avr/wdt.h> #include "utility/w5100.h" #define READ_TEMPERATURE 0 #define EXEC_CONVERSION 1 byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED }; IPAddress ip(10, 210, 100, 1); IPAddress myDns(10, 210, 1, 10); IPAddress gateway(10, 210, 1, 1); IPAddress subnet(255, 255, 0, 0); EthernetServer server(23); inkey extInput(A0); inkey lineUPS(A1); inkey lineA(A2); inkey lineB(A3); OneWire ds(A4); unsigned int thrmBuffer[2]; unsigned int thrmCounter = 0; // -- nastaveni -- void setup() { wdt_enable(WDTO_4S); Ethernet.begin(mac, ip, myDns, gateway, subnet); server.begin(); Serial.begin(115200); Serial.print("IP address: "); Serial.println(Ethernet.localIP()); readThrm(EXEC_CONVERSION); delay(1000); thrmBuffer[0] = readThrm(READ_TEMPERATURE); thrmBuffer[1] = thrmBuffer[0]; } // -- pracovni smycka -- void loop() { unsigned char i = 0; byte remoteip[4]; // -- obsluha sitoveho pripojeni -- server.available(); while (i < 4) { EthernetClient client(i); if (client.connected()) { Serial.print("New connection from "); W5100.readSnDIPR(i, remoteip); Serial.println(IPAddress(remoteip)); client.print("Arduino-Square-Box:"); if (lineUPS._state) client.print("LineUPS-OFF|"); else client.print("LineUPS-ON|"); if (lineA._state) client.print("LineA-OFF|"); else client.print("LineA-ON|"); if (lineB._state) client.print("LineB-OFF|"); else client.print("LineB-ON|"); if (extInput._state) client.print("ExtIn-OPEN|"); else client.print("ExtIn-CLOSE|"); client.print("Thrm-"); client.print((thrmBuffer[0] + thrmBuffer[1]) / 2, DEC); client.print("#"); client.println(); client.stop(); } i++; } // -- obsluha smycek -- if (extInput.beat()) { if (extInput._state) Serial.println("ExtIn OPEN"); else Serial.println("ExtIn CLOSE"); } if (lineUPS.beat()) { if (lineUPS._state) Serial.println("LineUPS OFF"); else Serial.println("LineUPS ON"); } if (lineA.beat()) { if (lineA._state) Serial.println("LineA OFF"); else Serial.println("LineA ON"); } if (lineB.beat()) { if (lineB._state) Serial.println("LineB OFF"); else Serial.println("LineB ON"); } // -- obsluha teplomeru -- thrmCounter++; if (thrmCounter == 1180) readThrm(EXEC_CONVERSION); if (thrmCounter >= 1200) { thrmBuffer[1] = thrmBuffer[0]; thrmBuffer[0] = readThrm(READ_TEMPERATURE); thrmCounter = 0; Serial.print("Read new temperature: "); Serial.println(thrmBuffer[0]); } delay(50); wdt_reset(); } // -- spusti prevod nebo nacteni cidla -- unsigned int readThrm(unsigned char action) { unsigned char data_lo, data_hi; signed int t; unsigned int tout = 0; switch (action) { case EXEC_CONVERSION: ds.reset(); ds.write(0xCC); ds.write(0x44); break; case READ_TEMPERATURE: ds.reset(); ds.write(0xCC); ds.write(0xBE); data_lo = ds.read(); data_hi = ds.read(); t = ((data_hi << 8) | data_lo) * 0.625; tout = t + 1000; if ((tout < 500) || (tout > 1500)) tout = 0; } return (tout); }
inkey.h
#ifndef inkey_h #define inkey_h #include <Arduino.h> class inkey { public: inkey(unsigned char pin); unsigned char beat(); unsigned char _state; private: unsigned char _pin; unsigned char _counter = 0; }; #endif
inkey.cpp
#include <Arduino.h> #include "inkey.h" inkey::inkey(unsigned char pin) : _pin(pin) { pinMode(_pin, INPUT_PULLUP); } unsigned char inkey::beat() { unsigned char somethingChanged = 0; if (digitalRead(_pin)) { if (_counter < 20) _counter++; if (_counter == 15) { somethingChanged = 1; _state = 1; } } else { if (_counter) _counter--; if (_counter == 5) { somethingChanged = 1; _state = 0; } } return(somethingChanged); }
Toto je obvyklé použití Ethernet Library, které je možné najít na webu Arduina jako example. Je to sice jednoduché, funguje to, ale podlě mě dosti nešťastně.
#include <SPI.h> #include <Ethernet.h> byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED }; IPAddress ip(10, 210, 100, 1); IPAddress myDns(10, 210, 1, 10); IPAddress gateway(10, 210, 1, 1); IPAddress subnet(255, 255, 0, 0); EthernetServer server(23); // -- nastaveni -- void setup() { Ethernet.begin(mac, ip, myDns, gateway, subnet); server.begin(); } // -- pracovni smycka -- void loop() { EthernetClient client = server.available(); if (client) { if (client.available() > 0) { // zde je cteni dat pomoci client.read // nebo odpoved pomoci client.print apod. } } }
Vadí mi především toto:
Knihovna pro ethernet je zde c:\Program Files\Arduino\libraries\Ethernet\src\ a bude se jednat o úpravy v souborech EthernetClient.cpp a EthernetClient.h. Upravovat samotné knihovny mě přijde dost zhovadilé, ale i přes veškerou snahu se mi nepodařil najít způsob, jak v C++ přidat do existující třídy nějakou další metodu bez zásahu do hlavičkového souboru a nebo jak se ve třídě, kterou z existující podědím dostat k privátním atributům a metodám třídy rodičovské.
Ve třídě EthernetClient je číslo aktuálně používaného socketu v privátní proměnné _sock. Řešením je udělat z privátní proměnné proměnnou veřejnou nebo napsat veřejnou metodu na vrácení její hodnoty. Rozhodl jsem se pro druhou variantu, protože mi příjde přehlednější.
Do EthernetClient.h přidat do sekce public prototyp metody getSockID()
.... public: .... uint8_t getSockID(); ....
Do EthernetClient.cpp přidat implementaci metody getSockID()
uint8_t EthernetClient::getSockID() { return(_sock); }
Použití:
.... // -- pracovni smycka -- void loop() { // promena pro ulozeni id klienta unsigned char idClient; // ziskani instance klienta EthernetClient client = server.available(); if (client) { // zjisteni hodnoty _sock, tedy cislo socketu idClient = client.getSockID(); // nyni vim, ktery vzdaleny klient poslal data // idClient bude cislo 0-3 if (client.available() > 0) { // zde je cteni dat pomoci client.read // nebo odpoved pomoci client.print apod. } } }
Důvodem pro záměrné přerušení spojení může být fakt, že vzdálený klient neposlal data v nějakém rozumeném časovém intervalu. Nemá smysl čekat věčnost a přitom si blokovat drahocenný socket a tím i možnost připojení dalších klientů. Pokud tedy na základě předchozí úpravy vím, se kterým socketem si povídám, tak si mohu měřit timeout a podle něj pak spojení odstřelovat. Praxi se mi odsvědčil přístup takový, že jsem si vytvořil čtyři časovače, které se nulovaly vždy po příchozu nějakého znaku z daného socketu a pokud znak dlouho nepřišel, tak kill.
Do EthernetClient.h přidat do sekce public prototyp metody kill(uint8_t sock)
.... public: .... void kill(uint8_t sock); ....
Do EthernetClient.cpp přidat implementaci metody kill(uint8_t sock)
void EthernetClient::kill(uint8_t sock) { // attempt to close the connection gracefully (send a FIN to other side) disconnect(sock); unsigned long start = millis(); // wait up to a second for the connection to close uint8_t s; do { s = socketStatus(sock); if (s == SnSR::CLOSED) break; // exit the loop delay(1); } while (millis() - start < 1000); // if it hasn't closed, close it forcefully if (s != SnSR::CLOSED) close(sock); EthernetClass::_server_port[sock] = 0; }
Použití:
.... // 4 citace v poli unsigned char timer[4] = {0,0,0,0}; .... // -- pracovni smycka -- void loop() { unsigned char i; EthernetClient client = server.available(); if (client) { if (client.available() > 0) { // nastavovani citace na max hodnotu 60*50ms, tj. 3 sekundy timer[client.getSockID()] = 60; // zde je cteni dat pomoci client.read // nebo odpoved pomoci client.print apod. } } // vyhodnoceni timeoutu i=4; while(i--) { if(timer[i]>0) timer[i]--; // odecitani citacu smerem k nule if(timer[i]==1) client.kill(i); // pokud je citac na hodnote 1, tak kill } //50ms krok delay(50); }
Řešením může být to, že se vybodneme na metodu server.available() a budeme si instance třídy Ethernet Client vytvářet sami. Argumentem konstruktoru této třídy je totiž číslo socketu 0-3. Metoda connected() nám řekne, zda-li je k socketu někdo připojen.
.... // 4 citace v poli unsigned char timer[4] = {0,0,0,0}; // 4 flagy aktivniho spojeni, aby se welcome message poslala jen jednou unsigned char connected[4] = {0,0,0,0}; .... // -- pracovni smycka -- void loop() { unsigned char i = 0; server.available(); // krokovani po socketech while (i < 4) { // vytvoreni instance klienta EthernetClient client(i); // dotaz, zda-li je ve W5100 aktualni socket pripojeny ke vzd. klientovi if (client.connected()) { // uvitaci zprava a nastaveni flagu o pripojeni if(connected[i]==0) { // welcome message client.print("Arduino-Square-Box:"); // nastaveni flagu o aktivnim spojeni connected[i]=1; // spusteni timeoutu, aby se killnulo i spojeni, kde vzd. klient neposlal zadny znak timer[i] = 60; } // cteni dat if (client.available() > 0) { // nastaveni timeoutu timer[i] = 60; // zde je cteni dat pomoci client.read // nebo odpoved pomoci client.print apod. } } else { // pokud neni spojeni aktivni, tak nastavime flag pripojeni na 0 connected[i]=0; } i++; } // vyhodnoceni timeoutu i=4; while(i--) { if(timer[i]>0) timer[i]--; // odecitani citacu smerem k nule if(timer[i]==1) { // pokud je citac na hodnote 1, tak kill client.kill(i); connected[i]=0; } } delay(50); }
Toto lze udělat dvěma způsoby. První je bez úpravy Ethernet knihovny a to vložením utility/w5100.h do include a zavoláním W5100.readSnDIPR(i, remoteip), kde i je číslo socketu a remoteip je pointer na pole 4 bytes. Výstupem této metody je pole remoteip naplněné IP adresou vzdáleného klienta. Pro snadné zformátování pro tisk (nejen) na sériový port je použita třída IPAddress.
// vlozeni tridy pro ovladani W5100 #include "utility/w5100.h" ... // -- pracovni smycka -- void loop() { unsigned char i = 0; // pole 4 byte pro ulozeni IP adresy byte remoteip[4]; // -- obsluha sitoveho pripojeni -- server.available(); while (i < 4) { EthernetClient client(i); if (client.connected()) { Serial.print("New connection from "); // zjisteni IP vzdaleneho klienta na socketu i do remoteip W5100.readSnDIPR(i, remoteip); // tisk IP na seriovy port Serial.println(IPAddress(remoteip)); ...
Když už se však zasahuje do Ethernet knihovny, je lepší variantou přidat metodu na zjištění vzdádlené adresy přímo do ní a to takto:
Do EthernetClient.h přidat do sekce public prototyp metody getRemoteIP()
.... public: .... IPAddress getRemoteIP(); ....
Do EthernetClient.cpp přidat implementaci metody getRemoteIP()
IPAddress EthernetClient::getRemoteIP() { byte rip[4]; W5100.readSnDIPR(_sock, rip); return rip; }
Použití je pak jednodušší:
// -- pracovni smycka -- void loop() { unsigned char i = 0; // -- obsluha sitoveho pripojeni -- server.available(); while (i < 4) { EthernetClient client(i); if (client.connected()) { Serial.print("New connection from "); // zjisteni a tisk IP vzd. klienta na seriovy port Serial.println(client.getRemoteIP()); ...