Anemometr TX23 a Wi-Fi ESP8266

20.04.2019

Intro

Sice nebydlíme na horách, ale někdy si tak připadám. Tedy nikoliv úhrnem srážek nebo sněhovou nadílkou (té je zde minimum), ale neustálým větrem. Předloni jsem ho silně podcenil a výsledkem jedné letní bouřky byl poházený dětský domeček po zahradě, převrácená lavice s masivu a při jednom podzimním větříku trampolína 1/2 kilometru v polích + ohlý sloupek plotu. Od té doby jsou mými kamarády beton, zemní vrut, lano a řetez. A nyní také anemometr, který mi může nabonzovat - fouká, bude průser :-)

Pořídil jsem si anemometr TX23 z meteoshop.cz. Jedná se o kousek, který byl jako náhradní díl k meteostanici WS1600. Našel jsem ho však i pod jinými značkami. O nějaké extra přesnosti si nedělám iluze, ale pro základní orientaci stačí.

Komunikační protokol

Komunikační protokol je možné snadno najít na Internetu. Zde jsou linky, ze kterých jsem vycházel:

Níže následuje kompilát s doplněnými vlastními postřehy.

Fyzické zapojení

Z čidla vede kabel se čtyřmi vodiči. Jejich význam je následující:

Barva vodiče Význam
červená (spíše růžová) napájení 3.3 V
žlutá GND
hnědá data
zelená (světle) nezapojeno

Komunikační sběrnice je podobná jako one-wire, tedy s otevřeným kolektorem a systémem master/slave. Čidlo TX23 je vždy slave. Pomocí tzv. pull-up rezistoru musí být na sběrnici definována klidová úroveň log. 1. Master (Arduino) nebo slave (TX23) ji během komunikace "stáhnou" k GND v případě, že se má přenášet log. 0 nebo nechají být, pokud se má přenášet log. 1.

Protokol

Sekce Význam
A Master stáhne sběrnici na log.0 na dobu 500ms
B Sběrnice se vrátí do stavu log.1 Délka cca. 1220 us.
C Slave stáhne sběrnici na log.0 na dobu cca. 20ms.
D Slave pošle na sběrnici fixní hlavičku 5-ti bitů 11011. Podle této hlavičky se dá vypočítat rychlost přenosu jednotlivých bitů. Ta se totiž dle teploty mění a mě na stole se pohybovala mezi 1212-1224 uS. Pokud by se v ovládacím programu měření neprovedlo, tak v extrémním případě může dojít k situaci, kdy by vzorkování stavu následujících bitů ujelo takovým způsobem, že by se detekovaly stavy bitů sousedních. Měření stačí provést tak, že se detekuje vzestupná a sestupná hrana prvních dvou bitů hlavičky a výsledná hodnota se vydělí dvěma.
E Slave pošle 4-bitovou hodnotu směru větru. Rozlišení je tedy 16 různých směrů a význam jednotlivých hodnot je v tabulce níže.
F Slave pošle 12-bitovou hodnotu rychlosti větru. Horní 3 bity jsou vždy nula, takže defakto vrací číslo v intervalu 0-511. Jedná se o rychlost v desetinách metrů za sekundu. Př. 85 = 8.5 m/s, což je 30.6 km/h.
G Slave pošle 4 bity představující kontrolní součet hodnot směru a rychlosti větru. Vysvětleno níže. Lze využít pro kontrolu integrity komunikace.
H Slave pošle 4 bity představující bitově invertovanou hodnotu směru větru. Tedy jednoduše se vezmou bity zaslané v sekci E a ty se bit po bitu invertují. Lze využít pro kontrolu integrity komunikace.
I Slave pošle 12 bitů představující bitově invertovanou hodnotu rychlosti větru. Tedy jednoduše se vezmou bity zaslané v sekci F a ty se bit po bitu invertují. Lze využít pro kontrolu integrity komunikace.

Čidlo posílá všechny hodnoty v pořadí LSB->MSB, takže je nutné převrátit pořadí jednotlivých bitů nebo je již při čtení zapisovat na správnou pozici v rámci byte. Příklad: Pokud čidlo pošle v sekci E sekvenci bitů 1101 (v tomto pořadí), je nutné do unsigned char proměnné směru větru zapsat 00001011, tedy desítkově 11. Analogicky pro větší počet bitů reprezentující rychlost.

Tabulka směrů

Platí za předpokladu, že se čidlo správně orientuje dle označení na krytu.

Hodnota sekce E Směr
0 sever (0°)
1 severo-severovýchod (22.5°)
2 severovýchod (45°)
3 východo-severovýchod (67.5°)
4 východ (90°)
5 východo-jihovýchod (112.5°)
6 jihovýchod (135°)
7 jiho-jihovýchod (157.5°)
8 jih (180°)
9 jiho-jihozápad (202.5°)
10 jihozápad (225°)
11 západo-jihozápad (247.5°)
12 západ (270°)
13 západo-severozápad (292.5°)
14 severozápad (315°)
15 severo-severozápad (337.5°)

Bitové inverze

Čidlo posílá v sekci H bitově invertovanou hodnotu sekce E a v sekci I bitově invertovanou hodnotu sekce G. Kontrola integrity je prostá - bitově invertované hodnoty bitově invertujeme ještě jednou a měli bychom dostat výsledek shodný jako hodnoty v neinvertovaných sekcích.

Příklad: Invertovanou hodnotu ze sekce H 0100 opětovně bitově invertujeme a dostaneme 1011. To se rovná hodnotě ze sekce E. Při opětovném invertování je však nutné dávat pozor na to, že hodnoty máme zpravidla uloženy v datovém typu, který je delší než počet načítaných bitů z čidla. Bitovou inverzí (operátor ~) se invertuje celý byte (8 bitů) nebo integer (16 (32) bitů) a pak je nutné nepotřebné bity nastavit na nulu.Pokud je invertovaná hodnota uložena v datovém typu unsigned char, tak tento byte obsahuje 00000100. Invertováním dostaneme 11111011, což rozhodně není to, co je načteno z neinvertované sekce 00001011. V případě tého 4-bitové hodnoty je řešením logický součin s maskou 0x0F, tedy 11111011 & 00001111 = 00001011.

Kontrolní součet

Kontrolní součet představuje spodní 4 bity aritmetického součtu směru větru a všech třech půlbyte 12-ti bitové hodnoty rychlosti větru. Příklad:

Směr větru ze sekce E: 1011
Rychlost větru ze sekce F: 0000 0101 0101

Hodnota Výběr bitů Příklad
Směr větru 3 - 0 1011
Rychlost větru 11 - 8 0000
Rychlost větru 7 - 4 0101
Rychlost větru 3 - 0 0101
spodní 4 bity součtu 1 0101

Kontrolní součet příkladu je 0101, což odpovídá sekci G.

Schéma a mechanické provedení

K uvaření pokrmu potřebujeme tyto ingredience:

Oranžovou hvězdičkou jsou označeny součástky, které slouží jako částečná ochrana vstupu ESP8266. Pull-up rezistor 4k7 představuje "silnější" pull-up než je k dispozici v ESP a slouží k zajištění strmějších vzestupných hran. Vzhledem k pomalosti komunikace čidla ho lze celkem bez obav vynechat (= nutné zapnout pull-up v ESP) - já jsem to však neudělal, neboť zajišťuje klidovou úroveň sběrnice i v situacích, kdy se ESP kousne nebo je do něj nahráván nový firmware.

Po zamíchání a půlhodinovém vaření vznikne toto:

Sketch pro Arduino ESP8266 (Wemos D1 mini)

Uvedený sketch má za úkol dělat toto:

V kódu nejsou využity časovače ani externí přerušení, takže je možné tyto funkcionality využít na cokoliv jiného.


#include <ESP8266WiFi.h>
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>

// nastaveni wifi
const char* ssid = "mojewifi";
const char* password = "supertajneheslo";
int failedWiFi = 0;


// struktura na data z TX23 a globani promenna vitr
typedef struct {
  unsigned char  smer;          // smer vetru 0-16
  unsigned int   rychlost;      // rychlost vetru v 0.1m/s, 0-512
  unsigned char  crc;           // kontrolni soucet
  unsigned char  smerInv;       // bitove invertovany smer
  unsigned int   rychlostInv;   // bitove invertovana rychlost
} TX23;
TX23 vitr;


// pin pripojeny na sbernici cidla TX23
// pres pullup rezistor 4k7 k 3V3
#define DATAPIN D2


// http client
HTTPClient http;
uint16_t httpFails = 0;

// casovac
uint32_t prevTime = 0;


// ------------------------------------------------------------------------------------------------
// SETUP
// ------------------------------------------------------------------------------------------------
void setup() {

  // -- disable SW wdt --
  ESP.wdtDisable();

  // -- seriovy port jako info konzole --
  Serial.begin(115200);

  // -- vychozi IO stav pinu pripojeneho na sbernici s TX23 --
  pinMode(DATAPIN, INPUT);

  // -- pripojeni do wifi --
  int maxRetries = 20;
  Serial.print("[WiFi] connecting...");
  WiFi.persistent(false); // neukladame nic do Flash - setrime ji
  WiFi.mode(WIFI_STA);
  WiFi.disconnect();
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.print('.');
    ESP.wdtFeed();
    if (!maxRetries--) while (1);
  }
  Serial.println(WiFi.localIP());

}
// ------------------------------------------------------------------------------------------------




// ------------------------------------------------------------------------------------------------
// LOOP
// ------------------------------------------------------------------------------------------------
void loop() {

  unsigned char i, err;


  if ((millis() - prevTime) >= 5000) {
    prevTime = millis();
    i = 3;
    while (i--) {
      err = readData(&vitr);
      Serial.println();
      Serial.print("Smer:     "); Serial.println(vitr.smer);
      Serial.print("Rychlost: "); Serial.println(vitr.rychlost);
      Serial.print("ErrCode:  "); Serial.println(err);
      if (err == 0) break;
    }
    sendviahttp(&vitr, err);

  }

  // -- smazani psa --
  ESP.wdtFeed();
}
// ------------------------------------------------------------------------------------------------





// ------------------------------------------------------------------------------------------------
// Funkce na precteni dat z anemometeru Lacrosse TX23U
// Vstup:    v - ukazatel pro promennou struktury TX23
// Vystup:   0 = vse je OK
//           1 = selhalo cekani na vzestupnou hranu prvniho bitu hlavicky
//           2 = selhalo cekani na sestupnou hrannu druheho bitu hlavicky
//           3 = hodnota zmerenoho bitrate je mimo meze
//           4 = chyba pri kontrole hodnoty smeru vetru
//           5 = chyba pri kontrole hodnoty rychlosti vetru
//           5 = chyba kontrolniho souctu
//           7 = cidlo nereaguje na start signal
// ------------------------------------------------------------------------------------------------
unsigned char readData(TX23 *v) {

  uint32_t bitrate, timeoutCnt;
  unsigned char k = 0, i = 0, b, crc;

  // -- smazani starych hodnot --
  v->smer = 0;
  v->rychlost = 0;
  v->crc = 0;
  v->smerInv = 0;
  v->rychlostInv = 0;

  // -- start signal, na 500ms stahneme sbernici na low --
  digitalWrite(DATAPIN, 0);
  pinMode(DATAPIN, OUTPUT);
  delay(500);
  pinMode(DATAPIN, INPUT);

  // -- po 1.2ms musi cidlo sbernici stahnout do low, pockame 5ms a zkontrolujeme --
  delay(5);
  if (digitalRead(DATAPIN) != 0) return (7);

  // -- nyni pockame na vzestupnou hranu prvniho bitu hlavicky a poznamename si cas --
  timeoutCnt = 2000000;
  while (digitalRead(DATAPIN) == 0) if (!timeoutCnt--) return (1);
  bitrate = micros();
  // -- nyni pockame na sestupnou hranu druheho bitu hlavicky a vypocitame bitrate --
  while (digitalRead(DATAPIN) == 1) if (!timeoutCnt--) return (2);
  bitrate = (micros() - bitrate) >> 1;
  // -- zkontrolujeme meze bitrate, standarte by melo byt 1200, lita to ale od 1217 do 1222 --
  if ( (bitrate < 1000) || (bitrate > 1400) ) return (3);

  // -- pockame 3.5 nasobek bitrate, dostaneme se tak na prostredek prvniho bitu po hlavicce --
  delayMicroseconds((uint16_t) 3 * bitrate + (bitrate >> 1) );

  // -- provedeme 36 cteni urovne sbernice s krokem bitrate --
  // -- dle poradi bitu zarazujeme do jednotlivych polozek struktury TX23
  // -- pri zapisu se otaci poradi bitu dle MSB->LSB, cidlo posila LSB jako prvni --
  while (i++ < 36) {
    b = digitalRead(DATAPIN);
    //Serial.print(b);
    if (i == 1) k = 0;
    if (i >= 1 && i <= 4) v->smer |= b << k++;
    if (i == 5) k = 0;
    if (i >= 5 && i <= 16) v->rychlost |= b << k++;
    if (i == 17) k = 0;
    if (i >= 17 && i <= 20) v->crc |= b << k++;
    if (i == 21) k = 0;
    if (i >= 21 && i <= 24) v->smerInv |= b << k++;
    if (i == 25) k = 0;
    if (i >= 25 && i <= 36) v->rychlostInv |= b << k++;
    delayMicroseconds((uint16_t) bitrate);
  }

  // -- kontrola, zda-li hodnota smeru odpovida invertovane hodnote --
  if (v->smer != ((~v->smerInv) & 0x0F)) return (4);

  // -- kontrola, zda-li hodnota rychlosti odpovida invertovane hodnote --
  if (v->rychlost != ((~v->rychlostInv) & 0x0FFF)) return (5);

  // -- vypocet CRC a porovnani s tim, co poslalo cidlo --
  crc = v->smer;
  crc += v->rychlost & 0x0F;
  crc += (v->rychlost >> 4) & 0x0F;
  crc += (v->rychlost >> 8) & 0x0F;
  crc = crc & 0x0F;
  if (v->crc != crc) return (6);

  // -- pokud vse dopadne dobre --
  return (0);

}
// ------------------------------------------------------------------------------------------------



// ------------------------------------------------------------------------------------------------
// Odeslani dat o vetru pres http get (verze funkcni 16.7.2021 s aktualnimi knihovnami)
// Vstup:    v - ukazatel pro promennou struktury TX23
//           errcode - navratova hodnota mereni cidla vetru
// ------------------------------------------------------------------------------------------------
void sendviahttp(TX23 *v, uint8_t errcode) {
  if ((WiFi.status() == WL_CONNECTED)) {
    WiFiClient client;
    HTTPClient http;
    char sendBuffer[500];
    sprintf(sendBuffer, "http://www.example.com/obsluhacidla.php?smer=%d&rychlost=%d&errcode=%d", v->smer, v->rychlost, errcode);
    if (http.begin(client, sendBuffer)) {
      int httpCode = http.GET();
      Serial.printf("http get return code: %d\n", httpCode);
      if (httpCode > 0) {
        if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) {
          Serial.print("http get response: ");
          String payload = http.getString();
          Serial.println(payload);
          httpFails = 0;
        }
      } else {
        Serial.printf("http get failed, error: %s\n", http.errorToString(httpCode).c_str());
        httpFails++;
      }
      http.end();
    } else {
      Serial.println("http get failed, unable to connect");
      httpFails++;
    }
  }
  if (httpFails > 180) while (1);
}
// ----------------------------------------------------------------------------------------------
  
  
  

Obsluha v PHP

Zde je úplně nejzákladnější možná obsluha v PHP obsluhacidla.php.

<?php
  foreach ($_GET as $val) if(!is_numeric($val)) die('int error');
  file_put_contents("data.dat", json_encode($_GET));
?>

Zde pak skript, který data zobrazí.

<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=windows-1250" />
  <META HTTP-EQUIV="Cache-Control" CONTENT="no-cache" />
  <META HTTP-EQUIV="pragma" CONTENT="no-cache" />
  <META HTTP-EQUIV="Expires" CONTENT="0" />
  <meta http-equiv="refresh" content="5">
</head>
<body>
<h1>Vitr</h1>
<pre>
<?php
$pole = json_decode(file_get_contents("data.dat"));
echo "Modtime = " . date ("d.m.Y H:i:s", filemtime("data.dat")) . "<br>";
echo "Rychlost: ".$pole->speed*0.32." km/h<br>";
$smer = array(
  0=>'sever (0°)',
  1=>'severo-severovýchod (22.5°)',
  2=>'severovýchod (45°)',
  3=>'východo-severovýchod (67.5°)',
  4=>'východ (90°)',
  5=>'východo-jihovýchod (112.5°)',
  6=>'jihovýchod (135°)',
  7=>'jiho-jihovýchod (157.5°)',
  8=>'jih (180°)',
  9=>'jiho-jihozápad (202.5°)',
  10=>'jihozápad (225°)',
  11=>'západo-jihozápad (247.5°)',
  12=>'západ (270°)',
  13=>'západo-severozápad (292.5°)',
  14=>'severozápad (315°)',
  15=>'severo-severozápad (337.5°)'  
);
echo "Smer: ".$smer[$pole->direction]."<br>";
?>
</pre>
</body>

Průměrování směru

Pro průměrování směru větru NELZE použít aritmetický průměr načtených hodnot (0-15). Proč? Pokud fouká severní vítr, oscilují hodnoty směru mezi 15 a 1 - tedy např. 0,1,15,1,0,0,15,0 apod. Aritmetický průměr je 4 (východ) - tedy nesmysl. Řešením je prostý vektorový součet viz. příklad níže. Mějme pole naměřených hodnot $poleSmeru. Pomocí interace přes toto pole si do proměnných $x a $y uložíme přírůstky v podobě koncových souřadnic vektoru daného úhlu s počátkem v 0,0. Následně funkcí atan2 zjistíme výsledný úhel. Pozor na jednotky - goniometrické funkce požadují úhel v radiánech.

$x=0;
$y=0;
foreach ($smer as $poleSmeru) {
  $x += sin( $smer * (pi()/8) );
  $y += cos( $smer * (pi()/8) );	 
}
$uhel = rad2deg(atan2($x, $y));
if($uhel<0) $uhel = 360 + $uhel;

$smerPrumer = $uhel / (360/16);