Blog (22)
Komentarze (619)
Recenzje (0)
@alucosoftwarePoznaj Spikit API - narzędzie dla prawdziwych geeków!

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

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.

516334

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 :‑)

516337
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!

516343

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
Komentarze (22)