czwartek, 26 maja 2016

[DSP2016] iOS prosto z poligonu odc.8 (sockety)

Dziś święto, początek długiego weekendu, ale DSP to DSP i rządzi się swoimi prawami. Sockety. Pewnie wszyscy znamy, ale nie wszystkim kojarzą się z interesującymi rzeczami. Aby choć trochę to zmienić, pokażemy “przejęcie władzy” nad diodami podłączonymi do Raspberry Pi. W roli “pilota” tym razem wystąpi iPhone (wcześniej pisałem o samej aplikacji Windows 10 na Raspberry oraz komunikacji socketowej z Android).

WP_20160526_09_54_54_Pro

Najpierw szczypta edukacji. W dokumentacji Apple całkiem przydatne okazują się strony Using Sockets and Socket Streams i Introduction to Stream Programming Guide for Cocoa. Można zerknąć na sampla SimpleNetworkStreams. Jak ktoś lubi patrzeć szerzej bardziej przekrojowe Networking Overview ukaże różne formy i oblicza komunikacji sieciowej…  Jednak najbardziej do gustu przypadł mi blog z postem Communication between iOS device (Client) and Raspberry Pi (Server), a zwłaszcza Sending RSA encrypted message - From iOS device to Python socket server (iOS Part).

Te źródła informacji przyczyniły się do powstania poniższego kodu:

class ViewController: UIViewController, NSStreamDelegate  {

    …        
    var outStream: NSOutputStream?

override func viewWillAppear(animated: Bool) {
       super.viewWillAppear(animated)
      
       self.defaultsChanged()
      
       NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ViewController.defaultsChanged), name: NSUserDefaultsDidChangeNotification, object: nil)
  
       //test
       onLightOrganDataUpdated(0.3, midLevel: 1, trebleLevel: 0)
   }

func onLightOrganDataUpdated(bassLevel: CGFloat, midLevel: CGFloat, trebleLevel: CGFloat) {
        setLight(bassLight, ratio: bassLevel)
        setLight(midLight, ratio: midLevel)
        setLight(trebleLight, ratio: trebleLevel)
       
        let bassValue = UInt8(round(255 * bassLevel))
        let midValue = UInt8(round(255 * midLevel))
        let trebleValue = UInt8(round(255 * trebleLevel))
        let bytes:[UInt8] = [bassValue, midValue, trebleValue]
       
        sendCommand(bytes)
    }
   
    func sendCommand(bytes: [UInt8]) {       
        let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
       
        dispatch_async(queue) {
            self.outStream?.write(UnsafePointer<UInt8>(bytes), maxLength: bytes.count)
        }
    }
   
    func createNewSocket(defaults: NSUserDefaults) {
        let host = defaults.stringForKey("remote_device_host_preference")
        let port = defaults.integerForKey("remote_device_port_preference")
       
        if host != nil && port > 0 {
            NSStream.getStreamsToHostWithName(host!, port: port, inputStream: nil, outputStream: &outStream)
           
            outStream?.delegate = self
            outStream?.scheduleInRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode)
            outStream?.open()
        }
    }
   
    func releaseOutStream() {
        outStream?.delegate = nil
        outStream?.removeFromRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode)
        outStream?.close()
        outStream = nil
    }
   
    func stream(aStream: NSStream, handleEvent eventCode: NSStreamEvent) {
        switch eventCode {
        case NSStreamEvent.EndEncountered:
            print("EndEncountered")
            releaseOutStream()
        case NSStreamEvent.ErrorOccurred:
            print("ErrorOccurred")
            releaseOutStream()
        case NSStreamEvent.HasSpaceAvailable:
            print("HasSpaceAvailable")
        case NSStreamEvent.None:
            print("None")
        case NSStreamEvent.OpenCompleted:
            print("OpenCompleted")
        default:
            print("Unknown")
        }
    }
   
    func defaultsChanged() {
        let defaults = NSUserDefaults.standardUserDefaults()
        let useRemoteDevice = defaults.boolForKey("use_remote_device_preference")
       
        if outStream != nil {
            releaseOutStream()
        }
       
        if useRemoteDevice {
            createNewSocket(defaults)
        }
    }
}

Kod ten możecie sprawdzić, jest już na github. Wyjaśnijmy go teraz!

Co trzeba zrobić, by przygotować łączność z danym adresem i portem hosta? 

U mnie funkcja createNewSocket wywołuje absolutnie najważniejszą systemową funkcję NSStream.getStreamsToHostWithName, która zwraca dla danego adresu hosta i portu strumień wejściowy i wyjściowy, przy pomocy których możemy coś odczytywać lub zapisywać (zdaje się, że dopiero od iOS8 nie trzeba robić przejściówek do bardziej corowego API w C). Interesuje mnie tylko wysyłanie do serwera, dlatego obsłużyłem tylko strumień wyjściowy. Kolejną ważną rzeczą jest odbiór zdarzeń ze strumienia. Tu wystarczy zaimplementować protokół NSStreamDelegate z funkcją func stream(aStream: NSStream, handleEvent eventCode: NSStreamEvent). Trzecim ważnym aspektem jest przetwarzanie strumienia w pętli (za pomocą scheduleInRunLoop).

Jak wysłać bajty z klienta na serwer?

Zapisujemy po prostu bajty do strumienia wyjściowego. Hurra! Działa. Nie zaprzątam sobie na razie głowy zabezpieczaniem komunikacji, jak czyni to autor polecanego przeze mnie postu. Zacząłem jednak troche to testować przy różnej konfiguracji ustawień i wyszło kilka przypadłości. Trzeba będzie w niedalekiej przyszłości zmierzyć się z utrzymaniem socketów przy przejściu aplikacji do background (przy okazji jak w ogóle będę odkrywać tajniki pracy w tle na iOS). Z tego co patrzyłem nie bedzie to łatwe. Dziś natomiast poprawiłem coś bardziej trywialnego. W przypadku podania błędnych namiarów na serwer w konfiguracji app-ka mi się przywieszała, pokazując na jakiś czas biały ekran. Aby tak się nie działo, w funkcji sendCommand zacząłem korzystać z innego wątku do wysyłania. Teraz działa wszystko jak należy.

Na razie jedynie dla celów testowych wywołuję sobie funkcję onLightOrganDataUpdated. Jej implementację wzorowałem na uprzednio stworzonej wersji dla Android. To tyle na teraz.

Brak komentarzy: