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.
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.
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).
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.
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).
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.
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.
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.
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.
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.
<?
$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.
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.
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.
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).
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.
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.
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