Interfejs do liczników ORNO. Rejestrator danych, link USB-RS485 poprzez Wi-Fi

Interfejs do liczników ORNO. Rejestrator danych, link USB-RS485 poprzez Wi-Fi
Pobierz PDF Download icon

System automatyki domowej nie kończy się na sterowaniu oświetleniem czy roletami w oknach. Równie ważne jest monitorowanie i rejestracja parametrów takich, jak temperatura, ciśnienie czy pobór energii. Dzięki temu można bardziej optymalnie zarządzać elementami systemu i np. określić, które urządzenia warto całkowicie odłączać od zasilania, w czasie gdy z nich nie korzystamy.

W artykule zaprezentowano interfejs przeznaczony dla liczników ORNO-WE504/514 (fotografia 1), ale po niewielkich modyfikacjach zadziała z praktycznie każdym licznikiem wyposażonym w port RS485.

Fotografia 1. Licznik energii elektrycznej ORNO WE514

Funkcja linku USB (VCOM) – RS485 poprzez Wi-Fi pozwoli skorzystać z oprogramowania liczników dostarczonego przez producenta i łatwo kontrolować zużycie energii elektrycznej.

Budowa i zasada działania

Urządzenie składa się z modułu ESP8266 zamontowanego na płytce Wemos D1 Mini. Do płytki dołączono interfejs UART-RS485 na bazie układu MAX3485. Schemat połączeń został pokazany na rysunku 1.

Rysunek 1. Schemat połączeń urządzenia

W interfejsie można zastosować układ MAX485 zasilając go z 5 V oraz dodając rezystor 470 Ω...1 kΩ na wyjściu RO (pin 1). Całość należy uzupełnić odpowiednim zasilaczem. Prototyp został zmontowany na „pająka” i pierwotnie była to płytka WemosD1, a dopiero później tańszej WemosD1-Mini (fotografia 2).

Fotografia 2. Prototyp urządzenia

Pracując w trybie mostka USB-RS485, za pośrednictwem programu USR-VCOM urządzenie przesyła dane z wirtualnego portu COM na port 8888 wybranego adresu IP. Moduł ESP odbiera te dane i przesyła na RS-485. Kod programu, odpowiedzialny za to zadanie, został pokazany na listingu 1 i znajduje się w pętli głównej programu.

Listing 1. Kod programu odpowiedzialny za odbieranie danych i przesłanie przez RS485

 if (localClient && localClient.connected() ) {
   if (localClient.available()) {
     size_t len = localClient.available();
     uint8_t sbuf[len];
     localClient.readBytes(sbuf, len);
     wyslij_na_RS485(sbuf, len);
   }
 }
 
 void wyslij_na_RS485( uint8_t *sbuf, size_t len) {
   digitalWrite(DIR485, HIGH);
   Serial.write(sbuf, len);
   // Get the number of bytes (characters) available for writing
   //in the serial buffer without blocking the write operation.
   while ( Serial.availableForWrite() != lenBufSerTx ) ;
   // Konieczne, bo używając "Serial.availableForWrite()"
   //stwierdzimy kiedy bufor jest pusty a nie kiedy znak wyslano z UART
   delayMicroseconds( TIM_SEND_BYTE_UART );
   digitalWrite(DIR485, LOW);
 }

Ze względu na to, że API Arduinolibs nie przewiduje możliwości stwierdzenia faktu wysłania znaku z bufora nadawczego, wprowadzono opóźnienie ustawiane definicją #define TIM_SEND_BYTE_UART dzięki czemu nie było potrzeby modyfikowania kodu biblioteki a kierunek transmisji zmieniany jest po wysłaniu znaku jak pokazuje oscylogram z rysunku 2 (żółty oscylogram pokazuje stan linii RE/DE).

Rysunek 2. Kierunek transmisji RS485 jest zmieniany po wysłaniu znaku, co pokazuje oscylogram
Listing 2. Funkcja przesyłająca dane z UART przez Wi-Fi

 void Uart_do_Wifi() {
   uint32_t static timeoutWr, timCzasRd;
   size_t static prevLen;
   // jesli zmienil sie rozmiar bufora
   if ( prevLen != Serial.available()) {
     prevLen = Serial.available();
     if ( prevLen ) { // i jest rozny od 0
       // Przychodzacy znak ustawia timeout
       timeoutWr = millis() + 5;
       if ( ! timCzasRd ) timCzasRd = millis();
     }
   }

   // Gdy timeout minie
   if ( millis() >= timeoutWr ) {
     // Zatrzymujemy liczenie
     timeoutWr = 0xFF000000;   

     size_t len = Serial.available();
     uint8_t sbuf[len];
     Serial.readBytes(sbuf, len);

     localClient.write(sbuf, len);
   }
 }

Funkcja przesyłająca dane z UART przez Wi-Fi ma bardziej skomplikowane działanie choć jest krótsza – listing 2. Znaki otrzymywane przez UART są zapamiętywane w programowym FIFO, które dla ESP8266 ma rozmiar 128 znaków i taka jest maksymalna wielkość ramki jaką można przesłać. Gdy przerwa pomiędzy znakami przekroczy 5 ms, zapamiętane znaki zostaną wysłane. W rzeczywistości funkcje są bardziej rozbudowane i zawierają wysyłanie informacji debugujących na UART1. Cały kod źródłowy znajduje w materiałach dodatkowych do projektu.

Listing 3. Funkcja wysyłająca zapytanie do licznika

 void Zapytania_Do_Orno() {
   // INFO: 514 bez względu na to o ile rejestrów pytamy odpowie jednym
   const byte zapytanie_514[][8] = {
     { 0x00, 0x03, 0x01, 0x31, 0x00, 0x01, 0xD5, 0xE8 }, // Pytanie o napiecie
     { 0x00, 0x03, 0x01, 0x39, 0x00, 0x02, 0x14, 0x2B }, // Pytanie o prad
     { 0x00, 0x03, 0x01, 0x40, 0x00, 0x02, 0xC5, 0xF2 }, // Pytanie o moc czynna
     { 0x00, 0x03, 0x01, 0x48, 0x00, 0x02, 0x44, 0x30 }, // Pytanie o moc bierna
     { 0x00, 0x03, 0x01, 0x58, 0x00, 0x01, 0x05, 0xF4 }, // Pytanie o PF
     { 0x00, 0x03, 0xA0, 0x00, 0x00, 0x0A, 0xE6, 0x1C }, // Pytanie o zuzyta energie czynna
     { 0x00, 0x03, 0xA0, 0x1E, 0x00, 0x0A, 0x86, 0x1A }, // Pytanie o zuzyta energie bierna
     { 0x00, 0x03, 0x01, 0x50, 0x00, 0x02, 0xC4, 0x37 }, // Pytanie o moc pozorna
     { 0x00, 0x03, 0x01, 0x30, 0x00, 0x01, 0x84, 0x28 }, // Pytanie o czestotliwosc
     { 0xFF }
   };

   // INFO: 504 można pytać o dowolną liczbę rejestrów
   const byte zapytanie_504[][8] = {
     {
       0x00, 0x03, //00 03   ID, Function READ
       0x00, 0x00, //00 00   adr rej H, L
       0x00, 0x0a, //00 10   liczba rej do odczytu H, L
       0x45, 0xd7  //45 d7   CRC L, H
     },
     { 0xFF }
   };
 
 
   if ( millis() >= timeoutOdpytanie ) {
     timeoutOdpytanie = millis() + CO_ILE_PYTAC_ORNO;
     timOdpowiedziOrno = millis();
 
     ++nrPytaniaOrno;
     if ( (zapytanie_514[nrPytaniaOrno][0] != 0xFF && typOrno == 514) ||
          (zapytanie_504[nrPytaniaOrno][0] != 0xFF && typOrno == 504) ) {
 
       uint8_t buf[8];
       if ( typOrno == 514 ) {
         memcpy( buf, (uint8_t*)zapytanie_514[nrPytaniaOrno], 6 );
       }
       else {
         memcpy( buf, (uint8_t*)zapytanie_504[nrPytaniaOrno], 6 );
       }
 
       uint16_t crc = CRC16( buf, 6 );
       buf[6] = crc;
       buf[7] = crc >> 8;
       wyslij_na_RS485( buf, 8 );
     }
     else {
       nrPytaniaOrno = -1;
       timeoutOdpytanie = millis() + PAZUZA_ZAPYTAN_ORNO;
 
         wyslij_na_RS485( dane, 17 );
       }
     }
   }
 }

Przy pracy w trybie odpytywania licznika cyklicznie wywoływana funkcja wysyła zapytanie do licznika. Ma inną postać dla urządzeń WE504 i WE514 – listing 3.

Podczas prób z licznikami ujawniła się pewna wada modeli WE514/517. Bez względu na liczbę zapytanych rejestrów licznik odpowie tylko jednym. Odpytanie wszystkich rejestrów związanych z pomiarem mocy, napięcia itp. dla licznika 3-fazowego, przy maksymalnej dopuszczalnej prędkości komunikacji, zajmuje ok. 900 ms. Czas transmisji jest stosunkowo długi w porównaniu do liczby przesłanych danych – od wysłania zapytania do rozpoczęcia transmisji odpowiedzi mija 40...50 ms. W przypadku WE504 odpowiedzi są odsyłane w losowym czasie 28...100 ms. Przy prędkości transmisji 9600 b/s (czas bajtu ok. 1 ms) jest trwa to długo.

Listing 4. Kod funkcji Uart_do_Wifi() analizującej odpowiedzi napływające z licznika

    uint16_t crcRX = sbuf[len - 2] | sbuf[len - 1] << 8;
     uint16_t crcCalculate = CRC16( sbuf, len - 2 );
     if ( crcRX != crcCalculate ) {
       if ( rdORNOfull ) rdORNOfull--; // licznik kompletu danych
       sprintf( txt, ANSI_PEN_RED"\n\rCRC Error! Wyliczone %x otrzymane %x"ANSI_PEN_WHITE, crcCalculate, crcRX ); printDebug( txt );
       LedFlash( LED_R, 500 );
     }
     else {
       if ( rdORNOfull < LICZBA_PYTAN_ORNO * 2 ) rdORNOfull++;
       timeoutAskOrno = millis() + TIM_OFLINE_ORNO;
       printDebugModbus( ANSI_PEN_GREEN"\n\rCRC OK"ANSI_PEN_WHITE );
       LedFlash( LED_Y, 50 );

       if ( sbuf[1] != 3 ) {
         printDebug( ANSI_PEN_RED"\n\r Odrzucono ze wzgledu na kod funkcji."ANSI_PEN_WHITE );
         return;
       }
       if ( typOrno == 514 ) {
         switch ( nrPytaniaOrno ) {
           case 0:   // Pytanie o napiecie
             Napiecie = sbuf[3] << 8 | sbuf[4];
             sprintf( txt, "\n\rNapiecie=%d,%02d ", Napiecie / 100, Napiecie % 100  ); printDebugModbus( txt );
             break;
           case 1:   // Pytanie o prad
             Prad = sbuf[3] << 24 | sbuf[4] << 16 | sbuf[5] << 8 | sbuf[6];
             sprintf( txt, "Prad=%d,%03d ", Prad / 1000, Prad % 1000 ); printDebugModbus( txt );
             break;
           case 2:   // Pytanie o moc czynna
             MocCzynna = sbuf[3] << 24 | sbuf[4] << 16 | sbuf[5] << 8 | sbuf[6];
             sprintf( txt, "MocCzynna=%d,%03d ", MocCzynna / 1000, MocCzynna % 1000 ); printDebugModbus( txt );
             if ( MocCzynna > MAX_MOC_CZYNNA ) printDebugModbus( "\n\r********* Bzdurna moc czynna !!!!!!!!!\n\r" );
#ifndef MODBUS_HEX_ALL
             if ( MocCzynna > MAX_MOC_CZYNNA )
#endif
             {
               fl_bzdura = len;
               if ( fl_bzdura > LEN_ODP_ORNO ) fl_bzdura = LEN_ODP_ORNO;
               memcpy( BufOdpOrno, sbuf, len );    // Kopia odebranej ramki

               stModbus = MocCzynna;
             }
             break;
           case 3:   // Pytanie o moc bierna
             MocBierna = sbuf[3] << 24 | sbuf[4] << 16 | sbuf[5] << 8 | sbuf[6];
             sprintf( txt, "MocBierna=%d,%03d ", MocBierna / 1000, MocBierna % 1000 ); printDebugModbus( txt );
             break;
           case 4:   // Pytanie o PF
             WspolczynnikMocy = sbuf[3] << 8 | sbuf[4];
             sprintf( txt, "WspolczynnikMocy=%d,%03d ", WspolczynnikMocy / 1000, WspolczynnikMocy % 1000  ); printDebugModbus( txt );
             break;
           case 5:   // Pytanie o zuzyta energie czynna
             EnergiaCzynna = sbuf[3] << 24 | sbuf[4] << 16 | sbuf[5] << 8 | sbuf[6]; // Total, kolejne UINT32 to taryfy T1, T2, T3 i T4
             sprintf( txt, "EnergiaCzynnaa=%d,%03d ", EnergiaCzynna / 1000, EnergiaCzynna % 1000 ); printDebugModbus( txt );
             break;
           case 6:   // Pytanie o zuzyta energie bierna
             EnergiaBierna = sbuf[3] << 24 | sbuf[4] << 16 | sbuf[5] << 8 | sbuf[6]; // Total, kolejne UINT32 to taryfy T1, T2, T3 i T4
             sprintf( txt, "EnergiaBierna=%d,%03d ", EnergiaBierna / 1000, EnergiaBierna % 1000 ); printDebugModbus( txt );
             break;
           case 7:   // Pytanie o moc pozorna
             MocPozorna = sbuf[3] << 24 | sbuf[4] << 16 | sbuf[5] << 8 | sbuf[6];
             sprintf( txt, "MocPozorna=%d,%03d ", MocPozorna / 1000, MocPozorna % 1000 ); printDebugModbus( txt );
             break;
           case 8:   // Pytanie o czestotliwosc
             Czestotliwosc = sbuf[3] << 8 | sbuf[4];
             sprintf( txt, "Czestotliwosc=%d,%02d ", Czestotliwosc / 100, Czestotliwosc % 100 ); printDebugModbus( txt );
             break;
         }
       }
       else {
         //0 Napięcie (0,1V),
         //1 Natężenie (0,1A),
         //2 Częstotliwość (0,1Hz),
         //3 Moc czynna (1W),
         //4 Moc bierna (1var),
         //5 Moc pozorna (1VA),
         //6 Współczynnik mocy (1000),
         //7 i 8 Energia czynna (1Wh), Decimal Long - little endian
         //9 i A Energia bierna (1varh), Decimal Long - little endian

         // Rejestr 0 (buf = 0*2+3=3)
         Napiecie = sbuf[3] << 8 | sbuf[4]; Napiecie *= 10;
         sprintf( txt, "\n\rNapiecie=%d,%02d ", Napiecie / 100, Napiecie % 100  ); printDebugModbus( txt );
         // Rejestr 1 (buf = 1*2+3=5)
         Prad = sbuf[5] << 8 | sbuf[6]; Prad *= 100;
         sprintf( txt, "Prad=%d,%03d ", Prad / 1000, Prad % 1000 ); printDebugModbus( txt );
         Czestotliwosc = sbuf[7] << 8 | sbuf[8]; Czestotliwosc *= 10;
         sprintf( txt, "Czestotliwosc=%d,%02d ", Czestotliwosc / 100, Czestotliwosc % 100 ); printDebugModbus( txt );
         MocCzynna = sbuf[9] << 8 | sbuf[10];
         sprintf( txt, "MocCzynna=%d,%03d ", MocCzynna / 1000, MocCzynna % 1000 ); printDebugModbus( txt );
         MocBierna = sbuf[11] << 8 | sbuf[12];
         sprintf( txt, "MocBierna=%d,%03d ", MocBierna / 1000, MocBierna % 1000 ); printDebugModbus( txt );
         MocPozorna = sbuf[13] << 8 | sbuf[14];
         sprintf( txt, "MocPozorna=%d,%03d ", MocPozorna / 1000, MocPozorna % 1000 ); printDebugModbus( txt );
         // Rejestr 6 (buf = 6*2+3=15)
         WspolczynnikMocy = sbuf[15] << 8 | sbuf[16];
         sprintf( txt, "WspolczynnikMocy=%d,%03d ", WspolczynnikMocy / 1000, WspolczynnikMocy % 1000  ); printDebugModbus( txt );
         // Rejestr 7+8 (buf = 7*2+3=17)
         EnergiaCzynna = sbuf[17] << 24 | sbuf[18] << 16 | sbuf[19] << 8 | sbuf[20];
         sprintf( txt, "EnergiaCzynnaa=%d,%03d ", EnergiaCzynna / 1000, EnergiaCzynna % 1000 ); printDebugModbus( txt );
         // Rejestr 9+10 (buf = 9*2+3=21)
         EnergiaBierna = sbuf[21] << 24 | sbuf[22] << 16 | sbuf[23] << 8 | sbuf[24];
         sprintf( txt, "EnergiaBierna=%d,%03d ", EnergiaBierna / 1000, EnergiaBierna % 1000 ); printDebugModbus( txt );
         // SN - Rejestr 11+12 (buf = 11*2+3=25)
         // BAUD RATE - Rejestr 14 (buf = 14*2+3=31)
         // ID - Rejestr 15 (buf = 15*2+3=33)

         if ( rdORNOfull < LICZBA_PYTAN_ORNO ) rdORNOfull = LICZBA_PYTAN_ORNO;

         if ( MocCzynna > MAX_MOC_CZYNNA ) printDebugModbus( ANSI_PEN_RED"\n\r********* Bzdurna moc czynna !!!!!!!!!"ANSI_PEN_WHITE );
#ifndef MODBUS_HEX_ALL
         if ( MocCzynna > MAX_MOC_CZYNNA )
#endif
         {
           fl_bzdura = len;
           if ( fl_bzdura > LEN_ODP_ORNO ) fl_bzdura = LEN_ODP_ORNO;
           memcpy( BufOdpOrno, sbuf, len );    // Kopia odebranej ramki
         }

Po małej dygresji na temat funkcjonowania oprogramowania licznika, wróćmy do zasady działania interfejsu. Odpowiedzi napływające z licznika są analizowane w funkcji Uart_do_Wifi() której kod jest zawarty na listingu 4 (ze względu na objętość dostępny w materiałach dodatkowych). Po krótkiej analizie widać wiele funkcji debugujących. Były potrzebne, ponieważ czasem rejestrowana moc znacznie odbiegała od rzeczywiście pobieranej. Z istotnych funkcji warto o napisać o void obsluga_thingspeak() pokazanej na listingu 5.

Listing 5. Kod funkcji, której zadaniem jest wysyłanie danych na serwer ThingSpeak

 void obsluga_thingspeak() {
 
   if ( ! ee.fl_ThingOn ) return;
 
 
   if (WiFi.status() == WL_CONNECTED)  {
     if ( millis() > CzasDoTHINGSPEAK && rdORNOfull >= LICZBA_PYTAN_ORNO ) {
       CzasDoTHINGSPEAK = millis() + Co_ILE_NA_THINGSPEAK + random( 0, 1000 );
 
       printDebug( ANSI_PEN_YELLOW"\n\rSend Thingspeak..."ANSI_PEN_WHITE );
       LedOn( LED_B );
       LedOff( LED_SCK );
 
       //-----
       uint32_t tim = millis();
       ThingSpeak.setField(1, (float)(WspolczynnikMocy) / 1000 );
       ThingSpeak.setField(2, (float)MocCzynna );
       ThingSpeak.setField(3, (float)MocBierna );
       ThingSpeak.setField(4, (float)MocPozorna );
       ThingSpeak.setField(5, (float)(Prad) / 1000 );
       ThingSpeak.setField(6, (float)(Napiecie) / 100 );
       ThingSpeak.setField(7, (float)(Czestotliwosc) / 100 );
       if ( ip[3] == 52 ) {
         ThingSpeak.writeFields(thingspeak_myChannelNumber, thingspeak_apiKey_sasedw);
       }
       else if ( ip[3] == 53 ) {
         ThingSpeak.writeFields(thingspeak_myChannelNumber, thingspeak_apiKey_rmikliczniki);
       }
       else if ( ip[3] == 54 ) {
         //todo: uzupelnic jak uzywany wykre
       }
       char txt[50];
       sprintf( txt, " %dms", millis() - tim ); printDebug( txt );
     }
     LedBlink( LED_B, TIM_BLINK_RUN );     //----- Miganie diody RUN -----//
     LedBlink( LED_SCK, TIM_BLINK_RUN );     //----- Miganie diody RUN -----//
   }
 }

Której zadaniem jest wysyłanie danych na serwer ThingSpeak. Niestety, operacja ta blokuje działanie programu na ponad 5 sekund. W tym czasie, nie będzie działać link VCOM. Ze względu na ten fakt, lepiej skorzystać z wysyłania danych na serwer WWW w sieci lokalnej czy w Internecie.

Listing 6. Funkcja umożliwiająca wysłanie danych na serwer WWW w sieci lokalnej lub w Internecie

 obsluga_www() {
   webServer.handleClient();       //----- serwer HTTP -----//
 
 
   if (WiFi.status() == WL_CONNECTED)  {
 
     if ( millis() > CzasDoURL ) {
       CzasDoURL = millis() + Co_ILE_NA_URL + random( 0, 500 );
 
       //----- WWW (RPi)
       byte static cnt;
       if ( ++cnt >= Co_ILE_NA_URL_LOCAL_HOST ) cnt = 0;
       for (byte www = 0; www < LICZBA_HOSTOW; www++) {
         WiFiClient client;
         const int httpPort = 80;
         const char* host;
 
         if ( www == 0 ) host = DATA_HOST_REMOTE_1;
         else if ( www == 1 ) host = DATA_HOST_REMOTE_2;
         else if ( www == 2 ) host = DATA_HOST_LOCAL_2;
         else host = DATA_HOST_LOCAL_1;
         if ( host == "" ) { // Jesli nazwa hosta istnieje
           printDebug( ANSI_PEN_YELLOW"\n\rPomijam wylaczony host"ANSI_PEN_WHITE );
         }
         else {
           if (!client.connect(host, httpPort)) {
             printDebug( ANSI_PEN_RED"\n\rERROR: connection failed"ANSI_PEN_WHITE );
           }
           else {
             String url = LOGGER_URL;
             char txt[1000];
             if ( ! ornoOnLine ) { // Jesli off-line
               url += "moc=’off-line";    // Gdy nie ma komunikacji z licznikiem
               url += "&pf=’off-line";
             }
             else {
               url += "moc=";
               sprintf( txt, "%d,%03d", MocCzynna / 1000, MocCzynna % 1000 );
               url += txt;
 
               url += "&pf=";
               sprintf( txt, "%d,%03d", WspolczynnikMocy / 1000, WspolczynnikMocy % 1000 );
               url += txt;
 
               url += "&var=";
               sprintf( txt, "%d,%03d", MocBierna / 1000, MocBierna % 1000 );
               url += txt;
 
               url += "&VA=";
               sprintf( txt, "%d,%03d", MocPozorna / 1000, MocPozorna % 1000 );
               url += txt;
 
               url += "&I=";
               sprintf( txt, "%d,%03d", Prad / 1000, Prad % 1000  );
               url += txt;
 
               url += "&U=";
               sprintf( txt, "%d,%02d", Napiecie / 100, Napiecie % 100  );
               url += txt;
 
               url += "&f=";
               sprintf( txt, "%d,%02d", Czestotliwosc / 100, Czestotliwosc % 100  );
               url += txt;
 
               url += "&EP=";
               sprintf( txt, "%d,%03d", EnergiaCzynna / 1000, EnergiaCzynna % 1000 );
               url += txt;
 
               url += "&Evar=";
               sprintf( txt, "%d,%03d", EnergiaBierna / 1000, EnergiaBierna % 1000 );
               url += txt;
 
               if ( fl_bzdura ) { // Nienormalne ramki
                 url += "&modbus=";
                 for ( byte x = 0; x < fl_bzdura; x++) {
                   sprintf( txt, "%02x_", BufOdpOrno[x]  );
                   url += txt;
                 }
                 url += "&st=";
                 if ( rdORNOfull < LICZBA_PYTAN_ORNO ) strcpy( txt, "no-complit" );  // jeśli dane niekompletne
                 else sprintf( txt, "%d", stModbus  );
                 url += txt;
                 if ( www == LICZBA_HOSTOW - 1 ) fl_bzdura = false;  // Flage kasujemy gdy wysylka na ostatni host
               }
             }
             if ( ip[3] == 52 ) url += "&id=glowny"; //sprintf( txt, "%d", ip[3] ); url += "&id=";
             else if ( ip[3] == 53 ) url += "&id=warsztat";
             else if ( ip[3] == 54 ) url += "&id=przenosny";
             else {
               url += "&id=nowy";
               sprintf( txt, " %d", ip[3] ); url += txt;
             }
 
             uint32_t tim = timWwwStat = micros();
             timWwwEnd = 0;
             if ( www == 0 || www == 1 || ( www >= 2 && !cnt) ) { // na zdalny (www==0 lub 1) zawsze, lokalny (www<>0 i 1) tylko gdy cnt==0
 #ifdef DEBUG_URL
               if ( www >= 2 ) printDebug( "\n\rSend >>> Local <<< host ‘" );
               else printDebug( "\n\rSend Remote host ‘" );
               printDebug( (char*)host );
               strcpy( txt, url.c_str() ); printDebug( txt );
               sprintf( txt, " [cnt=%d] ", cnt ); printDebug(txt);
               printDebug( "’" );
 #endif
               client.print( String("GET ") + url + " HTTP/1.1\r\n" + "Host: " + host + "\r\n" + "Connection: close\r\n\r\n" );
 #ifdef DEBUG_URL
               sprintf( txt, "\n\r Send GET %d.%02dms ", (micros() - tim) / 1000, (micros() - tim) % 1000 ); printDebug( txt ); //Czas na jaki angarzowany jest uC
 #endif
 #ifdef REPLY_WWW
               //----- Odpowiedz z WWW
               uint32_t timeout = millis() + OVERTIME_REPLY_WWW;
               while ( client.connected() || client.available() ) {
                 if ( millis() >= timeout ) break;
                 if ( client.available() ) {
                   if ( ! timWwwEnd ) timWwwEnd = micros();
                   String line = client.readStringUntil(‘\n’);
 #if defined( DEBUG_PRINT_REP_WWW ) && defined( DEBUG_UART1 )
                   printDebug( ANSI_PEN_CYAN"\n\rOdpowiedz ze strony \n\r"ANSI_PEN_YELLOW );
                   Serial1.println(line); //todo: printDebug
                   printDebug( ANSI_PEN_WHITE"\n\r" );
 #endif
                 }
               }
 #ifdef DEBUG_URL
               sprintf( txt, "\n\r Read HTML %d.%02dms", (timWwwEnd - timWwwStat) / 1000, (timWwwEnd - timWwwStat) % 1000 ); printDebug( txt ); // Czas odpowiedzi z WWW
 #endif
 #endif
             }
           }
         }
       }
     }
   } // END if ( WiFi.status() == WL_CONNECTED )
 }

Kod funkcji umożliwiającej to zadanie pokazuje listing 6 (ze względu na objętość dostępny w materiałach dodatkowych). Wysłanie danych poprzez URL może być analizowane skryptem PHP pokazanym na listingu 7. Dane są zapisywane w formacie CSV, zajmuje to 50...80 ms w przypadku sieci lokalnej, 150..180 ms w Internecie. Skrypt zapisuje dane z różnych liczników pod osobnymi nazwami, ponadto dzieli na okresy dni i miesiące.

Listing 7. Skrypt PHP umożliwiający analizę danych wysłanych poprzez URL

<?
$stoper_start = microtime(true); // start pomiaru
?>
<HTML>
<HEAD>
<META http-equiv="Content-Type" content="text/html; charset=ISO-8859-2">
<title>Automatyka domowa</title>
</HEAD>
<BODY>
<?
// URL.php?moc=123&pf=1.0&id=30&modbus=11_22_33
// id - identyfikator urządzenia, ciąg znaków dodawany do nazwy pliku (adres ip nadawcy)
// przydatne dla serwerów zdalnych (jedno IP dla kilku urządzeń)

ini_set( ‘display_errors’, ‘On’ );
error_reporting( E_ALL );


include "data_time.php";

$adr=strval($_SERVER[‘REMOTE_ADDR’]);
$ip=explode(".",$adr); # tworzy tablice z czlłnami adresu IP
$ip1="$ip[0]"; # zmienna 1 czlon adresu ip "xxx"
$ip2="$ip[0].$ip[1]"; # zmienna 2 czlony adresu ip "xxx.xxx"
$ip3="$ip[0].$ip[1].$ip[2]"; # zmienna 3 czlony adresu ip "xxx.xxx.xxx"
$ip4="$ip[0].$ip[1].$ip[2].$ip[3]"; # zmienna 3 czlony adresu ip "xxx.xxx.xxx"

// if( $ip1=="192" )

echo $_SERVER[‘HTTP_HOST’].’<BR>’.$_SERVER[‘REQUEST_URI’];
echo ‘<BR>elementow ‘.count($_GET);
echo ‘<BR>’.$_GET["id"];
echo ‘<BR>’.$_GET["moc"];
echo ‘<BR>’.$_GET["pf"];
echo ‘<BR>’.$_GET["var"];
echo ‘<BR>’.$_GET["AV"];
echo ‘<BR>’.$_GET["I"];
echo ‘<BR>’.$_GET["U"];
echo ‘<BR>’.$_GET["f"];
echo ‘<BR>’.$_GET["EP"];
echo ‘<BR>’.$_GET["Evar"];
echo ‘<BR>’.$_GET["modbus"];
echo ‘<BR>’.$_GET["st"];


for($zapis=0; $zapis<2; $zapis++){
$nazwa_pliku = ‘data/’;
if( $ip[0] == "192" ) $nazwa_pliku = $nazwa_pliku.$ip[0].’_’.$ip[1].’_’.$ip[2].’_’.$ip[3].’-’.$_GET["id"]; // Siec lokalna
else $nazwa_pliku = $nazwa_pliku.$_GET["id"]; // Zdalna
if( $zapis == 0 ) $nazwa_pliku = $nazwa_pliku."_".$rok."-".$miesiac."-".$dzien; // Plik dzienny
else $nazwa_pliku = $nazwa_pliku."_".$rok."-".$miesiac; // Plik miesięczny
$nazwa_pliku = $nazwa_pliku.’.csv’;

echo ‘<BR>nazwa pliku: "’.$nazwa_pliku.’"<P>’;

$rekord = $TimeAll.";".$_GET["moc"].";".$_GET["pf"].";".$_GET["var"].";".$_GET["VA"].";".$_GET["I"].";".$_GET["U"].";".$_GET["f"].";".$_GET["EP"].";".$_GET["Evar"].";’".$_GET["modbus"].";".$_GET["st"]."\n\r";
echo ‘<BR>rekord ‘.$rekord;

if ( $HlogWR = fopen($nazwa_pliku,"a") ) // Zapisanie logu, katalog musi mieć uprawnienia 777
{
flock($HlogWR,2);

fwrite($HlogWR, $rekord );

flock($HlogWR,3);
fclose($HlogWR);
}
}


$stoper_stop = microtime(true); //koniec pomiaru

echo "<P>Czas wykonania skryptu ".(($stoper_stop-$stoper_start)*1000)." ms"; // wynik np 1.0123 sekundy
?>

Jakkolwiek użycie własnych skryptów jest lepsze, bo nie wnosi ograniczeń co do liczby zapisanych danych (co mam miejsce na ThingSpeak nawet w wersji płatnej). Ma to też wadę ujawniającą się, gdy serwer jest nieosiągalny. W takiej sytuacji program jest zablokowany przez timeout trwający 5 sekund. Skutecznym rozwiązaniem jest skorzystanie z UDP. Testową funkcję można znaleźć pod nazwą obsluga_udp().

Uruchomienie

Zanim program zostanie wgrany do płytki, należy zmodyfikować kody źródłowe. W pierwszej kolejności należy skonfigurować siec bezprzewodową. W tym celu odnajdujemy w kodzie definicje:

#define WIFI_SSID "ssid"
#define WIFI_PASS "pass"

i wpisujemy dane własnej sieci.

Jeżeli chcemy nadać statyczny adres ip komentujemy linie:

//#define DHCP

oraz wpisujemy dane własnej sieci:

IPAddress staticIP(192, 168, 0, 101);
IPAddress gateway(192, 168, 0, 1);
IPAddress subnet(255, 255, 255, 0);

Należy jeszcze wybrać płytkę, której używamy:

#define _WEMOS 1 // 0-D1, 1-MINI

Jeśli będziemy korzystać z Thingspeak wpisujemy dane uzyskane w procesie rejestracji:

const char * thingspeak_apiKey = "apikey";

Odnajdując w kodzie frazę if ( ip[3] == 52 ) można uzależnić linki od adresu płytki. Fragmrenty do modyfikacji wyróżniono kolorem:

if ( ip[3] == 52 ) {
strcat( buf, "<A HREF=’https://thingspeak.com/channels/xxxxxxxx’ TARGET=’_blank’ >Rejestrator</A>\r" );
if ( ! ee.fl_ThingOn ) strcat( buf, "<font color=’RED’> (Off)</font>" );
strcat( buf, "</TD><TD><input type=’button’ value=’Reload Page’ onClick=’document.location.reload(true)’>\r" );
strcat( buf, "</TD></tr></TABLE>\r" );
strcat( buf, "<P><iframe width=’450’ height=’260’ style=’border: 1px solid #cccccc;’ src=’https://thingspeak.com/channels/xxxxxxxx/charts/2? bgcolor=%23ffffff&color=%23d62020 &dynamic=true&results=60&title= Moc+G%C5%82%C3%B3wny&type=line’> < / iframe >" );
}

Gdy dane będą zapisywane na własnym serwerze należy wypełnić:

#define DATA_HOST_LOCAL_1 "192.168.0.120" // IP lokalnego serwera
#define DATA_HOST_LOCAL_2 "192.168.2.121" //IP lokalnego serwera
#define DATA_HOST_REMOTE_1 "domena.pl" //IP serwera, może byc nazwa domenowa
#define DATA_HOST_REMOTE_2 "domena2.pl" //IP serwera, może byc nazwa domenowa

Jeśli serwerów jest mniej, zamiast adresu IP czy nazwy należy wpisać ””.

Definicja:

#define LOGGER_URL "/sciezka/nazwa-skryptu.php?"

określa ścieżkę dostępu do skryptu PHP, który zapisuje dane. W przypadku prób z UDP należy uzupełnić:

#define BROADCAST "192.168.0.255"
unsigned int localUdpPort = 2102;

Dla ułatwienia, wszystkie miejsca w kodzie, które należy zmodyfikować, są odpowiednio oznaczone.

Rysunek 3. Wybór płytki, na którą kompilujemy program

Po wprowadzeniu zmian należy wybrać płytkę, na którą kompilujemy program, jak pokazano na rysunku 3, oraz port COM, po czym należy skompilować i wgrać program na płytkę. W ustawieniach routera warto zadbać o to, aby płytka dostawała zawsze ten sam adres IP ewentualnie skonfigurować adres statyczny.

Rysunek 4. Konfiguracja programu USR-VCOM

Jeśli zamierzamy korzystać z VCOM należy zainstalować program USR-VCOM dostępny na stronie http://bit.ly/3on789v. Po zainstalowaniu i uruchomieniu należy nacisnąć przycisk ADD (rysunek 4). W oknie, które się pojawi, należy wybrać numer wirtualnego COM pod jakim ma być dostępny link. Protokół komunikacyjny należy zmienić z domyślnego TCP na UDP.

Pozostaje wpisać adres IP interfejsu oraz ustawić port na 8888.

Rysunek 5. Strona www, na której można zobaczyć aktualne parametry monitorowane przez licznik

Pod adresem IP interfejsu dostępna jest strona WWW. Można na niej zobaczyć aktualne parametry monitorowane przez licznik ORNO (rysunek 5). Pod linkiem FW dostępne są informacje o wersji programu, dacie konfiguracji oraz jest możliwość włączenia bądź wyłączenia rejestracji na serwerze Thingspeak oraz wyboru typu licznika (rysunek 6).

Rysunek 6. Konfiguracja parametrów licznika przez stronę www

Link „Rejestrator” kieruje na stronę Thingspeak, na której są reprezentowane dane wysyłane przez interfejs.

Uwagi końcowe

Płytkę WemosMini wraz z driverem RS485 można zmieścić w obudowie Z-105, natomiast razem z zasilaczem IRM01-5 w Z-106. Przy wyborze modelu licznika należy wziąć pod uwagę fakt, że model WE504 przy poborze mocy przez odbiornik poniżej 12 W zachowuje się niestabilnie. Raz pokazuje 0, innym razem rzeczywiście pobieraną moc. W tym samym czasie, ORNO-514 poprawnie pokazuje pobór mocy nawet o wartości 2...3 W.

Rysunek 7. Program dostarczony przez producenta licznika

Nie są to błędy w komunikacji, co potwierdzono analizatorem logicznym, jak i przy pomocy programu dostarczonego przez producenta (rysunek 7). Jeżeli więc licznik ma mierzyć małe prądy trzeba zrezygnować z WE504 na rzecz WE514.

W ramkach danych MODBUS dla ORNO brak konsekwencji przy przesyłaniu danych 16 i 32 bit. Same dane 16-bitowe, wymagane przez MODBUS, wysyłane są poprawnie – najpierw młodszy, później starszy bajt, natomiast w polu danych najstarszy bajt wysyłany jest jako pierwszy. Trzeba o tym pamiętać bo łatwo o pomyłkę podczas analizy ramki danych.

W materiałach dodatkowych można znaleźć poza kodami źródłowymi programu, logi zarejestrowane analizatorem LA2016. Logi można obejrzeć darmowym programem dostępnym na stronie producenta http://bit.ly/3hTwHNk.

Rysunek 8. Informacje diagnostyczne wysyłane na port szeregowy UART1

Na port szeregowy UART1 wysyłane są informacje diagnostyczne (rysunek 8). O tym czy, jakie, i z jaką prędkością decydują definicje:

#define DEBUG_UART1
#define BAUD_DEBUG 921600

#define DEBUG_MODBUS
#define DEBUG_RTC
#define DEBUG_URL
#define DEBUG_PRINT_REP_WWW

Dokumentacja zawiera kilka błędów, aby uwolnić Czytelników od błądzenia w materiałach dodatkowych zamieściłem dodatkowy plik z moimi spostrzeżeniami i uwagami.

SaS, AVT
sas@elportal.pl

Artykuł ukazał się w
Elektronika Praktyczna
styczeń 2021
DO POBRANIA
Pobierz PDF Download icon
Materiały dodatkowe

Elektronika Praktyczna Plus lipiec - grudzień 2012

Elektronika Praktyczna Plus

Monograficzne wydania specjalne

Elektronik marzec 2021

Elektronik

Magazyn elektroniki profesjonalnej

Raspberry Pi 2015

Raspberry Pi

Wykorzystaj wszystkie możliwości wyjątkowego minikomputera

Świat Radio marzec 2021

Świat Radio

Magazyn krótkofalowców i amatorów CB

Automatyka Podzespoły Aplikacje marzec 2021

Automatyka Podzespoły Aplikacje

Technika i rynek systemów automatyki

Elektronika Praktyczna marzec 2021

Elektronika Praktyczna

Międzynarodowy magazyn elektroników konstruktorów

Praktyczny Kurs Elektroniki 2018

Praktyczny Kurs Elektroniki

24 pasjonujące projekty elektroniczne

Elektronika dla Wszystkich kwiecień 2021

Elektronika dla Wszystkich

Interesująca elektronika dla pasjonatów