piątek, 22 kwietnia 2016

[DSP2016] Raspberry Pi & Windows 10 IoT Core

Android Androidem, ale jak chcemy mieć kolorowe migające światła to trudno by ekran telefonu uznać za szczyt możliwości. W tym odcinku zbudujemy rozwiązanie na mikrokomputerze Raspberry Pi 2, które następnie zintegrujemy z naszą uprzednio napisaną app-ką.

Jeśli chodzi o IoT czy Raspberry Pi to w ramach DSP będzie to interesujący epizod, w stosunku do wszystkich planów na techniczne eksperymenty do końca maja, jakie siedzą w mojej głowie –Winking smile 

Co to jest Raspberry Pi? Jak pisać na niego uniwersalne aplikacje Windows 10?  Do kupna i praktycznej zabawy z Raspberry Pi 2 zachęcił mnie w zeszłym roku kurs Tomasza Kopacza Raspberry PI 2 i Windows 10 IoT Core – jak zacząć (fanem Windows 10 we wszystkich odmianach byłem od pierwszych buildów, ale brakowało mi takiego praktycznego wejścia w IoT). Swoje eksperymenty mające na celu przyswojenie pewnych podstaw z tej tematyki zawarłem w cyklu postów Raspberry Pi 2 & Windows 10 IoT Core, który jest dostępny na tym blogu np. w ramach #Windows 10 IoT. Teraz mamy kwiecień 2016 roku i jest już w sprzedaży Raspberry Pi 3 z wbudowanym Bluetooth i Wi-Fi, z ładnym wsparciem także przez Windows 10 IoT Core. Może kiedyś w przyszłości nabędę i tę płytkę, ale na razie sprzętowy szkic oświetlenia oprzemy na wersji drugiej (Raspberry Pi nadal oferowane jest różnych wersjach, a Windows 10 Iot wspiera zarówno wersję drugą, jak i trzecią, która jest kompatybilna z poprzedniczką).

Sprzętowy szkic oświetlenia? Tak, w ramach epizodu szkic dla zrobienia sobie małej dyskoteki na biurku!  Oczywiście bardziej profesjonalna realizacja sprzętowego projektu obejmowałaby listwy czy też panele LED sterowane przy pomocy tranzystorów, ale z uwagi na ograniczony czas w DSP zostawię sobie to ewentualnie na dalszą przyszłość, ponieważ na dniach chcę mocno wejść w iOS, a to być może nie koniec. Na obecny moment zadowolę się płytką prototypową z trzema diodami LED wpiętymi przez oporniki 100 omów do trzech wybranych pinów GPIO z napieciem +5V (czerwona – GPIO 5, żółta – GPIO 6, zielona – GPIO 13) oraz (oczywiście) masy. Całość przedstawia poniższy schemat, ktory wykonałem w popularnym do tego rodzaju zastosowań darmowym programie Fritzing.

light_organ

Na żywo wygląda to tak:

WP_20160422_00_25_24_Pro

WP_20160422_00_26_39_Pro

Cześć sprzętowa to jedno, ale jak sprawić by diody świeciły z różną jasnością i jak przekazywać dane z mobilnej aplikacji do Raspberry Pi? Otóż napisałem sobie uniwersalną aplikację, którą wgrałem na płytkę. Aplikacja ta nasłuchuje na sokecie przychodzących do niej komend w postaci ciągów trzech bajtów i steruje jasnością diód przy pomocy PWM (co to takiego? pisałem kiedyś o tym w postach Raspberry Pi 2 & Windows 10 IoT Core - odc.6- PWM (servo) oraz Raspberry Pi 2 & Windows 10 IoT Core - odc.7- PWM (LED RGB)). Nasłuch zrealizowany jest w klasie SocketServer:

public class SocketServer: IDisposable
    {
        private StreamSocketListener tcpListener;
        private const string port = "8181";

        public event EventHandler<MessageSentEventArgs> NewMessageReady;

        public async void StartListener()
        {
            tcpListener = new StreamSocketListener();
            tcpListener.ConnectionReceived += TcpListener_ConnectionReceived;
            await tcpListener.BindServiceNameAsync(port);
        }

        private async void TcpListener_ConnectionReceived(StreamSocketListener sender, StreamSocketListenerConnectionReceivedEventArgs args)
        {
            var streamSocket = args.Socket;
            var reader = new DataReader(streamSocket.InputStream);
            try
            {
                while (true)
                {
                    uint bytesLoaded = await reader.LoadAsync(3);
                    if (bytesLoaded != 3)
                    {
                        return;
                    }

                    var bytes = new byte[3];
                    reader.ReadBytes(bytes);

                    NewMessageReady?.Invoke(this, new MessageSentEventArgs { Bytes = bytes });
                }
            }
            catch
            {               
            }
        }       

        public void Dispose()
        {
            if (tcpListener != null)
                tcpListener.Dispose();
        }
    }

    public class MessageSentEventArgs : EventArgs
    {       
        public byte[] Bytes;
    }

}

Klasa LightsController odpowiada za sterowanie światłem:

public class LightsController: IDisposable
    {
        private const int REDLED_PIN = 5;
        private const int ORANGELED_PIN = 6;
        private const int BLUELED_PIN = 13;

        PwmPin redPin;
        PwmPin orangePin;
        PwmPin bluePin;

        PwmController pwmController;

        public async Task InitAsync()
        {
            pwmController = (await PwmController.GetControllersAsync(PwmSoftware.PwmProviderSoftware.GetPwmProvider()))[0];
            pwmController.SetDesiredFrequency(100);

            redPin = InitPin(REDLED_PIN);
            orangePin = InitPin(ORANGELED_PIN);
            bluePin = InitPin(BLUELED_PIN);

            SetCyclePercentage(redPin, 1);
            SetCyclePercentage(orangePin, 1);
            SetCyclePercentage(bluePin, 1);

            Task.Delay(50).Wait();

            SetCyclePercentage(redPin, 0);
            SetCyclePercentage(orangePin, 0);
            SetCyclePercentage(bluePin, 0);
        }

        public void SetValues(double bassValue, double midValue, double trebleValue)
        {
            SetCyclePercentage(redPin, bassValue);
            SetCyclePercentage(orangePin, midValue);
            SetCyclePercentage(bluePin, trebleValue);
        }

        public void Stop()
        {
            StopPin(redPin);
            StopPin(orangePin);
            StopPin(bluePin);
        }

        private PwmPin InitPin(int pinNumber)
        {
            var pin = pwmController.OpenPin(pinNumber);           
            pin.Start();          

            return pin;
        }

        private void SetCyclePercentage(PwmPin pin, double value)
        { 
            if (value >= 0 && value <=1)         
                pin.SetActiveDutyCyclePercentage(value);
        }

        private void StopPin(PwmPin pin)
        {
            if (pin.IsStarted)
                pin.Stop();
        }

        private void DisposePin(PwmPin pin)
        {
            if (pin != null)
            {
                StopPin(pin);
                pin.Dispose();
                pin = null;
            }
        }

        public void Dispose()
        {
            DisposePin(redPin);
            DisposePin(orangePin);
            DisposePin(bluePin);
        }
    }

Należy tu skomentować dwie sprawy. Pierwsza, PwmController jest od jakiegoś czasu w Windows Api, ale domyślna instancja okazuje się być null-em, jeśli nie mamy jakiejś sprzętowej kompozycji. Możemy jednak zarejestrować sobie softwarowy PWM provider dostarczany przez Microsoft na github. Druga sprawa to kwestia dobrego zainicjowania jasności świecenia diód. Otóż nie ma lekko, po ustawieniu zera procentowego sygnału i starcie “pinu” dioda … świeci i to maksymalnie, zamiast być kompletnie zgaszona, a potem nie reaguje dobrze na przychodzące komendy dotyczące stopnia jasności. Chodzi o to by zero cyklu oznaczało całkowite zgaśnięcie, a 1 maksymalne świecenie. Wzorując się na patencie z https://github.com/jmservera/RgbLed po “wystartowaniu” pin-ów najpierw je zapalam maksymalnie, po czym po 50 ms je zupełnie gaszę. W efekcie przy uruchamianiu aplikacji widzimy króciutki błysk diód zanim przejdą w stan gotowości do sterowania jasnością.

Całość app-ki spięta jest w MainPage.xaml.cs.  Nie umieszczałem serwera socketów w tle. Do prostego celu jaki tu omawiamy jest to wystarczające:

public sealed partial class MainPage : Page
    {
        private LightsController _controller;
        private SocketServer _socketServer;

        private bool _isIoT;

        public MainPage()
        {
            this.InitializeComponent();

            _isIoT = IsIoT();
        }

        private bool IsIoT()
        {
            return AnalyticsInfo.VersionInfo.DeviceFamily == "Windows.IoT";
        }       

        protected override async void OnNavigatedTo(NavigationEventArgs e)
        {
            base.OnNavigatedTo(e);

            if (_isIoT)
            {
                _controller = new LightsController();
                await _controller.InitAsync();

                _socketServer = new SocketServer();
                _socketServer.StartListener();
                _socketServer.NewMessageReady += SendCommand;
            }          
        }       

        private void SendCommand(object sender, MessageSentEventArgs e)
        {
            if (e.Bytes == null || e.Bytes.Length != 3)
                return;

            var bassValue = e.Bytes[0] / 255d;
            var midValue = e.Bytes[1] / 255d;
            var trebleValue = e.Bytes[2] / 255d;

            _controller.SetValues(bassValue, midValue, trebleValue);
        }

        protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
        {
            if (_isIoT)
            {
                if (_socketServer != null)
                {
                    _socketServer.NewMessageReady -= SendCommand;
                    _socketServer.Dispose();
                    _socketServer = null;
                }

                if (_controller != null)
                {
                    _controller.Dispose();
                    _controller = null;
                }
            }

            base.OnNavigatingFrom(e);
        }
    }

Dla przypomnienia aplikacja jest uniwersalna, można ją zdeploypować także na inne odmiany Windows 10 (np. desktop). Aktualnie jej prawie cały kod działa z użyciem API z Windows IoT Extension (może kiedyś w przyszłości rozbuduję ją do odpowiednika znanego z Androida). Jeśli jednak ktoś uruchomiłby ją na czymś innym, to aby nie poszedł na dzień dobry wyjątek używam zmiennej _isIoT.

Aha, oczywiście app-ka musi mieć w manifeście nadane uprawnienia internetClient, internetClientServer i privateNetworkClientServer. Bycie serwerem w sieci w Windows 10 IoT jest możliwe i odpowiednie do poruszanej przez nas problematyki (urządzenia IoT mają małą wydajność, więc nie jest to oczywiście najlepszy pomysł na serwer w ogóle, zwłaszcza w Internecie czy gdy nie panujemy nad ruchem sieciowym).

LightOrganTestApp

Napisałem sobie też drugą testową aplikację (także uniwersalną, choć nie ma to tutaj dużego znaczenia), która na wskazany adres (i port zaszyty w kodzie) wysyła odpowiedni ciąg trzech bajtów tworzony na podstawie danych wpisywanych do okna (od 0 do 255). Jej zasadniczy kod znajdziemy w MainPage.xaml.cs:

public sealed partial class MainPage : Page
    {
        private StreamSocket _streamSocket;
        private static DataWriter _writer;

        public MainPage()
        {
            this.InitializeComponent();
        }       

        private async void Connect(string serverIP, string serverPort)
        {
            try
            {
                var hostName = new HostName(serverIP);
                _streamSocket = new StreamSocket();
                await _streamSocket.ConnectAsync(hostName, serverPort);
                _writer = new DataWriter(_streamSocket.OutputStream);

                var dialog = new MessageDialog("Connection OK");
                await dialog.ShowAsync();
            }
            catch
            {
                var dialog = new MessageDialog("Connection error");
                await dialog.ShowAsync();
            }
        }

        private async void SendBytes(byte[] bytes)
        {
            _writer.WriteBytes(bytes);
            await _writer.StoreAsync();
        }

        private void connectBtn_Click(object sender, RoutedEventArgs e)
        {
            Connect(hostTxt.Text, "8181");
        }

        private void test1Btn_Click(object sender, RoutedEventArgs e)
        {
            byte bassValue = Convert.ToByte(bassTxt.Text);
            byte midValue = Convert.ToByte(midTxt.Text);
            byte trebleValue = Convert.ToByte(trebleTxt.Text);
          
            SendBytes(new byte[] { bassValue, midValue, trebleValue });
        }       
    }

Aplikacja testowa to oczywiście szybki prototyp, nie kładę pary w jego dopracowanie (np. zabezpieczenie pól tesktowych przed wpisywaniem złych wartości czy nawet disablowanie kontrolek przy braku połączenia), ponieważ w najbliższym czasie skupię się już na docelowej integracji w Androidzie. Ta app-ka jest przeznaczona tylko do początkowych wewnętrznych testów.

Źródła powyższych obu app-ek są na github.

Na koniec kilka pytań. Czy komunikacja przez zwykłe czyste sokety była jedyną możliwością? Nie. W grę wchodziło choćby nawiazanie zdalnej sesji SSH (na pewno Raspbian, nie wiem jak szczegóły wyglądają w przypadku Windows 10) czy popularny dla gadżetów Bluetooth (nie mam na ten moment stosownego rozszerzenia do Raspberry Pi 2, może wrócę kiedyś do tematu przy Raspberry Pi 3?). W tematyce sieciowej, której tutaj nie zgłębiałem jest jeszcze kwestia stawiania sieci Wi-Fi przez samo Raspberry Pi. Nie jest to jednak konieczne, jeśli mamy już inną sieć Wi-Fi (a tak zwykle jest).

Brak komentarzy: