"Mądrzejszy" termometr, część 3
Trochę upłynęło czasu. Można więc zadać pytanie: jak tam mój bardzo skomplikowany termometr? Działa?
Działa.
Przypominając: mam czujnik temperatury na 1‑wire, DS18B20, a nawet kilka. Podpięte są one do Raspberry Pi. RPi wraz z modułem w1‑gpio oraz w1‑therm tworzy pewne pliki, które zawierają dane z urządzenia. Te dane są parsowane przez stado skryptów PHP tworząc usługę webową, która może udostępniać temperaturę w postaci różnych formatów danych.
Jednym z tych formatów jest czysty tekst, którego intensywnie używam, jest wszechobecny JSON, a także coś specyficznego dla Windows - format WNS, będący w gruncie rzeczy odpowiednim XML‑em.
Całe założenie tego co robiłem od początku było takie, że chciałem mieć temperaturę za oknem prezentowaną w postaci wartości na przypiętym "kafelku" w systemie Windows 8. Skoro mamy już sprzęt, mamy już udostępnione dane, co będzie następnym krokiem? Oczywiście sama aplikacja, dla Windows 8. Wygląda ona mniej więcej tak:
Głównie składa się z jednej prostej rzeczy - listy wielkich kwadratów prezentujących aktualne odczyty, oraz - nieco mniejsze - ich nazwy. Jest to graficzne przedstawienie pewnego modelu danych, który zawiera takie pola jak nazwa czujnika, adres serwera, wartość odczytu i parę innych. Zaktualizowanie wartości temperatury odbywa się "ręcznie", poprzez wywołanie metody UpdateReading(). Metoda ta ma następującą postać:
public async Task UpdateReading() { if (this.Server == null && this.SensorName == null) this.Reading = float.NaN; var filter = new HttpBaseProtocolFilter(); filter.CacheControl.ReadBehavior = Windows.Web.Http.Filters.HttpCacheReadBehavior.MostRecent; filter.CacheControl.WriteBehavior = Windows.Web.Http.Filters.HttpCacheWriteBehavior.NoCache; HttpClient h = new HttpClient(filter); try { Uri uri = new Uri(this.Server + this.SensorName + ".txt"); string r = await h.GetStringAsync(uri); this.Reading = float.Parse(r); this.ReadingError = false; } catch (Exception) { this.ReadingError = true; this.Reading = float.NaN; } }
Wykorzystany tutaj jest HttpClient... jednak należy uważać, ponieważ jest to Windows.Web.Http.HttpClient, nie System.Net.HttpClient. Posiada od trochę dodatkowych możliwości w stosunku do swojego kolegi z System.Net, przede wszystkim kontrolę nad cache oraz fakt, że wszystko realizowane jest poprzez async/await. Aktualizacja odczytu po prostu łączy się z serwerem i odczytuje dane z pliku http://<serwer>/<nazwa czujnika>.txt, na co usługa webowa automatycznie odpowiada zwykłym, czystym tekstem. Ten tekst parsowany jest do postaci liczby, która wrzucana jest do właściwości Reading.
Po dowiązaniu modelu danych do kontrolki GridView wraz z ustawieniem szablonu danych, dostajemy taką aplikację, jaką mniej więcej pokazałem. Do tego niezbędne były jeszcze dwie rzeczy - muszę mieć możliwość dodawania i usuwania czujników na bieżąco. W dodatku fajnie by było, gdyby taka lista czujników podróżowała wraz ze mną po różnych urządzeniach. No i chcę mieć możliwość "przypięcia" dowolnego wielkiego kwadratu do ekranu startowego, aby te odczyty oglądać bez uruchamiania aplikacji.
Dodawanie i usuwanie czujników z listy okazało się nietrudne - model danych zawarty jest w typowej kolekcji typu ObservableCollection, która jest bezproblemowo dowiązywana do kontrolki GridView. Większym problemem okazało się po pierwsze to, że w przeciwieństwie do Windows Phone, w XAML dla Windows 8 nie ma przy wiązaniu danych parametru StringFormat i aby liczbę powiązać z wyświetlaniem w taki sposób, aby tam jeszcze pokazywać napis "stopni C", trzeba było pokombinować z własnym konwerterem.
Dodatkowo, zgodnie z oficjalnymi wymogami, dodawanie nowych czujników (oraz przypinanie) powinno być realizowane w postaci tzw. flyoutów, a nie przechodzenia do innej strony aplikacji. Dodałem zatem pasek komend (AppBar) na dole aplikacji, zawierający takie rzeczy jak pokazywanie małego okienka do dodawania elementu, usuwanie zaznaczonych elementów (zaznaczanie elementów już realizuje GridView sam z siebie), aktualizację odczytów oraz przypinanie/odpinanie od ekranu startowego.
To, co dla mnie było naistotniejsze, przypinanie do ekranu głównego, realizowane jest przez "secondary tiles". Aplikacja może mieć wiele drugorzędnych kafelków - u mnie tworzony jest jeden kafelek na każdy czujnik. Kafelki identyfikowane są przez ich unikatowe identyfikatory, u mnie odpowiada za to metoda ToSecondaryTileName() w moim modelu. Posiadając nazwę, pewne dodatkowe właściwości, obrazek i ustawiając rozmiar drugorzędnego kafelka można dodać go do ekranu startowego za pomocą takiego kodu:
Uri logo = new Uri("ms-appx:///Assets/squareTile.png"); string tileActivationArguments = appbarTileId + " was pinned at " + DateTime.Now.ToLocalTime().ToString(); SecondaryTile secondaryTile = new SecondaryTile(appbarTileId, t.SensorName, tileActivationArguments, logo, TileSize.Default); secondaryTile.VisualElements.ForegroundText = ForegroundText.Light; bool isPinned = await secondaryTile.RequestCreateForSelectionAsync(GetElementRect((FrameworkElement)sender), Windows.UI.Popups.Placement.Above);
Warto tutaj zwrócić uwagę, że nie powoduje to od razu przypięcia kafelka do ekranu głównego, a wywołanie systemowego okienka, gdzie użytkownik ma podgląd oraz może nadać własną nazwę.
Jeżeli przypięcie się powiodło, to mamy kafelek, który uruchamia aplikację, przekazując pewne dane (mało istotne dla mnie - tileActivationArguments - tutaj powinno być coś sensownego, a nie data przypięcia kafelka). Trzeba teraz jakoś wymusić jego aktualizację automatycznie co pewien czas na podstawie danych z usługi sieciowej. Do tego służy TileUpdateManager.
if (isPinned) Windows.UI.Notifications.TileUpdateManager.CreateTileUpdaterForSecondaryTile(appbarTileId).StartPeriodicUpdate(t.ToWnsUri(), Windows.UI.Notifications.PeriodicUpdateRecurrence.Hour);
TileUpdateManager zaczyna aktualizować kafelek korzystając z pewnego adresu internetowego zwracanego przez metodę ToWnsUri(), z częstotliwością jednego razu na godzinę. Minimalny czas aktualizacji kafelka w taki sposób to raz na 30 minut. Wyszedłem z założenia, że temperatura aktualizowana raz na godzinę mi wystarczy.
Po przypięciu kafelków do głównego ekranu oraz odczekaniu aż system je zaktualizuje dostajemy mniej więcej taki efekt:
Potem system aktualizuje je zgodnie z tymi zadeklarowanymi harmonogramami co jakiś czas, metodą "pull", znaczy łączy się z odpowiednią usługą i pobiera XML, którym zmienia dane na kafelku. Moja aplikacja przewiduje tylko jeden rozmiar i tylko temperaturę, ale można to oczywiście uatrakcyjnić na różne sposoby. Przede wszystkim dodając logo aplikacji, którego wciąż nie dodałem i mam zamiast tego kwadrat z przekątnymi w środku.
Przy aplikacji pogrzebałem nieco dalej - bo przecież fajnie by było, gdyby przycisk "przypnij" zmieniał się na "odepnij", wskazane by było, aby użytkownik wiedział, że tam jest AppBar na dole, a nie żeby był schowany (wzorem aplikacji Poczta na przykład), czego nie da się zrobić w standardzie (ale jest gotowa klasa do tego) i tak dalej. Samą aplikację zacząłem sklejać z kilku przykładów Microsoftu, więc nie jest najpiękniejsza, niestety, ale póki co - działa.
Tutaj chciałbym powiedzieć o jeszcze jednej rzeczy - czujniki DS18B20 łączy się szeregowo. Ja sobie zrobiłem malutką płytkę, która łączy odpowiednie gniazdka, natomiast każdy czujnik ma zrobioną końcówkę do gniazdek pasujących. Czujniki mam obecnie trzy, jeden na długim kabelku wyrzucony za okno, drugi mierzący temperaturę w pokoju i trzeci - wewnątrz obudowy RPi. Zrobione jest to za pomocą płytki uniwersalnej, która leży w środku obudowy, w której jest moje Raspberry... ale z góry przepraszam wszystkich, którzy mają pojęcie o lutowaniu za zrobienie takiego potworka :‑)
(swoją drogą wnętrze mojej obudowy dla RPi zawierające te wszystkie komponenty wpychane na siłę też nie wygląda dobrze).
Samą aplikację, w wersji jakiej mam to aktualnie, czyli dość rozgrzebanej, udostępniam tutaj: http://trash.ktos.info/TemperatureApp.zip (do własnoręcznej kompilacji). Działa, ale mogłaby działać lepiej, ma kilka błędów. W obecnym stanie nie nadaje się do publikacji w Sklepie za nic.
Ale sama aplikacja bez danych jest niewiele warta, dlatego też mam wystawioną usługę "na świat" - pod adresem http://ktostemperature.azurewebsites.net/external.txt są aktualizowane co jakiś czas dane z mojego faktycznego czujnika zewnętrznego, i taki adres można dodać do tej aplikacji (adres serwera: "http://ktostemperature.azurewebsites.net/", nazwa czujnika "external") i powinno to działać. Oprócz .txt dostępne jest oczywiście również .json i .wns.
Niestety, w trakcie używania pierwszej wersji mojego wielce skomplikowanego rozwiązania (które da się zastąpić wystawioną za okno rurką z kolorowym alkoholem) okazało się, że czujnik był źle zaizolowany i zdarzały się zwarcia - zwarcie powodowało, że moduł 1‑wire na RPi przestawał działać poprawnie aż do restartu systemu, kiedy działał już bardziej poprawnie (nie wykrywał czujnika ze zwarciem). Poprawiłem, ale błędy w hardware są gorsze do rozwiązania niż błędy w oprogramowaniu.
Podsumowując:
- mam czujniki DS18B20 do pomiaru temperatury podłączone do Raspberry Pi,
- moduł jądra pozwala dane z czujnika przedstawiać jako pliki tekstowe o określonej zawartości,
- dane temperatury z tych plików są prezentowane przez napisaną w PHP usługę sieciową (mniej więcej zgodną z podejściem REST),
- jest napisana w C# aplikacja dla Windows 8, która odczytuje dane z czujników i prezentuje je jako wielkie kwadraty,
- lub pozwala "przypiąć" czujnik do ekranu Start,
- a dodatkowo mam napisany w PHP skrypt uruchomiony na Windows Azure, który dane z czujnika z mojej sieci lokalnej wyprowadza również na świat.
I to wszystko, aby tylko zobaczyć czy jest ciepło za oknem :‑)