ESP8266 – wincyj mocy

Podłączenie przekaźnika do ESP8266

Pierwszym wyzwaniem jest fakt iż przekaźnik elektromagnetyczny zawiera cewkę, która gromadzi energię w postaci pola elektromagnetycznego. W momencie gdy na wejście przestaje być dostarczane napięcie cewka gwałtownie oddaje znaczną część energii w postaci prądu o wysokim napięciu, które potrafi zabić konia (#pdk), a co dopiero mały tani układ cyfrowy.

Rozwiązaniem jest wpięcie diody zaporowo (czyli kreską do plusa) równolegle do przekaźnika. Prąd wygenerowany z pola magnetycznego ma kierunek odwrotny niż prąd, którym była zasilana cewka przekaźnika, więc dioda jest dla niego skierowana przepustowo – cały prąd popłynie przez nią i zamieni się w ciepło.

Ponownie gotowe moduły przekaźników zazwyczaj zawierają odpowiednią diodę.

Kolejną przeszkodą jest fakt iż na wyjściach ESP8266 znajduje się prąd o napięciu 3.3V zaś większość przekaźników działa w zakresie napięć 5-24V. Przydałby się „słabszy przekaźnik”, który wysteruje prądem o napięiu 3.3V prąd o troszeczkę większym.

Takim przekaźnikiem może być tranzystor bipolarny. Ja w swojej skrzyneczce posiadam tranzystory npn BD135-16.

Tranzystor bipolarny posiada 3 nóżki: bazę, kolektor i emiter. W przypadku tranzystorów npn przepływ prądu między bazą a emiterem znacząco obniża opór między emiterem a kolektorem. Przy czym emiter powinien być podłączony do wspólnej masy.

Na tym etapie pojawia się pytanie: Czy można zrezygnować z przekaźnika i wykorzystać sam tranzystor? Odpowiedź brzmi: to zależy.

Tranzystory nie nadają się do sterowania prądem przemiennym. Są też dość wrażliwe na natężenie prądu przez nie płynący. W przypadku BD135-16 maksymalne natężenie prądu płynącego przez kolektorem i emiter to 1.5A, a i od tej wartości trzeba trzymać się daleko.

Taśma LED o której pisałem wcześniej w artykule wymaga 2A. Więc mój tranzystor jest za słaby, a w moim warsztacie nie ma mocniejszego.

Na tym etapie układ wygląda następująco: akumulatorek 3.3V zasila układ ESP8266, źródło o napięciu między 5 a 24V jest sterowany przez tranzystor, który znowu jest sterowany przez wyjście GPIO ESP8266 i zasila cewkę przekaźnika. Oba zasilacze, emiter tranzystora i ESP8266 są spięte do wspólnej masy.

Sterowany układ składa się ze źródła 12V i diód.

Podłączenie ESP8266 do internetu

Nawiązanie połączenia

Aby ESP mogło połączyć się z serwerem należy włączyć kartę WiFi i ustawić ją w odpowiedni tryb, aby to zrobić należy zaimportować pakiet ESP8266WiFi i wywołać metodę mode() w klasie WiFi. Metoda przyjmuje jedną z wartości: WIFI_OFF – brak wifi , WIFI_STA – tryb stacji roboczej – czyli ten o który nam chodzi, WIFI_AP – Access Point, WIFI_AP_STA – tryb mieszany.

#include <ESP8266WiFi.h>

void setup() {
    WiFi.mode(WIFI_STA);
}

Klasa WiFi umożliwia podłączenie się sieci, jej użycie wymaga jednak pewnej ilości nadmiarowego kodu. Znacznie łatwiej jest użyć klasy ESP8266WiFiMulti z pakietu o tej samej nazwie. Obsługuje ona ponowne nawiązywanie utraconego połączenia, czy przełączanie się między różnymi sieciami WiFi. Klasa ta posiada metody addAP i run. Pierwsza pozwala dodać sieć i wymaga podania nazwy sieci i hasła. Metodę tą wywołuje się w metodzie setup(). Bezparametrowa metoda run() zwraca informację o stanie WiFi, w szczególności czy urządzenie jest podłączone do jakiejś sieci. Poniższy kod mruga wbudowaną diodą led jeżeli urządzenie nie ma połączenia z siecią i gasi ją na stałe jeżeli połączenie zostaje nawiązane.

#include <ESP8266WiFi.h>
#include <ESP8266WiFiMulti.h>

ESP8266WiFiMulti WiFiMulti;

void setup() {
    WiFi.mode(WIFI_STA);
    WiFiMulti.addAP("<SSID>", "<Password>");
    pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
  if (WiFiMulti.run() != WL_CONNECTED) {
    digitalWrite(LED_BUILTIN, LOW);
    delay(300);
    digitalWrite(LED_BUILTIN, HIGH);
    delay(300);
    return;
  }
  delay(500);
}

W miejsce <SSID> należy wstawić nazwę swojej sieci WiFi, a <Password> hasło.

Biblioteka zawiera zawiera klasy WiFiClient oraz Udp do obsługi protokołów warstwy transportu (TCP, UDP), umożliwiające implementację obsługi dowolnego protokołu warstwy aplikacji.

#include <ESP8266WiFi.h>
#include <ESP8266WiFiMulti.h>

#include <WiFiClient.h>

ESP8266WiFiMulti WiFiMulti;

void setup() {
    WiFi.mode(WIFI_STA);
    WiFiMulti.addAP("<SSID>", "<Password>");
}

void loop() {
  if (WiFiMulti.run() != WL_CONNECTED) {
    delay(300);
    return;
  }
  WiFiClient client;
  String host = "<your-dns-name-or-ip>";
  int port = 5000;
  if (!client.connect(host, port)) {
    delay(300);
    return;
  }

  client.println("Hello");
  String response = client.readStringUntil('\n');

  delay(300);
}

W miejsce <your-dns-name-or-ip> należy wstawić adres IP komputera na którym działa aplikacja serwerowa.

Powyższy kod nie wykorzystuje protokołu http i nie zadziała z , wcześniej utworzoną, aplikacją serwerową. Ręczna implementacja tego protokołu jest dość złożona. Na szczęście nie trzeba tego robić.

ESP8266 – podłączenie przycisku

Wykrycie wciśnięcia przycisku

Aby ESP traktował nóżkę jako wejście należy wywołać metodę pinMode z parametrem INPUT.

Do odczytania stanu wejścia służy metoda digitalRead, która zwraca wartość HIGH gdy na pin jest podane napięcie zasilania.

Wydawałoby się, że należy napisać i wgrać na płytkę kod, który zapamiętuje w zmiennej czy lampka jest zapalona czy nie a gdy na wejściu jest stan wysoki odwraca wartość tej zmiennej i ustawia stan na wyjściu buildin_led:

int ledState = LOW;
void setup() {
   pinMode(0, INPUT);
   pinMode(LED_BUILTIN, OUTPUT);
   digitalWrite(LED_BUILTIN, ledState);
}

void loop() {
    int readValue = digitalRead(0);
    if (readValue == HIGH) {
        ledState = ledState == HIGH ? LOW : HIGH;
        digitalWrite(LED_BUILTIN, ledState);
    }
}

Następnie podłączyć masę (nóżkę GND) płytki do minusa baterii, jedną końcówkę przycisku do nóżki IO0, a zasilanie (nóżkę 3V3) i drugą końcówkę przycisku do plusa baterii.

Taki układ jednak po prostu nie działa. Wbudowana dioda LED zapala się wraz z podłączeniem zasilania i nie reaguje na wciśnięcie przycisku.

Dzieje się tak ponieważ układ wykrywa napięcie na nóżce, jednak nie pozwala mu ujść do uziemienia. Sytuację komplikuje dodatkowo fakt iż podczas startu, przed wywołaniem metody setup, układ dostarcza na nóżki GPIO0 i GPIO2 napięcie. Wybór innej nóżki niewiele poprawia sytuację, co prawda początkowo na nóżce nie ma napięcia, jednak gdy się już pojawi pozostaje tam na stałe.

Ładunek powinien mieć możliwość ujścia do masy (minusa baterii). Nie można go jednak tak po prostu zewrzeć go z masą, ponieważ po naciśnięciu przycisku elektrony będą omijać układ scalony i biec bezpośrednio do dodatniego bieguna baterii przy okazji powodując silne nagrzewanie się przewodów i samej baterii.

Rozwiązaniem tego problemu jest silny opornik (ponad 1kOhm). Można go włączyć na 2 sposoby:

  • Pull-down(po lewej) – gdy przycisk jest wyłączony na wejściu utrzymuje się stan niski ponieważ jest on połączony z ujemnym biegunem baterii. Po wciśnięciu przycisku wszystkie elektrony zostają „wessane” do dodatniego bieguna baterii, opornik spowalnia napływ nowych, co skutkuje pojawieniem się stanu wysokiego na nóżce, po ponownym wyłączeniu przycisku, elektrony które „przecisnęły” się przez opornik powodują przejście w stan niski.
  • Pull-up (po prawej) – gdy przycisk jest wyłączony na nóżce jest stan wysoki, ponieważ jest ona połączona z dodatnim biegunem baterii. Po wciśnięciu przycisku elektrony z ujemnego bieguna docierają zarówno do opornika jak i nóżki scalaka, ponieważ opornik spowalnia ich ruch na nóżce pojawia się stan niski. Zwolnienie przycisku powoduje odcięcie dopływu nowych elektronów, a te obecne powoli odpływają do dodatniego bieguna baterii powodując pojawienie się stanu wysokiego.

W przypadku nóżki GPIO0 nie można stosować rozwiązania pull-down ponieważ spięcie jej z masą przy starcie sprawia, że układ przechodzi w stan wgrywania nowego programu. Należy więc zastosować pull-up, a to wiąże się z drobną zmianą w programie: warunek wykrywający wciśnięcie przycisku ma sprawdzać czy na nóżce jest stan niski.

int ledState = LOW;
void setup() {
   pinMode(0, INPUT);
   pinMode(LED_BUILTIN, OUTPUT);
   digitalWrite(LED_BUILTIN, ledState);
}

void loop() {
    int readValue = digitalRead(0);
    if (readValue == LOW) {
        ledState = ledState == HIGH ? LOW : HIGH;
        digitalWrite(LED_BUILTIN, ledState);
    }
}

Da się jednak uniknąć stosowania zewnętrznego opornika, ponieważ układ ESP8266 ma już taki wbudowany, aby go użyć należy przełączyć pin w tryb INPUT_PULLUP, który jest dedykowany do użycia z przyciskami, tryb input przeznaczony jest raczej dla innych czujników (np. czujnika podczerwieni).

int ledState = LOW;
void setup() {
   pinMode(0, INPUT_PULLUP);
   pinMode(LED_BUILTIN, OUTPUT);
   digitalWrite(LED_BUILTIN, ledState);
}

void loop() {
    int readValue = digitalRead(0);
    if (readValue == LOW) {
        ledState = ledState == HIGH ? LOW : HIGH;
        digitalWrite(LED_BUILTIN, ledState);
    }
}

IoT z wykorzystaniem ESP8266

Pierwsza aplikacja

Programy w Arduino posiadają 2 metody:

  • setup() – uruchamiana po uruchomieniu/restarcie urządzenia
  • loop() – uruchamiana cyklicznie przez urządzenie

Arduino IDE posiada wbudowaną bibliotekę przykładowych projektów. Nie jestem zbyt kreatywny więc zacząłem od projektu „Blink” (File > Examples > 01. Basics > Blink).

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
}
void loop() {
  digitalWrite(LED_BUILTIN, HIGH);
  delay(1000);
  digitalWrite(LED_BUILTIN, LOW);
  delay(1000);
}

W metodzie setup() znajduje się wywołanie metody pinMode(), akceptująca 2 parametry: numer pinu i tryb. Numer pinu w przypadku ESP01 może przyjmować następujące wartości 0 – GPIO0, 1 – TX, 2 – GPIO2, 3 – RX. Tryb może przyjmować wartości OUTPUT – dany pin będzie traktowany jako wyjście, INPUT – wejście i INPUT_PULLUP – wejście ale trochę inne 😉. Użyta wartość LED_BUILTIN oznacza port do którego podpięta jest wbudowany LED – w przypadku ESP01 – 2.

Metoda loop() zawiera wywołanie metody digitalWrite(), która akceptuje numer pina, oraz stan który może przyjmować wartości HIGH i LOW. Po przekazaniu wartości HIGH na danym pinie pojawia się napięcie – dioda się zaświeca. Metoda delay() wstrzymuje wykonanie programu na określony czas – bez niej ludzie oko nie byłoby w stanie dostrzec mrugania.

Niezmienny obiekt i budowniczy

Bob Budowniczy

Wszystkie wcześniej wymienione problemy da się rozwiązać, stosując wzorzec budowniczego.

public class Address 
{
	// poprzednia zawartość klasy adres

	public class Builder 
	{
		private string addressLine1;
		private string addressLine2 = "";
		private string zipCode;
		private string city;
		private string stateArea = "";
		private string country = "Poland";
		
		public Builder()
		{

		}

		public Builder(Address address)
		{
			addressLine1 = address.addressLine1;
			addressLine2 = address.addressLine2;
			zipCode = address.zipCode;
			city = address.city;
			stateArea = address.stateArea;
			country = address.country;
		}

		public Builder AddressLine1(string addressLine1)
		{
			this.addressLine1 = addressLine1;
			return this;
		}

		public Builder AddressLine2(string addressLine2)
		{
			this.addressLine2 = addressLine2;
			return this;
		}

		public Builder ZipCode(string zipCode)
		{
			this.zipCode = zipCode;
			return this;
		}

		public Builder City(string city)
		{
			this.city = city;
			return this;
		}

		public Builder StateArea(string stateArea)
		{
			this.stateArea = stateArea;
			return this;
		}

		public Builder Country(string country)
		{
			this.country = country;
			return this;
		}

		public Address Build() 
		{
			return new Address(
				addressLine1,
				addressLine2,
				zipCode,
				city,
				stateArea,
				country);
		}
	}
}

Dodanie budowniczego rozdmuchało klasę Address, z drugiej jednak strony pozwala czytelnie zainicjować jej instancję z pominięciem pól, których nie chcemy ustawiać.

var address = new Address.Builder()
	.AddressLine1("Konwaliowa 31/20")
	.ZipCode("20036")
	.City("Lublin")
	.StateArea("Lubelskie")
	.Build();

Kopiowanie obiektu też jest znacznie czytelniejsze:

var templateAddress = new Address.Builder()
	.AddressLine1("Konwaliowa 31/20")
	.ZipCode("20036")
	.City("Lublin")
	.StateArea("Lubelskie")
	.Build();

var modifedAddress = new Address.Builder(templateAddress)
	.AddressLine1("Konwaliowa 31/21")
	.Build();

Immutable Map – modyfikowanie wielu pól

Klasa Map z pakietu immutable jest obecnie popularnie wybierana jako kontener dla globalnego stanu w aplikacjach wykorzystujących redux.

Zauważyłem też że powszechną, ale błędną, praktyką jest modyfikowanie wielu pól przy pomocy łańcucha poleceń set.

const newState = oldState
    .set('field1', 'value1')
    .set('field2', 'value2')
    .set('field3', 'value3');
Czytaj dalej Immutable Map – modyfikowanie wielu pól