Blog (22)
Komentarze (619)
Recenzje (0)

Poznaj Spikit API - narzędzie dla prawdziwych geeków!

@alucosoftwarePoznaj Spikit API - narzędzie dla prawdziwych geeków!10.05.2014 14:12

Program w jego najnowszej wersji coraz częściej wykorzystują osoby wdrażające w swoich czterech kątach idee inteligentnych domów i np. sterują za pomocą Spikit popularnym na polskim rynku systemem Fibaro. Nie dziwię się im. Połączenie tych dwóch technologii przynosi wiele satysfakcji (nie mówiąc o efekcie WOW!). Jednocześnie w sieci pojawiają się jak grzyby po deszczu kolejne amatorskie lub bardziej profesjonalne projekty wykorzystujące platformy Arduino lub Raspberry Pi.

Kilku nad wyraz aktywnych użytkowników wymieniło na ten temat ze mną poglądy. Osoby te podzieliły się również pasją i planami budowy własnych, zdalnie sterowanych urządzeń. Tym, co nas wszystkich łączy jest chęć wykorzystywania wszędzie interfejsu głosowego. O ile w przypadku istniejącego już systemu Fibaro, komunikacja ze specjalną centralką odbywa się za pośrednictwem protokołu HTTP i metody GET, co nie stanowi dla Spikit żadnego problemu i nie wymaga żadnych modyfikacji oprogramowania, o tyle chęć wykorzystania rozpoznanej przez Spikit mowy w kilku różniących się od siebie projektach wymagała wprowadzenia nowego mechanizmu.

Owocem wprowadzonych zmian jest możliwość komunikowania się programu z zewnętrznymi aplikacjami w bardzo przystępny sposób tj. za pomocą potoków (named pipes). Pomysł równie prosty, co wysoce skuteczny. Tym bardziej, że komunikacja może odbywać się w dwóch kierunkach :-)

Ja rozpoznam Twoją mowę, a Ty zajmij się resztą!

Osobom zaznajomionym choć trochę z problematyką komunikacji międzyprocesowej (Inter-Process Communication, w skrócie IPC) na pewno zaświeciła się stosowna żaróweczka. Idea potoków (zarówno tych anonimowych jak i posiadających nazwę) jest przecież obecna z nami już od dawna (różowe lata 70.). Mając dwa, zupełnie odseparowane od siebie programy musimy doprowadzić do sytuacji, w której wyjście jednego programu stanowić będzie wejście dla drugiego - ot, co. Różna może być jedynie implementacja tej idei między poszczególnymi środowiskami uruchomieniowymi (Windows, Linux). Niewątpliwą zaletą potoków nazwanych jest natomiast możliwość ustanowienia komunikacji między niepowiązanymi procesami przez sieć, jednocześnie nie będąc uzależnionym od konkretnego protokołu sieciowego. Inne zalety to szybkość działania, ale także możliwość wykorzystania np. uprawnień dostępu systemu Windows w procesie autoryzacji połączenia z potokiem.

W Windows mianem potoków nazywamy po prostu pewien obszar pamięci współdzielony między dwoma procesami. Komunikacja przez taki przewód (z ang. pipe) łączący dwa procesy (programy) może odbywać się w jednym kierunku lub w sposób dwukierunkowy (dupleks). Proces, który tworzy potok nazywamy serwerem, a ten, który się z nim łączy - klientem. Więcej informacji o potokach, wadach i zaletach tego rozwiązania w kontekście IPC uzyskacie w zasobach MSDN:

Jak łatwo się domyślić, w naszym przypadku Spikit jest właśnie takim serwerem i z chwilą uruchomienia tworzy potok na komputerze lokalnym o wdzięcznej nazwie... Spikit. W chwili obecnej Spikit zezwala na ustanowienie jednego połączenia w danym czasie (kolejne będą odrzucane). Zdaje się być w miarę idiotoodporny (tego nigdy za wiele!) i przekierowuje do podłączonego klienta wszystkie polecenia, którym przypisana została akcja {POTOK_NAZWANY_PRZEKIERUJ}. Użytkownik programu może przełączać tryb pracy potoku nazwanego tak, by w informacji przekazywanej klientowi Spikit uwzględniał również transkrypcję tekstu będącą rezultatem operacji dyktowania. Na aplikacji po stronie klienta spoczywa natomiast ciężar wykonania z rozpoznanym poleceniem lub fragmentem tekstu czegoś sensownego. Więc do dzieła, hulaj dusza!

Jak to ugryźć od strony programisty?

Nie byłbym sobą, gdybym na łamach dobrychprogramów nie zamieścił przy tej okazji stosownego dobrego kodu ;-) Niezależnie od tego, czy kod ten miałby zostać wykorzystany przez Czytelnika do ustanowienia połączenia ze Spikit i wykonania jakiejś fajnej czynności, czy też miałby wejść w skład aplikacji o zupełnie innym charakterze - jest to po prostu interesujący przykład z punktu widzenia każdego programisty .NET. Brać, póki dają.

Z racji tego, że ta funkcjonalność programu ma jeszcze charakter czysto rozwojowy i może ulec drobnym zmianom, poniższy kod API należy skompilować samodzielnie i póki co próżno szukać stosownej biblioteki DLL dołączonej do oficjalnego wydania programu Spikit. Taka biblioteka na pewno pojawi się w swoim czasie. Natomiast w pełni działający przykład odnajdziecie tu. Wystarczy uruchomić oba programy jednocześnie.

Jeden ze stałych użytkowników programu zasugerował mi, abym uzupełnił program o możliwość odczytywania godziny i bieżącej daty (wzrok z czasem płata figle). Takoż czynię, ale w formie przykładowej wtyczki. Informacją zwrotną dla Spikit będzie więc informacja o bieżącej dacie i godzinie poprzedzona kilkoma słowami. Spikit odczyta na głos każdą informację tekstową przesłaną przez zewnętrzną aplikację wbudowanym głosem syntezatora mowy lub głosem firmy trzeciej (IVONA?), oczywiście jeśli głos taki będzie zainstalowany w systemie Windows. Spikit respektuje przy tym systemowe ustawienia syntezatora mowy (szybkość, barwa, itp.) wprowadzone w systemowym oknie konfiguracyjnym 'Tekst na mowę'.

SpikitAPI.PipeClient


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace SpikitAPI
{
    public class PipeClient
    {
        private bool running = true;
        private System.Threading.AutoResetEvent messageEvent = new System.Threading.AutoResetEvent(true);
        private System.IO.Pipes.NamedPipeClientStream client = null;

        public event EventHandler Disconnected;
        protected virtual void OnDisconnected(EventArgs e)
        {
            var temp = Disconnected;
            if (temp != null)
                temp(this, e);
        }

        public event EventHandler Connected;
        protected virtual void OnConnected(EventArgs e)
        {
            var temp = Connected;
            if (temp != null)
                temp(this, e);
        }

        public delegate void MessageReceivedEventHandler(object sender, PipeClientEventArgs e);
        public event MessageReceivedEventHandler MessageReceived;
        protected virtual void OnMessageReceived(PipeClientEventArgs e)
        {
            var temp = MessageReceived;
            if (temp != null)
                temp(this, e);
        }

        public class PipeClientEventArgs : EventArgs
        {
            public readonly string Message;

            public PipeClientEventArgs(string message)
            {
                Message = message;
            }
        }

        public bool SendToServer(string message)
        {
            bool success = false;
            if (client != null && client.IsConnected && client.CanWrite) // Wyślij wiadomość do serwera tylko, gdy jest taka możliwość (połączony z możliwością przesyłania danych)
            {
                try
                {
                    byte[] output = System.Text.Encoding.UTF8.GetBytes(message);
                    client.Write(output, 0, output.Length);
                    success = true;
                }
                catch { }
            }
            Console.WriteLine(success ? string.Format("Wiadomość zwrotna została wysłana: {0}", message) : "Wystąpił błąd podczas przesyłania odpowiedzi");
            return success;
        }

        public void Stop() // Zakończ pracę klienta. Wątek zakończy pracę w normalny, niezakłócony sposób (wyjście z pętli do-while).
        {
            running = false;
            messageEvent.Set();
        }

        public PipeClient(string serverName = ".", System.Security.Principal.TokenImpersonationLevel impersonationLevel = System.Security.Principal.TokenImpersonationLevel.None)
        {
            System.Threading.Thread t = new System.Threading.Thread(() =>
            {
                do
                {
                    try
                    {
                        if (client == null) // Utwórz instancję klasy NamedPipeClientStream
                            client = new System.IO.Pipes.NamedPipeClientStream(serverName, "Spikit", System.IO.Pipes.PipeDirection.InOut, System.IO.Pipes.PipeOptions.Asynchronous, impersonationLevel);

                        client.Connect(5000); // Nawiąż połączenie z potokiem Spikit na komputerze lokalnym (serverName = ".") lub w sieci (serverName = "NAZWA_PC")
                        Console.WriteLine("Połączyłem się");
                        OnConnected(EventArgs.Empty);
                        while (messageEvent.WaitOne() && client.IsConnected) // Jeśli obiekt zasygnalizowany i gdy klient połączony, w przeciwnym razie blokuj wątek
                        {
                            if (!running) // Jeśli wywołana metoda Stop(), wyjdź z pętli
                                break;

                            Console.WriteLine("Czekam na wiadomość...");
                            byte[] buffer = new byte[4096]; // Domyślna wielkość bufora danych przesyłanych przez Spikit
                            client.BeginRead(buffer, 0, buffer.Length, callback => // Wywołaj asynchroniczną metodę BeginRead i obsłuż wywołanie funkcji 'callback', gdy w potoku pojawią się nowe dane
                            {
								bool error = false;
                                try
                                {
                                    int count = client.EndRead(callback); // Zakończ asynchroniczną operację odczytywania danych
                                    if (count > 0)
                                    {
                                        string input = System.Text.Encoding.UTF8.GetString(buffer, 0, count);
                                        Console.WriteLine("Otrzymałem wiadomość ({0}): {1}", count, input);
                                        OnMessageReceived(new PipeClientEventArgs(input));
                                    }
                                    else
                                    {
										error = true;
                                        client.Close(); // Zamknij strumień, jeśli ilość przesłanych danych jest równa zeru (możliwe zerwanie połączenia po stronie serwera)
                                        Console.WriteLine("Brak wiadomości/Zerwane połączenie");
                                    }
                                }
                                catch { error = true; }
                                finally
                                {
                                    if (error)
                                    {
                                        OnDisconnected(EventArgs.Empty);
                                    }
                                    messageEvent.Set(); // Sygnalizuj nadejście wiadomości lub zerwania połączenia (count == 0), zasygnalizuj konieczność odblokowania wątku w pętli while
                                }
                            }, null);
                        }
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine("Błąd/Brak połączenia: {0}", e.Message);
                        messageEvent.Set(); // Zasygnalizuj obiekt messageEvent, umożliwiając wejście do pętli while, gdy tylko klient nawiąże połączenie z potokiem
                        if (client != null) // Zwolnij nieużywane zasoby
                        {
                            client.Dispose();
                            client = null;
                        }
                        System.Threading.Thread.Sleep(5000); // Zatrzymaj wykonywanie kodu na jakiś czas przed podjęciem kolejnej próby
                    }
                } while (running);

                if (client != null) // Zwolnij nieużywane zasoby
                {
                    client.Dispose();
                    client = null;
                }
            });
            t.Start();
        }
    }
}

Przykład aplikacji konsolowej - dla początkujących


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace NamedPipeTest
{
    class Program
    {
        static void Main(string[] args)
        {
            SpikitAPI.PipeClient client = new SpikitAPI.PipeClient(); // Wystarczy utworzyć instancję klasy PipeClient i gotowe
            client.MessageReceived += new SpikitAPI.PipeClient.MessageReceivedEventHandler(client_MessageReceived); // Obsługa przychodzących wiadomości odbywa się w zdarzeniu 'MessageReceived'
        }

        static void client_MessageReceived(object sender, SpikitAPI.PipeClient.PipeClientEventArgs e)
        {
            // e.Message zawiera treść rozpoznanego polecenia głosowego lub rozpoznanej mowy (funkcja dyktowania)
            string message = e.Message.Trim(); // Dla pewności usuniemy wszelkie białe znaki przed oraz po wiadomości

            if (string.IsNullOrEmpty(message)) // Dobre przyzwyczajenie, a sytuacja mało prawdopodobna
                return;

            string reply = null;

            // Wykorzystamy podejście staromodne i oczywiste: if/else if/else. Dlaczego nie instrukcji 'switch'? Z uwagi na porównywanie łańcuchów 'OrdinalIgnoreCase', czyli bez rozróżniania wielkości znaków
            if (message.Equals("która godzina", StringComparison.OrdinalIgnoreCase))
            {
                reply = string.Format("jest godzina {0}", DateTime.Now.ToShortTimeString()); // godzina
            }
            else if (message.Equals("jaki dziś dzień", StringComparison.OrdinalIgnoreCase))
            {
                System.Globalization.CultureInfo locale = new System.Globalization.CultureInfo("pl-PL");
                reply = string.Format("dziś jest {0} {1}", DateTime.Now.ToString("dddd", locale), DateTime.Now.ToString("d", locale)); // dzień tygodnia i data
            }
            else if (message.Equals("o co mogę zapytać", StringComparison.OrdinalIgnoreCase))
            {
                StringBuilder result = new StringBuilder();

                result.Append("Słucham następujących poleceń");
                result.Append(". ");
                result.Append("która godzina");
                result.Append(". ");
                result.Append("jaki dziś dzień");
                result.Append(". ");
                result.Append("o co mogę zapytać");
                result.Append(". ");
                result.Append("wyłącz się");
                result.Append(". ");

                reply = result.ToString();
            }
            else if (message.Equals("wyłącz się", StringComparison.OrdinalIgnoreCase))
            {
                Environment.Exit(0);
            }
            else
            {
                reply = "niestety, chyba mówimy różnym językiem, bo naprawdę nie rozumiem o co tobie chodzi"; // każda inna komenda przesłana do klienta zwróci ten wynik
            }

            if (!string.IsNullOrEmpty(reply)) // Jeśli odpowiedź została przez nas określona
            {
                SpikitAPI.PipeClient client = sender as SpikitAPI.PipeClient;
                client.SendToServer(reply); // Wyślij odpowiedź z powrotem do programu
            }
		}
    }
}

Przykład aplikacji konsolowej - dla średniozaawansowanych


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace NamedPipeTest
{
    class Program
    {
        static Dictionary<string, Func<string>> dictionary = null;
		
        static void Main(string[] args)
        {
            dictionary = new Dictionary<string, Func<string>>(StringComparer.OrdinalIgnoreCase);
            dictionary.Add("która godzina", () =>
                {
                    return string.Format("jest godzina {0}", DateTime.Now.ToShortTimeString());
                }
            );

            dictionary.Add("jaki dziś dzień", () =>
                {
                    System.Globalization.CultureInfo locale = new System.Globalization.CultureInfo("pl-PL");
                    return string.Format("dziś jest {0} {1}", DateTime.Now.ToString("dddd", locale), DateTime.Now.ToString("d", locale));
                }
            );

            dictionary.Add("wyłącz się", () =>
                {
                    Environment.Exit(0);

                    return null; // duh... ;-)
                }
            );

            dictionary.Add("o co mogę zapytać", () =>
                {
                    StringBuilder result = new StringBuilder();

                    result.Append("Słucham następujących poleceń");
                    result.Append(". ");
                    result.Append("która godzina");
                    result.Append(". ");
                    result.Append("jaki dziś dzień");
                    result.Append(". ");
                    result.Append("o co mogę zapytać");
                    result.Append(". ");
                    result.Append("wyłącz się");
                    result.Append(". ");

                    return result.ToString();
                }
            );

            SpikitAPI.PipeClient client = new SpikitAPI.PipeClient(); // Wystarczy utworzyć instancję klasy PipeClient i gotowe
            client.MessageReceived += new SpikitAPI.PipeClient.MessageReceivedEventHandler(client_MessageReceived); // Obsługa przychodzących wiadomości odbywa się w zdarzeniu 'MessageReceived'
        }

        static void client_MessageReceived(object sender, SpikitAPI.PipeClient.PipeClientEventArgs e)
        {
            // e.Message zawiera treść rozpoznanego polecenia głosowego lub rozpoznanej mowy (funkcja dyktowania)
            string message = e.Message.Trim(); // Dla pewności usuniemy wszelkie białe znaki przed oraz po wiadomości

            if (string.IsNullOrEmpty(message)) // Dobre przyzwyczajenie, a sytuacja mało prawdopodobna
                return;

            string reply = null;

            if (dictionary != null && dictionary.ContainsKey(message))
                reply = dictionary[message].Invoke();
            else
                reply = "niestety, chyba mówimy różnym językiem, bo naprawdę nie rozumiem o co tobie chodzi";

            if (!string.IsNullOrEmpty(reply)) // Jeśli odpowiedź została przez nas określona
            {
                SpikitAPI.PipeClient client = sender as SpikitAPI.PipeClient;
                client.SendToServer(reply); // Wyślij odpowiedź z powrotem do programu
            }
		}
    }
}

Jak to wygląda w praktyce? Jak na załączonym poniżej filmie:

Niespodzianka

Wszystkim, którzy dotarli do końca szczerze gratuluję i dziękuję. By umilić niektórym Czytelnikom kolejny majowy weekend proponuję następującą zabawę:

Utwórz ciekawą aplikację wykorzystującą Spikit lub dowolną wtyczkę rozszerzającą możliwości programu. Do komunikacji z programem użyj klasy PipeClient z przestrzeni nazw SpikitAPI (w oryginale lub zmodyfikowanej do własnych potrzeb). Link do swojej aplikacji wraz z krótkim opisem zamieść w komentarzu do tego wpisu.

Linki do podstawowych narzędzi programistycznych:

Osoba, które wykaże się świetnym pomysłem, zrealizuje go w ciekawy sposób i udokumentuje swoją pracę krótkim filmem w YouTube, otrzyma ode mnie licencję. Klucz licencyjny likwiduje sztuczne opóźnienie (od 1 do 8 sekund) znane z bezpłatnej edycji programu i uprawnia do późniejszych aktualizacji. Na zgłoszenia czekam przez tydzień. Nie czekaj na polską Cortanę, weź sprawy w swoje ręce!

Oczywiście z miłą chęcią poznam Wasze komentarze i spostrzeżenia dotyczące całego wpisu oraz nowej, jakże istotnej funkcji.

Wybrane dla Ciebie
Szanowna Użytkowniczko! Szanowny Użytkowniku!
×
Aby dalej móc dostarczać coraz lepsze materiały redakcyjne i udostępniać coraz lepsze usługi, potrzebujemy zgody na dopasowanie treści marketingowych do Twojego zachowania. Twoje dane są u nas bezpieczne, a zgodę możesz wycofać w każdej chwili na podstronie polityka prywatności.

Kliknij "PRZECHODZĘ DO SERWISU" lub na symbol "X" w górnym rogu tej planszy, jeżeli zgadzasz się na przetwarzanie przez Wirtualną Polskę i naszych Zaufanych Partnerów Twoich danych osobowych, zbieranych w ramach korzystania przez Ciebie z usług, portali i serwisów internetowych Wirtualnej Polski (w tym danych zapisywanych w plikach cookies) w celach marketingowych realizowanych na zlecenie naszych Zaufanych Partnerów. Jeśli nie zgadzasz się na przetwarzanie Twoich danych osobowych skorzystaj z ustawień w polityce prywatności. Zgoda jest dobrowolna i możesz ją w dowolnym momencie wycofać zmieniając ustawienia w polityce prywatności (w której znajdziesz odpowiedzi na wszystkie pytania związane z przetwarzaniem Twoich danych osobowych).

Od 25 maja 2018 roku obowiązuje Rozporządzenie Parlamentu Europejskiego i Rady (UE) 2016/679 (określane jako "RODO"). W związku z tym chcielibyśmy poinformować o przetwarzaniu Twoich danych oraz zasadach, na jakich odbywa się to po dniu 25 maja 2018 roku.

Kto będzie administratorem Twoich danych?

Administratorami Twoich danych będzie Wirtualna Polska Media Spółka Akcyjna z siedzibą w Warszawie, oraz pozostałe spółki z grupy Wirtualna Polska, jak również nasi Zaufani Partnerzy, z którymi stale współpracujemy. Szczegółowe informacje dotyczące administratorów znajdują się w polityce prywatności.

O jakich danych mówimy?

Chodzi o dane osobowe, które są zbierane w ramach korzystania przez Ciebie z naszych usług, portali i serwisów internetowych udostępnianych przez Wirtualną Polskę, w tym zapisywanych w plikach cookies, które są instalowane na naszych stronach przez Wirtualną Polskę oraz naszych Zaufanych Partnerów.

Dlaczego chcemy przetwarzać Twoje dane?

Przetwarzamy je dostarczać coraz lepsze materiały redakcyjne, dopasować ich tematykę do Twoich zainteresowań, tworzyć portale i serwisy internetowe, z których będziesz korzystać z przyjemnością, zapewniać większe bezpieczeństwo usług, udoskonalać nasze usługi i maksymalnie dopasować je do Twoich zainteresowań, pokazywać reklamy dopasowane do Twoich potrzeb. Szczegółowe informacje dotyczące celów przetwarzania Twoich danych znajdują się w polityce prywatności.

Komu możemy przekazać dane?

Twoje dane możemy przekazywać podmiotom przetwarzającym je na nasze zlecenie oraz podmiotom uprawnionym do uzyskania danych na podstawie obowiązującego prawa – oczywiście tylko, gdy wystąpią z żądaniem w oparciu o stosowną podstawę prawną.

Jakie masz prawa w stosunku do Twoich danych?

Masz prawo żądania dostępu, sprostowania, usunięcia lub ograniczenia przetwarzania danych. Możesz wycofać zgodę na przetwarzanie, zgłosić sprzeciw oraz skorzystać z innych praw wymienionych szczegółowo w polityce prywatności.

Jakie są podstawy prawne przetwarzania Twoich danych?

Podstawą prawną przetwarzania Twoich danych w celu świadczenia usług jest niezbędność do wykonania umów o ich świadczenie (tymi umowami są zazwyczaj regulaminy). Podstawą prawną przetwarzania danych w celu pomiarów statystycznych i marketingu własnego administratorów jest tzw. uzasadniony interes administratora. Przetwarzanie Twoich danych w celach marketingowych realizowanych przez Wirtualną Polskę na zlecenie Zaufanych Partnerów i bezpośrednio przez Zaufanych Partnerów będzie odbywać się na podstawie Twojej dobrowolnej zgody.