以前、Qiita HackathonでiRemoconをいただきました。

iPhoneがあらゆるリモコンになるという画期的な製品なのですが、公式アプリは4インチ非対応な上に
iPhoneとあまりマッチしないUIとなっているので、自分用に代用品をつくろうと思いました。

おそらく、そう考える人はたくさんいると思うので、通信の仕方を紹介します。

iRemoconにはTCP/IP通信でコマンドを送信することができます。
TCP/IP通信を行う手段にはNSStreamを採用しました。

NSStreamのペアを作成する

NSStream自体は抽象クラスで、実際にはNSInputStreamNSOutputStreamのペアを利用します。
CFStreamCreatePairWithSocketToHostを利用するとペアを作成してくれます。

CFReadStreamRef readStream = NULL;
CFWriteStreamRef writeStream = NULL;
CFStringRef hostNameRef = (__bridge CFStringRef)@"10.0.1.3";

CFStreamCreatePairWithSocketToHost(NULL,
                                   hostNameRef,
                                   51013,
                                   &readStream,
                                   &writeStream);

NSInputStream *inputStream = (__bridge_transfer NSInputStream *)readStream;
NSOutputStream *outputStream = (__bridge_transfer NSOutputStream *)writeStream;

NSRunLoopにスケジュールする

NSStreamNSRunLoopにスケジュールするとデリゲートメソッドで通知を受け取ることができます。

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
NSString *mode = NSDefaultRunLoopMode;

inputStream.delegate = self;
[inputStream scheduleInRunLoop:runLoop forMode:mode];
[inputStream open];

outputStream.delegate = self;
[outputStream scheduleInRunLoop:runLoop forMode:mode];
[outputStream open];

selfが何者かは特に指定していませんが、NSStreamDelegateに適合しているものとします。
これらがメインスレッド以外のスレッドで実行される場合にはNSRunLoopを回す必要があります。
以下は非同期型のNSOperationでの実装例です。

do {
    @autoreleasepool {
        [self.runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:.1]];
        if (self.isCancelled) {
            [self unscheduleStreams];
            [self completeOperation];
        }
    }
} while (self.isExecuting);

NSStreamのイベントを受け取る

イベントに対して行う処理は以下の通りです。

  • NSStreamEventHasSpaceAvailable: NSOutputStreamにiRemoconのコマンドを書き込む
  • NSStreamEventHasBytesAvailable: NSInputStreamからレスポンスを読み込む

イベントはNSStreamDelegatestream:handleEvent:で受け取ります。

- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode
{
    switch(eventCode) {
        case NSStreamEventHasBytesAvailable:
            if (stream == self.inputStream) {
                [self readBuffer];
            }
            break;
            
        case NSStreamEventHasSpaceAvailable:
            if (stream == self.outputStream) {
                [self sendCommand];
            }
            break;
        
        ...
    }
}

NSOutputStreamにコマンドを書き込む

コマンドの形式は公式の技術資料にある通りで、通信確認を行うためには*au\r\nを送信します。
送信後はNSOutputStreamが要らなくなるのでNSRunLoopから外します。

- (void)sendCommand
{
    NSString *command = @"*au\r\n";
    const uint8_t *ccommand = (const uint8_t *)[command UTF8String];
    
    [self.outputStream write:ccommand maxLength:strlen((const char *)ccommand)];
    [self.outputStream close];
    [self.outputStream removeFromRunLoop:self.runLoop forMode:self.mode];
}

NSInputStreamからレスポンスを読み込む

結果がok\r\nならば通信成功です。
レスポンスの末尾は\r\nなので、\r\nを受け取ったらNSInputStreamNSRunLoopから外します。

- (void)readBuffer
{
    uint8_t buffer[1024];
    unsigned int length = [self.inputStream read:buffer maxLength:1024];
    [self.data appendBytes:buffer length:length];
    
    NSString *joinedString = [[NSString alloc] initWithData:self.data encoding:NSUTF8StringEncoding];
    if ([joinedString rangeOfString:@"\r\n"].location != NSNotFound) {
        [self.inputStream close];
        [self.inputStream removeFromRunLoop:self.runLoop forMode:self.mode];
    }
}

以上で完了です。

これらの手順を非同期型のNSOperationにまとめてGitHubに上げました。
IRMCommandOperation

IRMCommandOperationを利用すると、以下のようにしてコマンドを送信出来ます。

IRMCommandOperation *operation =
[[IRMCommandOperation alloc] initWithCommand:@"is"
                                    argument:@"1000"
                                     handler:^(NSData *data, NSError *error) {
                                         // completion
                                     }];
[operation start];