Azure: Funkcje i Android Wear11.VII.2017

Spis treści

  1. Wstęp
  2. Przygotowanie - SQL Server
  3. Funkcja Azure
  4. Android Wear - tarcza zegarka
  5. Android Wear - wrzucanie aplikacji na zegarek
  6. Działanie
  7. Dane zebrane w tabeli

Wstęp

Celem było zalogowanie aktywności podczas snu dlatego od razu na myśl przyszedł zegarek z systemem Android Wear. Ponieważ jednak aby pobrać dane z zegarka trzeba się trochę namęczyć (czyt. ADB) dlatego najwygodniejszym rozwiązaniem było wysyłanie danych na bieżąco do chmury. Sam zegarek jeśli posiada WiFi to zostanie ono użyte a gdyby zniknęło (brak połączenia) zegarek potrafi korzystać z danych pakietowych sparowanego telefonu.

Jeśli chodzi o część chmurową to nastąpił czas na sprawdzenie co oferują nowe, bezserwerowe aplikacje - funkcje w chmurze.

Przygotowanie - SQL Server

Po założeniu nowej bazy danych w obrębie SQL Servera portal Azure generuje Connection String po kliknięciu w link Show database connection strings

Server=tcp:nazwa_serwera.database.windows.net,1433;Initial Catalog=docelowa_baza;Persist Security Info=False;User ID={your_username};Password={your_password};MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;

Gdzie należy pozamieniać wpisy {your_username} i {your_password} na swoje.

Ponieważ portal nie pozwala na dodanie nowego użytkownika - należy tą operację wykonać z poziomu np SSMS (Sql Server Management Studio)

-- baza [master]
CREATE LOGIN db_login WITH password='db_password';

-- baza [docelowa_baza]
CREATE USER db_login FROM LOGIN db_login;
EXEC sp_addrolemember 'db_datareader', 'db_login';
EXEC sp_addrolemember 'db_datawriter', 'db_login';

Dalej należy przygotować tabelę docelową gdzie będą przechowywane wyniki.

CREATE TABLE [dbo].[SenData]
(
    [Id] INT NOT NULL PRIMARY KEY identity(1,1),
    [Now] DateTime not null default (getdate()),
    [X] Float not null,
    [Y] Float not null,
    [Z] Float not null
)

Funkcja Azure

Najpierw dodajemy tzw. aplikację funkcyjną (Function App), która jest kontenerem wszystkich funkcji, które w obrębie tej aplikacji dodamy. Główną zaletą tego typu aplikacji (w stosunku do web aplikacji) jest to, że plan hostingowy można ustawić na tryb Consumption Plan czyli płacimy tylko za czas wykonywania funkcji liczony w milisekundach i zajętości pamięci - szczegóły pod adresem Functions - cennik.

Web aplikacje działają tak na prawdę o zdefiniowane z góry zasoby na maszynach, za które trzeba płacić mimo iż nie wykorzystuje się ich w całości - App Service Plan. Przy funkcjach działamy bezserwerowo - z punktu widzenia użytkownika sprzęt na którym funkcja się wywołuje nie ma znaczenia.

Azure darmowo w ramach planu konsumpcyjnego daje cyt. "miesięczny bezpłatny przydział 1 mln żądań i 400 000 GB-s użycia zasobów miesięcznie" na dzień 11.VII.2017.

W konfiguracji całej aplikacji funkcyjnej dodajemy Connection String do bazy danych: Aplikacja - Platform features - Application settings i potem sekcja Connection strings gdzie należy dodać nowy wpis o kluczu docelowa_baza.

Teraz należy stworzyć nową funkcję o nazwie moja_funkcja na podstawie podstawowego szablonu HttpTrigger - C# i nadpisać jej zawartość poniższym kodem

#r "System.Configuration"
#r "System.Data"
using System.Net;
using System.Data.SqlClient;
using System.Configuration;

public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
    // request body
    dynamic data = await req.Content.ReadAsAsync<object>();

    string mode = data?.m;
    string s_x = data?.x;
    string s_y = data?.y;
    string s_z = data?.z;
    string s_d = data?.d;

    float x;
    float.TryParse(s_x?.Replace(",","."), out x);
    float y;
    float.TryParse(s_y?.Replace(",","."), out y);
    float z;
    float.TryParse(s_z?.Replace(",","."), out z);
    DateTime d;
    bool hasDate = DateTime.TryParse(s_d, out d);

    log.Info($"(x,y,z)=({x},{y},{z})");

    if ("+" == mode) {
        var str = ConfigurationManager.ConnectionStrings["docelowa_baza"].ConnectionString;
        using (SqlConnection conn = new SqlConnection(str))
        {
            conn.Open();
            var text = hasDate
                ? "insert into [dbo].[SenData] (Now,X,Y,Z) values (@d,@x,@y,@z);" 
                : "insert into [dbo].[SenData] (X,Y,Z) values (@x,@y,@z);";

            using (SqlCommand cmd = new SqlCommand(text, conn))
            {
                if (hasDate)
                    cmd.Parameters.Add(new SqlParameter("@d",d));
                cmd.Parameters.Add(new SqlParameter("@x",x));
                cmd.Parameters.Add(new SqlParameter("@y",y));
                cmd.Parameters.Add(new SqlParameter("@z",z));

                var rows = await cmd.ExecuteNonQueryAsync();
                log.Info($"{rows} rows were updated");
            }
        }
    }

    return "+" != mode
        ? req.CreateResponse(HttpStatusCode.BadRequest, "Go away robot!")
        : req.CreateResponse(HttpStatusCode.OK, "Saved");
}

Powyższa funkcja działa w ten sposób, że przyjmuje JSONa w postaci

{
  "m": "+",
  "x": "0,914585",
  "y": "-1,034295",
  "z": "9,524135",
  "d": "2017-07-11 12:43:00"
}

którego zawartość jest zapisywana do bazy danych. Niepodanie parametru d czyli daty odczytu spowoduje automatycznym nadaniem daty przez bazę danych.

Przechodzimy na zakładkę Integrate i upewniamy się, że pole Authorization level ma wartość Function.

Na zakładce Manage pobieramy klucz dostępowy (default) lub generujemy nowy.

Wracamy do definicji funkcji moja_funkcja i pobieramy jej URL klikając w link </> Get function URL

Azure podpowiada całe wywołanie z podaniem klucza jako parametr całego adresu ale w tym przykładzie klucz autoryzacyjny zostanie przekazany przez nagłówek x-functions-key. URL, który będzie użyty powinien wyglądać mniej więcej następująco:

https://aplikacja_fn.azurewebsites.net/api/moja_funkcja

Android Wear - tarcza zegarka

Dlaczego tarcza zegarka? W raz z pojawieniem się wersji systemu Android Wear 2.0, Google położyło przede wszystkim nacisk na możliwo szybkiej zmiany tarczy zegarka - gest przesuwania prawo-lewo. Dlatego jeśli zegarek ma logować do chmury to zmieniamy tarczę na logującą a jeżeli ma nie logować - wracamy do podstawowej. Wystarczą dwa proste ruchy palcem.

UWAGA Preferowanym systemem jest Android Wear w wersji 2.0, który pozwala na instalowanie aplikacji niezależnie od telefonu.

Używając Android Studio tworzymy nową aplikację tylko na zegarki - Wear. Z dostępnych początkowo szablonów wybieramy tarczę zegarka - Watch Face.

Po tym jak Android Studio wygeneruje wszystkie klasy znajdujemy dziedziczącą z CanvasWatchFaceService.Engine ponieważ chodzi o metodę onTimeTick()

@Override
public void onTimeTick() {
    super.onTimeTick();
    invalidate();

    // wykonanie własnej metody
    saveSensorDataToCloud();
}

Metoda onTimeTick() jest idealna do użycia ponieważ póki tarcza jest wyświetlana (widoczna) mamy pewność, że jest ona wykonywana co najmniej raz na minutę po to aby zegarek zaktualizował wyświetlaną godzinę. Częstsze jej użycie następuje wtedy gdy zegarek przechodzi z trybu aktywnego do trybu działania w tle i na odwrót - słowem gdy tarcza zegarka jest dotykana przez użytkownika.

Nie wdając się w szczegóły implementacji tarczy wysłanie danych z akcelerometru, korzystając z biblioteki Ok Http metodą POST do chmury wygląda w następujący sposób:

final MediaType JSON = MediaType.parse("application/json");
final OkHttpClient client = new OkHttpClient();

void post(String json){
    try {
        Log.v("__WAA_RQ__", "Data: " + json);

        RequestBody body = RequestBody.create(JSON, json);
        Request request = new Request.Builder()
                .url("https://aplikacja_fn.azurewebsites.net/api/moja_funkcja")
                .header(
                        "x-functions-key",
                        "heO/h2byLyadP3sTx/oWrv2PPbMCFWdVJVHlo7qDgabIp1bzUTmKlg=="
                )
                .post(body)
                .build();

        Response response = client.newCall(request).execute();
        boolean isOk = response.isSuccessful();
        String resBody = response.body().string();

        Log.v("__WAA_RE__", isOk + ": " + resBody);
    } catch (IOException iex) {
        Log.e("__WAA_E__", "Cannot send to URL!", iex);
    }
}

Android Wear - wrzucanie aplikacji na zegarek

Aby móc wrzucić aplikację na zegarek należy najpierw na zegarku w ustawieniach deweloperskich zezwolić na debuggowanie przez WiFi. UWAGA po przełączeniu tej opcji adres IP pojawia się po dłuższej chwili.

Teraz należy ADB połączyć do zegarka

# podłączenie ADB do zegarka
\> adb.exe connect 192.168.1.100:5555
connected to 192.168.1.100:5555

# wykonanie zrzutu ekranu (podane jako ciekawostka)
\> adb.exe shell screencap -p /sdcard/s.png
\> adb.exe pull -p /sdcard/s.png
\> adb.exe shell rm /sdcard/s.png

Ponieważ polecenie Run 'app' w Android Studio nie działa ze względu na brak startowego Activity należy z menu Run - Edit configurations... wybrać opcję Launch Options: Launch: Nothing

Działanie

Teraz w ADB Logcat mogą pojawić się co minutę następujące wpisy

# nie podano (prawidłowego) klucza autoryzacji
V/__WAA_RQ__: Data: {"m":"+", ...}
V/__WAA_RE__: false:

# nie ustawiono (prawidłowego) parametru "m"/"mode"
V/__WAA_RQ__: Data: {"m":"?", ...}
V/__WAA_RE__: false: "Go away robot!"

# ok
V/__WAA_RQ__: Data: {"m":"+","x":"0,972046","y":"-1,101333","z":"9,634268","d":"2017-07-11 12:46:09"}
V/__WAA_RE__: true: "Saved"

Jednak tylko w ostatnim przypadku, gdy wszystko zostało poprawnie skonfigurowane w portalu Azure w logach funkcji pojawi się następujący wpis

Welcome, you are now connected to log-streaming service.
Function started (Id=f92fd230-5283-4c3c-901c-bdbe6c37989f)
(x,y,z)=(0.972046,-1.101333,9.634268)
1 rows were updated
Function completed (Success, Id=f92fd230-5283-4c3c-901c-bdbe6c37989f, Duration=0ms)

Dane zebrane w tabeli

Po nocy działania na ręku z tabeli znalazły się takie dane

Teraz już tylko pozostaje kwestia analizy danych...