ESP8266 – wincyj mocy

Wykorzystanie jednego źródła zasilania

3 źródła zasilania komplikują wykonanie układu, pierwszym uproszczeniem jest wykorzystanie źródła zasilania listwy LED jako źródła dla obwodu cewka przekaźnika – tranzystor:

Niestety ESP8266 nie może być zasilane napięciem 12V, należy je zredukować. Można do tego celu wykorzystać regulator napięcia LM317.

Posiada on 3 nóżki: wejście do którego podpiąłem źródło 12V, wyjście które służy do zasilenia ESP8266 oraz nóżka służąco do regulacji wysokości napięcia wyjściowego. Układ reguluje się przy pomocy rezystorów. Podłączonych pomiędzy wyjście, wyjście regulacyjne i masę układu. Aby uzyskać napięcie 3.4V można użyć oporników 240 i 470ohm.

LM317 podczas redukcji napięcia emituje ciepło. Jego ilość zależy od spadku napięcia oraz prądu wyjściowego. Przy redukcji z 12V na 3.3V i prądzie 0.2A ilość generowanego ciepła uzasadnia użycie małego radiatora.

Na tym etapie układ wygląda następująco:

Jest jeszcze problem: ESP8266 nie jest w stanie się uruchomić jeżeli wyjście IO0 jest podłączone do masy, podobnie rzecz ma się gdy do masy podłączeone jest wyjście IO2 i TX na szęście problem nie występuje dla wyścia RX/IO3 . Więc ostatecznie układ wygląda tak:

Zaś tak prezentuje się w działaniu

Podłączenie ESP8266 do internetu

Klasa HttpClient

Klasa ta opakowuje klasę WiFiClient i udostępnia metody pozwalające obsłużyć protokół http. Metody begin i end wskazują początek i koniec obsługi wywołania http. Metoda begin pobiera jako parametr adres, z którym należy nawiązać połączenie. Dodatkowo klasa posiada metodę addHeader dodającą nagłówek http, bezparametrową metody GET i POST pozwalające wysłać lub odebrać dane z serwera.

Kod układu sterującego diodą wygląda następująco:

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

#include <WiFiClient.h>

ESP8266WiFiMulti WiFiMulti;

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

void loop() {
  if (WiFiMulti.run() != WL_CONNECTED) {
    delay(300);
    return;
  }
  WiFiClient client;
  HTTPClient http;

  if (http.begin(client, "http://<your-dns-name-or-ip>:5000/led")) {
    int httpCode = http.GET();

    String payload = http.getString();

    if (payload == "ON") {
      digitalWrite(LED_BUILTIN, LOW);
    } else {
      digitalWrite(LED_BUILTIN, HIGH);
    }
  }

  delay(300);
}

Natomiast układu reagującego na wciśnięcie przycisku:

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

#include <WiFiClient.h>

ESP8266WiFiMulti WiFiMulti;
int prevButtonState = LOW;

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

void loop() {
  if (WiFiMulti.run() != WL_CONNECTED) {
    delay(300);
    return;
  }
  WiFiClient client;
  HTTPClient http;
  int readValue = digitalRead(0);
  if (readValue == LOW && prevButtonState == HIGH) {
    if (http.begin(client, "http://<your-dns-name-or-ip>:5000/button")) {
      http.addHeader("content-type", "application/json");
      int httpCode = http.POST("");
    }
  }
  prevButtonState = readValue;

  delay(300);
}

Układ sterujący diodą został po prostu podłączony do zasilania (przez programator w trybie UART). Układ reagujący na wciśnięcie przycisku został skonstruowany w sposób opisany w poprzednim artykule. Jak widać na nagraniu całość działa.

Rozwiązanie ma jednak swoje wady:

  • Serwer nie weryfikuje czy urządzenie, które się z nim łączy jest tym za które się podaje
  • Nawet gdyby serwer dokonywał weryfikacji, to transmisja jest nieszyfrowana, więc ktoś mógłby ją podsłuchać i wydobyć dane potrzebne do podszycia się
  • Protokół http jest dość ciężki, na nagraniu wszystko wydaje się działać natychmiastowo, wynika to jednak tylko z faktu iż serwer znajduje się w tej samej sieci WiFi

Inne artykuły związane z Internetem rzeczy

ESP8266 – podłączenie przycisku

Układ działa już dużo lepiej, zdarza się jednak że po naciśnięciu przycisku lampka nie wyłącza/ nie włącza się. Wynika to z faktu iż układ jest znacznie szybszy niż ludzki palec i pojedyncze wciśnięcie przycisku jest interpretowane jako ciąg wciśnięć.

Rozwiązaniem jest zmiana wykrywania stanu niskiego na wykrywanie zmiany stanu z niskiego na wysoki:

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

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

Powyższy kod zredukował problem, nie usunął go jednak całkowicie. Winny temu jest tzw. bouncing. W trakcie wciskania elementy przycisku trą i odbijają się od siebie, co sprawia, że przepływ prądu jest przerywany i wznawiany.

Problem można rozwiązać sprzętowo: wstawiając kondensator wokół przycisku. W moim przypadku całkiem nie źle się sprawdził elektrolityczny kondensator 4,7 mikrofarada.

Problem można też rozwiązać programowo, program może sprawdzić czy kolejne przełączenie przycisku nastąpiło bezpośrednio po poprzednim przez wywołanie metody millis, która zwraca czas w milisekundach który upłynął od uruchomienia programu i sprawdzeniu czy od poprzedniej zmiany stanu minął określony przedział czasu.

int ledState = LOW;
int prevButtonState = LOW;
unsigned long lastDebounceTime = 0;
unsigned long debounceDelay = 100;

void setup() {
   pinMode(0, INPUT_PULLUP);
   pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
    int readValue = digitalRead(0);
    if (readValue == LOW && prevButtonState == HIGH) {
      if ((millis() - lastDebounceTime) > debounceDelay) {
        lastDebounceTime = millis();
        ledState = ledState == HIGH ? LOW : HIGH;
      }
    }
    digitalWrite(LED_BUILTIN, ledState);
    prevButtonState = readValue;
}

Czytelnikom chcącym dowiedzieć się czegoś więcej na temat bouncingu polecam film na kanale RS Elektronika

Inne artykuły związane z Internetem rzeczy

IoT z wykorzystaniem ESP8266

Uruchomienie programu

Aby móc uruchomić program na płytce potrzebny jest programator – urządzenie posiadające port szeregowy pracujący w napięciu 3.3V i prędkością 115.2kbps. Żaden ze standardowych portów w komputerze nie spełnia tych wymagań, W internecie można znaleźć opis jak wykorzystać Arduino jako adapter ale ja kupiłem gotowy układ.

Należy podpiąć układ do programatora, a programator do komputera. Trzeba się też upewnić czy programator jest poprawnie ustawiony. Mój posiada mały przełącznik który przełącza układ między trybami komunikacji i programowania. Inne programatory posiadają np regulację napięcia między 3.3 i 5 Volt – to wyższe usmaży nam płytkę.

Następnie należy otworzyć Arduino IDE, kliknąć „Tools > Port > Numer portu odpowiadający programatorowi”. Na liście może znajdować się wiele portów COM, w przypadku trudności z identyfikacją należy odłączać i podłączać programator – lista portów odświeża się przy otwarciu menu „Tools”.

Na koniec zostaje wgrać program przyciskiem upload (duża strzałka w prawo), podłączyć układ do zasilania i patrzeć jak działa.

Inne artykuły związane z Internetem rzeczy

Niezmienny obiekt i budowniczy

Budowniczy a pola wymagane

W obecnej postaci kod pozwala na utworzenie obiektu bez inicjowania żadnego pola. Pierwszym rozwiązaniem jakie przychodzi na myśl jest dodanie parametrów konstruktora w klasie builder. Jest to dobre rozwiązanie jeżeli wymaganych parametrów jest niewiele, jeżeli jest ich więcej ponownie pojawia problem pomyłek podczas przekazywania wartości parametrów.

Aby wymusić inicjację wymaganych nie tracąc innych zalet poprzedniego rozwiązania, należy jeszcze bardziej rozbudować klasę Address, na początek klasa budowniczego oraz konstruktor klasy Address powinny stać się prywatne.

public class Address 
{
	// ...
	private Address(string addressLine1, string addressLine2, string zipCode, string city, string stateArea, string country)
	//...

	private class Builder 
	{
		// ...
	}
}

Następnie należy zadeklarować szereg interfejsów, odpowiadających poszczególnym, obowiązkowym, polom klasy, dodatkowy interfejs pozwalający skonfigurować pola opcjonalne i zaimplementować wszystkie interfejsy w klasie Builder, w przykładzie poniżej zdecydowałem się na implementację dosłowną (implicit).

public class Address 
{
	// ...
	public interface IConfigureAddressLine1 
	{
		IConfigureZipCode AddressLine1(string addressLine1);
	}

	public interface IConfigureZipCode
	{
		IConfigureCity ZipCode(string zipCode);
	}

	public interface IConfigureCity
	{
		IBuilder City(string city);
	}

	public interface IBuilder
	{
		IBuilder AddressLine2(string addressLine2);
		IBuilder StateArea(string stateArea);
		IBuilder Country(string country);
		Address Build();
	}

	private class Builder : IConfigureAddressLine1, IConfigureZipCode, IConfigureCity, IBuilder
	{

		// ...
		IConfigureZipCode IConfigureAddressLine1.AddressLine1(string addressLine1)
		{
			return AddressLine1(addressLine1);
		}

		IConfigureCity IConfigureZipCode.ZipCode(string zipCode)
		{
			return ZipCode(zipCode);
		}

		IBuilder IConfigureCity.City(string city)
		{
			return City(city);
		}

		IBuilder IBuilder.AddressLine2(string addressLine2)
		{
			return AddressLine2(addressLine2);
		}

		IBuilder IBuilder.StateArea(string stateArea)
		{
			return StateArea(stateArea);
		}

		IBuilder IBuilder.Country(string country)
		{
			return Country(country);
		}
	}
}

Aby ponownie umożliwić utworzenie instancji klasy należy dodać metodę fabrykującą:

public class Address 
{
	public static IConfigureAddressLine1 CreateNew()
	{
		return new Builder();
	}
	 // ...
}

Na tym etapie IDE prowadzi użytkownika za rączkę podczas tworzenia nowej instancji klasy.

var address = Address
    .CreateNew()
    .AddressLine1("Konwaliowa 31/20")
    .ZipCode("20036")
    .City("Lublin")
    // Opcjonalnie
    // .StateArea("Lubelskie")
    // .Country("Polska")
    // .AddressLine2("Druga klatka")
    .Build();

Ostatnim krokiem jest dodanie interfejsu ICloneBuilder, pozwalającej zmienić wartość dowolnego pola, zaimplementowanie go w klasie Builder i dodanie metody Clone do klasy Address.

public class Address 
{
	// ...
	public ICloneBuilder Clone()
	{
		return new Builder(this);
	}
	// ...
	private class Builder : ICloneBuilder, IConfigureAddressLine1, IConfigureZipCode, IConfigureCity, IBuilder
	{
                // ...
		ICloneBuilder ICloneBuilder.AddressLine1(string addressLine1)
		{
			return AddressLine1(addressLine1);
		}

		ICloneBuilder ICloneBuilder.ZipCode(string zipCode)
		{
			return ZipCode(zipCode);
		}

		ICloneBuilder ICloneBuilder.City(string city)
		{
			return City(city);
		}

		ICloneBuilder ICloneBuilder.AddressLine2(string addressLine2)
		{
			return AddressLine2(addressLine2);
		}

		ICloneBuilder ICloneBuilder.StateArea(string stateArea)
		{
			return StateArea(stateArea);
		}

		ICloneBuilder ICloneBuilder.Country(string country)
		{
			return Country(country);
		}
                // ...
	}
}
var templateAddress = Address.CreateNew()
    .AddressLine1("Konwaliowa 31/20")
    .ZipCode("20036")
    .City("Lublin")
    .StateArea("Lubelskie")
    .Country("Polska")
    .Build();

var modifedAddress = templateAddress.Clone()
    .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