Zamiana kierunku wiatru wyrażonego w stopniach, na opisanego ‚ładniej’ -w nieco nawigacyjny sposób, mogłaby być w Pythonie wykonana następująco:

def wind_direction2descr_old(wind_dir):
"""
po prostu zmieniam wartość kątową na opisową kierunku wiatru
:param wind_dir: liczbowa wartość w stopniach
:return: unistring 1 lub dwie literki
"""
wind_dir_descr = u''
if wind_dir is not None and isinstance(wind_dir, (int, long, float)):
wind_dir = round(wind_dir % 360, 0)
if wind_dir < 22.5 or wind_dir >= (360 - 22.5):
wind_dir_descr = u'N'
dir_wokolo = 45
if (dir_wokolo + 22.5) > wind_dir >= (dir_wokolo - 22.5):
wind_dir_descr = u'NE'
dir_wokolo = 90
if (dir_wokolo + 22.5) > wind_dir >= (dir_wokolo - 22.5):
wind_dir_descr = u'E'
dir_wokolo = 135
if (dir_wokolo + 22.5) > wind_dir >= (dir_wokolo - 22.5):
wind_dir_descr = u'SE'
dir_wokolo = 180
if (dir_wokolo + 22.5) > wind_dir >= (dir_wokolo - 22.5):
wind_dir_descr = u'S'
dir_wokolo = 225
if (dir_wokolo + 22.5) > wind_dir >= (dir_wokolo - 22.5):
wind_dir_descr = u'SW'
dir_wokolo = 270
if (dir_wokolo + 22.5) > wind_dir >= (dir_wokolo - 22.5):
wind_dir_descr = u'W'
dir_wokolo = 315
if (dir_wokolo + 22.5) > wind_dir >= (dir_wokolo - 22.5):
wind_dir_descr = u'NW'
return wind_dir_descr

Czyli okrąg-tort – 360 stopni umownie podzielony na 8 równych kawałków, ten na górze N – jak północ, na dole S – południe, po bokach E i W oraz pomiędzy nimi: NE SE SW NW. Osiem kawałków razem, licząc od góry – zgodnie ze wskazówkami zegara da nam: ‚N’ ‚NE’ ‚E’ ‚SE’ ‚S’ ‚SW’ ‚W’ ‚NW’.
Gdy 360 stopni na 8 otrzymamy 45, tak więc dla 0 stopni ( +/- połowa 45 = 22.5 stopnia ), czyli od -22.5 do +22.5 daje N , 45 (+/- 22.5) czyli 22.5 do 67.5 daje NE itd.
Funkcja powyżej w ten właśnie sposób zamienia wartość kąta na jego nawigacyjny opis kierunku.

Wcześniej sprawdzamy, czy wejściowa dana istnieje i jest liczbą – aby uniknąć błędów, jeśli jest poza zakresem 0 – 360 stopni to przywracamy ją do tego zakresu, zaokrąglamy i porównujemy aby określić opis słowny.

Można to też zrobić tak:

def wind_direction2descr(wind_dir):
"""
po prostu zmieniam wartość kątową na opisową kierunku wiatru
:param wind_dir: liczbowa wartość w stopniach
:return: unistring 1 lub dwie literki
"""
wind_dir_descr = u''
if wind_dir is not None and isinstance(wind_dir, (int, long, float)):
index = int(((float(wind_dir) + 22.5) % 360) / 45) % 8
wind_dir_descr = [u'N', u'NE', u'E', u'SE', u'S', u'SW', u'W', u'NW', ][index]
return wind_dir_descr

…czyli przeliczyć wind_dir na liczbę 0…7, która będzie indexem tablicy oznaczeń kierunków i da nam oczekiwany wynik.

Zaraz, zaraz, jak sprawdzić, czy wynik będzie oczekiwany?
Może przy pomocy testu?

wind_dir = 0
wind_dir_descr = wind_direction2descr(wind_dir)
print wind_dir, '-->', wind_dir_descr

…można to sprawdzać na piechotę oczywiście, można też napisać ładny Python’owy test.

Na piechotę:

Test:

# -*- coding: utf-8 -*-

# TODO reload utf-8
import sys
reload(sys)
sys.setdefaultencoding("utf-8")

from datetime import datetime

import describe_weather

try:
    import unittest2 as unittest
except ImportError:
    import unittest


class DescribeWeatherTest(unittest.TestCase):

    def test_wind_direction2descr(self):

        wind_dir_test_list = [
            [0, u'N'],
            [360, u'N'],
            [22.4, u'N'],
            [337.6, u'N'],

            [45 - 22.4, u'NE'],
            [45 + 22.4, u'NE'],
            [90 - 22.4, u'E'],
            [90 + 22.4, u'E'],
            [135 - 22.4, u'SE'],
            [135 + 22.4, u'SE'],
            [180 - 22.4, u'S'],
            [180 + 22.4, u'S'],
            [225 - 22.4, u'SW'],
            [225 + 22.4, u'SW'],
            [270 - 22.4, u'W'],
            [270 + 22.4, u'W'],
            [315 - 22.4, u'NW'],
            [315 + 22.4, u'NW'],

            [None, u''],
        ]

        for test_case in wind_dir_test_list:
            wind_dir = test_case[0]
            expected = test_case[1]
            res = describe_weather.wind_direction2descr(wind_dir)
            self.assertIsInstance(res, unicode, u'nie jest UNICODE')
            self.assertEqual(res, expected, u'oczekiwano innej wartości: ' + unicode(expected)
                             + u' otrzymano: ' + unicode(res) + u' dla kierunku: ' + unicode(wind_dir))
        return None

Od początku:

# -*- coding: utf-8 -*-

– sposób kodowania znaków użyty tutaj, oraz

import sys
reload(sys)
sys.setdefaultencoding("utf-8")

– sposób kodowania użyty w wytworzonym pliku wynikowym testu,

import describe_weather

– pobieram plik, w którym zawarta jest opisana wyżej funkcja,

import unittest

– pobieram pliki wykonujące tworzone testy, tzw testy jednostkowe lub ‚unit-testy’

Na bazie pobranych unit-testów tworzę klasę:

class DescribeWeatherTest(unittest.TestCase):

a w niej metodę / funkcję wykonującą test na opisanej powyżej mojej funkcji zamiany stopni na opis kierunku wiatru:

def test_wind_direction2descr(self):

nazwa klasy tworzona jest w sposób CamelCasing, czyli wielbłądzi – ma garby (wielkie litery) jako rozdzielenie wyrazów i Test w nazwie (np. ‚OpiszMojTest’ albo ‚OpiszJakisDziwnyNieModTest’…), nazwa metody – małymi literami podkreślnik jako rozdzielenie i na końcu ‚_test’.
Taka konwencja pozwala łatwiej zrozumieć …niezrozumiałe.
Co wewnątrz testu?
Mam do przetestowania funkcje, chcę jej zadawać wartości kąta i sprawdzać, czy otrzymany wynik jest zgodny z oczekiwaniami. Np jeśli zadam 0 (zero), chciałbym uzyskać ‚N’.
Zapiszmy to jako jeden przypadeczek testowy:
[0, u’N’]
(literka u oznacza kodowanie jako unicode, choć tutaj nie ma większego znaczenia…)

jeśli zebrać przypadeczki razem i ułożyć z nich listę otrzymamy:

wind_dir_test_list = [
    [0, u'N'],
    [360, u'N'],
    [22.4, u'N'],
    [337.6, u'N'],

    [45 - 22.4, u'NE'],
    [45 + 22.4, u'NE'],
    [90 - 22.4, u'E'],
    [90 + 22.4, u'E'],
    [135 - 22.4, u'SE'],
    [135 + 22.4, u'SE'],
    [180 - 22.4, u'S'],
    [180 + 22.4, u'S'],
    [225 - 22.4, u'SW'],
    [225 + 22.4, u'SW'],
    [270 - 22.4, u'W'],
    [270 + 22.4, u'W'],
    [315 - 22.4, u'NW'],
    [315 + 22.4, u'NW'],

    [None, u''],
]

…gdzie pierwszy element to zadawana wartość, drugi – oczekiwana odpowiedź.

Jak testujemy?

for test_case in wind_dir_test_list:
    wind_dir = test_case[0]
    expected = test_case[1]
    res = describe_weather.wind_direction2descr(wind_dir)
    self.assertIsInstance(res, unicode, u'nie jest UNICODE')
    self.assertEqual(res, expected, u'oczekiwano innej wartości: ' + unicode(expected)
                     + u' otrzymano: ' + unicode(res) + u' dla kierunku: ' + unicode(wind_dir))
return None

 

Pętla pobierająca jeden przypadeczek do ‚test_case’, gdzie ‚test_case[0]’ to zadawana wartość, a test_case[1] – oczekiwany wynik,

… wykonanie funkcji (dwa człony nazwy – pierwszy to nazwa zaimportowanego pliku ‚describe_weather’, drugi – ‚wind_direction2descr’ -nazwa funkcji oraz w nawiasach dana wejściowa )
wynik zwracany do zmiennej ‚res’ – jak resource (zasoby) oraz porównywana -to już narzędzia unit-testów:

assertIsInstance – czy jest typu unicode (bo tylko taki akceptujemy zgodnie z założeniami),
oraz:
assertEqual – czy to co uzyskano ‚res’ jest identyczne (equal -> równe) z oczekiwamym ‚expected’.

W obu ‚assert`ach’ sprawdzających, ostatnią wartością jest komunikat tekstowy, który pojawia się gdy wynik jest inny niż oczekiwano.

 

Na końcu, aby wykonać wszystkie testy:

if __name__ == '__main__':

    # --- jeden sposób:
    # unittest.main()
    # --- drugi sposób (chyba ładniejszy no i dla Jenkinsa):
    import xmlrunner
    unittest.main(testRunner=xmlrunner.XMLTestRunner(output='test-reports'))

…i wykonanie (poprzez run dla PyCharm).

Narzędzie Unittest wygeneruje raport:

Running tests...
----------------------------------------------------------------------
..........................
----------------------------------------------------------------------
Ran 26 tests in 0.003s

OK

Generating XML reports...

…także do pliku:

<?xml version="1.0" ?>
<testsuite errors="0" failures="0" name="DescribeWeatherTest-20180423140707" tests="26" time="0.003">
   <testcase classname="DescribeWeatherTest" name="test_describe_weather_teraz" time="0.000"/>
   <testcase classname="DescribeWeatherTest" name="test_for_effect_id_find_hours_ranges" time="0.000"/>
   <testcase classname="DescribeWeatherTest" name="test_integer_to_effect_description" time="0.000"/>
   <testcase classname="DescribeWeatherTest" name="test_opisz_cisnienie" time="0.000"/>
   <testcase classname="DescribeWeatherTest" name="test_opisz_cisnienie_doby" time="0.000"/>
   <testcase classname="DescribeWeatherTest" name="test_opisz_dodatkowo_efekty_doby" time="0.000"/>
   <testcase classname="DescribeWeatherTest" name="test_opisz_dodatkowo_efekty_prognozy" time="0.000"/>
   <testcase classname="DescribeWeatherTest" name="test_opisz_prognoze_doby" time="0.000"/>
   <testcase classname="DescribeWeatherTest" name="test_opisz_temperature_doby" time="0.000"/>
   <testcase classname="DescribeWeatherTest" name="test_opisz_temperature_prognozy" time="0.000"/>
   <testcase classname="DescribeWeatherTest" name="test_opisz_wiatr_doby" time="0.000"/>
   <testcase classname="DescribeWeatherTest" name="test_opisz_wiatr_prognoze" time="0.000"/>
   <testcase classname="DescribeWeatherTest" name="test_opisz_wilgotnosc_popoludnia" time="0.000"/>
   <testcase classname="DescribeWeatherTest" name="test_opisz_zachmurzenie_doby" time="0.000"/>
   <testcase classname="DescribeWeatherTest" name="test_opisz_zachmurzenie_popoludnia" time="0.000"/>
   <testcase classname="DescribeWeatherTest" name="test_opracuj_pobrana_prognoze_max" time="0.001"/>
   <testcase classname="DescribeWeatherTest" name="test_opracuj_pobrana_prognoze_najblizsza" time="0.000"/>
   <testcase classname="DescribeWeatherTest" name="test_semi_zachmurzenie_percent2_octascale" time="0.000"/>
   <testcase classname="DescribeWeatherTest" name="test_special" time="0.000"/>
   <testcase classname="DescribeWeatherTest" name="test_temperatura_ze_znakiem" time="0.000"/>
   <testcase classname="DescribeWeatherTest" name="test_wind_boufort2descr_pl" time="0.000"/>
   <testcase classname="DescribeWeatherTest" name="test_wind_direction2descr" time="0.000"/>
   <testcase classname="DescribeWeatherTest" name="test_wind_speed_mps_2boufort" time="0.000"/>
   <testcase classname="DescribeWeatherTest" name="test_zachmurzenie_percent2_octascale" time="0.000"/>
   <testcase classname="DescribeWeatherTest" name="test_zamien_tylko_interesujace_efekty_atmosferyczne_na_opis" time="0.000"/>
   <testcase classname="DescribeWeatherTest" name="test_zapamietaj_okres_od_do" time="0.000"/>
   <system-out>
<![CDATA[]]>   </system-out>
   <system-err>
<![CDATA[]]>   </system-err>
</testsuite>

Opis unittestów można znaleźć tutaj: https://docs.python.org/2/library/unittest.html

Co do wiatru, mamy jeszcze jego prędkość, można ją wyrażać w stopniach Bouforta, opisowo a także zwyczajnie – w metrach na sekundę…

Zamiana prędkości na stopnie Bouforta – w zależności od zakresu – bułka z masłem,

zaś stopnie Bouforta (z pewnością pamiętacie że zapisujemy inaczej niż stopnie Celsjusza – mianowicie bez znaczka stopni, tylko samo B) na opis, to już kiełbaska na tym maśle 🙂 :

def wind_boufort2descr_pl(wind_boufort):
    """
    zamień stopnie bouforta na opis
    :param wind_boufort: liczba całkowita 0...12
    :return: unistring-opis
    """
    wind_descr = u''
    l_w_d = [u'cisza',
             u'powiew',
             u'słaby wiatr',
             u'łagodny wiatr',
             u'umiarkowany wiatr',
             u'dość silny wiatr',
             u'silny wiatr',
             u'bardzo silny wiatr',
             u'wicher',
             u'silny sztorm',
             u'bardzo silny sztorm',
             u'gwałtowny sztorm',
             u'huragan']
    if wind_boufort is not None and isinstance(wind_boufort, (int, long, float)) and 0 <= wind_boufort <= 12:
        wind_descr = l_w_d[wind_boufort]
    return wind_descr

…oczywiście jakiś test do tej bułki.

http://pogoda.wiks.eu

Radości!

WikS

Urządzenie lokalizacyjne, znajdujące się i poruszające wraz z lokalizowanym obiektem, wysyłające (odbierające?) dane przez internet.

Pomysły:

  • budowa własnego lokalizatora (moduł dla internetu z kartą SIM, odbiornik GPS, sterowanie ustawieniami np. Bluetooth, procesor ogarniający całość),
  • gotowy moduł np z rodziny tanich lokalizatorów TK102/TK106

Rozwiązanie pierwsze jest ambitniejsze, daje większe możliwości -jak choćby wspomniana możliwość sterowania urządzeniami obiektu (samochód/łódź) poprzez internet, SMS etc. Rozwiązanie drugie zaś – proste, tanie, jednak w tym wypadku trzeba wpasować się w wizję autorów.

Opiszę zarówno wykorzystanie gotowego urządzenia jak i fragmenty tworzenia swojego… Będzie zatem o programowaniu w asemblerze, elektronice, układach cyfrowych itd.

Gotowy lokalizator TK102, TK106.

Urządzenie niewielkie, niedrogie dość niezawodne, do działania potrzebuje zasilania, zasięgu GPS (radiowej widoczności nieba), karty SIM z dostępem do internetu i …wiedzy gdzie wysyłać dane o lokalizacji.

Wymienione urządzenia zostały przetestowane i dobrze sprawdzają się, pasując do założeń.

To co pozostaje do wykonania to oszukanie nieco poprzez podanie przetworzonego napięcia akumulatora w taki sposób, aby urządzenie sądziło iż jest zasilane z własnej baterii (w znanych mi przypadkach nie wystarczy podanie napięcia zasilającego przewodem). Można to łatwo osiągnąć podając odpowiedniej wielkości napięcie (tutaj 4,4V) na zacisk ‚główny’ baterii, oraz – poprzez rezystor- na zacisk dodatkowy. Napięcie 5V uzyskane z przetwornicy (np. z wejściowego 12V) – przepuszczone przez diodę krzemową – spadek ok. 0,65V – uzyskamy 4,4V -jak z baterii. Kondensator o dużej pojemności ‚załagodzi’ zmiany napięcia.

Schemat i foto.

Pozostaje sprawdzić, czy po ew. zaniku zasilania (które w zasadzie zdarzy się tylko przy odłączeniu akumulatora obiektu śledzonego) lokalizator ponownie włączy się i rozpocznie pracę. W wypadku TK106 tak się właśnie dzieje. Dla TK102 – konieczny dodatkowy kondensator z rezystorem, symulujący przyciśnięcie OnOff po włączeniu zasilania.

Miejsce umieszczenia lokalizatora, powinno zapewniać możliwie najlepszą ‚radiową’ obserwację nieba. Wiadomo, dla uzyskania pozycji GPS wymagana jest widoczność przynajmniej 4 satelitów. Wykluczone zostają zatem wszystkie miejsca zasłaniające odbiornik od nieba karoserią (no chyba, że jest to Trabant i karoseria nie -metalowa). Dobrym pomysłem wydaje się miejsce pod deską rozdzielczą z przodu lub z tyłu. Szyba nie utrudnia odbioru sygnału GPS.

Co nadaje lokalizator i skąd to pochodzi?

TK106 nadaje co 15 sekund poprzez GPRS protokołem UDP na ustalony adres IP i port coś podobnego do:

1409261359,+486*******2,GPRMC,055920.000,A,5354.6300,N,1414.9510,E,3.74,349.42,260914,00,0000.0,A*44,L,,imei:353579018219978,125

nadawane frazy składają się z szeregu danych, m.in daty/godziny, numery telefony (wygwiazdkowany) fragmentu frazy RMC (Recommended minimum specific GPS) i numeru IMEI urządzenia.
RMC opisana jest tutaj.

 

Próbne frazy sygnału NMEA symulowanego dla poruszającego się odbiornika można uzyskać np tutaj (program napisany w Delphi dla Windows. Tak, tak -używałem kiedyś Windows.). Wydaje się, że można prosto przerobić to także dla Python i Linux.

Słówko jeszcze o NMEA, pozycji GPS oraz wspomnianych 6 bajtach, w których można zawrzeć dokładną pozycję obiektu w dowolnym miejscu na Ziemi.

Pozycja GPS jest określana poprzez podanie dwóch współrzędnych – podobnie jak X i Y na wykresie, tutaj jest to fi i lambda lub -innymi słowy- lat (latitude) i lng (longitude) – czyli szerokość i długość geograficzna. Na wykresie X i Y występowały jako dodatnie lub ujemne, podobnie współrzędne geograficzne – umownie szerokość północna (N) jako dodatnia, południowa (S) -ujemna, oraz długość wschodnia (E) dodatnia i zachodnia (W) ujemna.

X i Y były wyrażone liczbowo, podobnie jak długość i szerokość, które jednak wyrażona są w stopniach i ułamkach stopni. Zapis np. +53,926310 i +14,277255, szyli szerokość ponad 53 stopnie Nord i długość ponad 14 stopni East można przedstawić w nieco ładniejszy -nawigacyjny- sposób, mianowicie jako stopnie, minuty i ułamki minut:

N53o55,5786 E014o166353. Jak to przełożyć na Kulę Ziemską? Zostało to wymyślone około 1569 roku i od nazwiska twórcy nazwane siatką Merkatora.

Orientacja w temacie będzie niezbędna do skonstruowania własnego lokalizatora i oprogramowania do niego.

Tak więc nasza długość – oś X otacza Ziemię i zawiera się w zakresie od W180 (-180) do E180 (+180). Jest to 360 stopni, każdy stopień ma 60 minut czyli 360 * 60 = 21600 minut. Dokładnie tyle ile mil morskich ma równik, bo 1 mila morska na równiku odpowiada 1 minucie kątowej długości geograficznej. Jedna mila morska to 1852,5 metra.

Dla szerokości – naszego Y, zakres wynosi od S90 (-90) do N90 (+90). Z różnic zakresów wynikają też historyczne uwarunkowania zapisów długości i szerokości. Tak jak każdy nawigator ze wstrętem zapisuje pozycję z ułamkami stopni zamiast minut, tak żaden nie zapisze długości geograficznej inaczej w trzycyfrowej postaci stopni – często z wiodącym zerem. Wracając do szerokości – jedna minuta kątowa odpowiada jednej mili morskiej, czyli 1852,5 metra. W każdym miejscu.

Co jednak z przeliczaniem długości kątowej na odległość poza równikiem?

Odległość 1 minuty kątowej wzdłuż równoleżnika (a więc równolegle do równika) równa jest 1 mili morskiej pomnożonej przez cosinus szerokości geograficznej tego równoleżnika. I tak dla równika będzie to dokładnie jednak mila, zaś dla szerokości geograficznych Polski, Świnoujścia – około

 

cosinus(54st) * 1852.5 = 1089 metrów

…a na biegunie? Taki paradox… znacie zagadkę o przejścu z bieguna północnego 1km na południe, skręt o 90 stopni w prawo, 1km na zachód, skręt o 90 stopni w prawo i znów 1km na północ. Tadam! jesteśmy w tym samym miejscu! Przeszliśmy trzy proste odcinki ułożone pod kątem prostym i wróciliśmy w to samo miejsce …hmm. To taka nawigacyjna anegdotka.

Ok.
Fraza z TK106, nadana poprzez GPRS na IP:port, odebrana, podzielona wzdłuż przecinków, sprawdzone IMEI i nr telefonu, mamy datę i godzinę oraz pozycję. Hop do bazy danych.
Po odebraniu następnej znów itd…
Przetwarzając to sprytnie, możemy uzyskać na początek taką wizualizację:

Prędkości obliczane na podstawie ilorazóœ odległości i czasu, wizualizacja Google-gauge
do tego mając pozycje i bazę danych TERYT określamy najbliższe miejscowości i odległości do nich (proszę zwrócić uwagę na ostatni wiersz każdego z okienek).

Tadam!
Teraz zaprzągnijmy do pracy analizę – sprawdzamy w sposób prawie-ciągły pozycje śledzonych obiektów względem interesujących obszarów. Dołączamy SMS i/lub powiadomienia Messenger…

Radości!
WikS.