FirstAppはOPH-5000iのBluetooth MFi と接続し、データの送受信を行うサンプルコードです。

FirstAppのダウンロード

  1. 次のリンクからFirstAppのzipファイルをダウンロードします。
  2. zipファイルを解凍すると次のファイルが得られます。
    FirstApp
    FirstApp.xcodeproj
  3. XcodeでFirstApp.xcodeprojを開きます。
  4. iOSデバイスとMac接続し、XcodeでFirstAppをビルドします。

FirstAppの動作手順

  1. iOS デバイスの[設定]を起動し、Bluetooth をオンにします。
  2. OPH-5000iサンプルアプリケーションでiOS12桁のBluetoothアドレスバーコードを読み取ります。
  3. 接続済みの状態になりましたら、本サンプル(FirstApp)を起動します。
  4. データを受信します
    OPH-5000iサンプルアプリケーションでバーコードを読み取ります。
    本サンプル(FirstApp)のテキストビューにOPH-5000iから送信したデータを表示されます。
  5. データを送信します。
    「Sent TestString」ボタンを押すと「TestString」文字列をOPH-5000iに送信します。

 OPHBluetoothServiceクラスは、OPH-5000iとBluetooth通信を行い、セッション管理、アクセサリ保持、I/O処理等を担当するコアクラスです。

 OPHBluetoothServiceは、GoFのSingletonパターンで実装しています。OPHBluetoothServiceクラスのインスタンスは、実行中ただ1つしか存在しません。

 OPHBluetoothServiceクラスでは、外部アクセサリとの通信にExternalAccessory.frameworkを利用します。


サンプル:OPHBluetoothService.m
//
//  OPHBluetoothService.m
//
//  Copyright (c) 2021年OPTOELECTRONICS CO., LTD. All rights reserved.
//

#import "OPHBluetoothService.h"

/** OPHBluetoothServiceは常に現在のアクセサリ、プロトコル、セッション、読み書きバッファをつずつ保持します。
 *  setupControllerForAccessory アクセサリ、プロトコルの初期化(セッション、バッファが破棄されていない場合は破棄)
 *  openSession セッション、バッファの生成(アクセサリ、プロトコルが初期化されていない場合は無効)
 *  closeSession セッション、バッファの破棄(セッション、バッファが生成されていない場合は無効)
 */
@interface OPHBluetoothService()  {
    EAAccessory    *__weak _accessory;       // 現在接続しているEAAccessory
    EASession      *_currentSession;  // 現在接続しているセッション
    NSString       *_protocolString;  // 現在接続しているprotocolString
    NSMutableData  *_writeData;       // 書込データのバッファ
    NSMutableData  *_readData;        // 読込データのバッファ
    NSMutableArray *_sessions;        // 生成したセッション
}
@end

static NSString * const OPH5000iProtocolString = @"jp.opto.opnprotocol";
static NSString * const OPH5000iAccessoryModelNumber = @"OPH-5000i";
//**************************************************************************************************
// @name OPHBluetoothService
//**************************************************************************************************


@implementation OPHBluetoothService

- (id)init
{
    self = [super init];
    if (self) {
        _sessions = [[NSMutableArray alloc] init];
    }
    return self;
}

#pragma mark - Memory Management
//--------------------------------------------------------------------------------------------------
// Memory Management
//--------------------------------------------------------------------------------------------------

- (void)dealloc
{
    [self closeSession];
    
    RELEASE_TO_NIL(_currentSession);
    RELEASE_TO_NIL(_writeData);
    RELEASE_TO_NIL(_readData);
    RELEASE_TO_NIL(_protocolString);
    RELEASE_TO_NIL(_accessory);
    RELEASE_TO_NIL(_sessions);
}
//--------------------------------------------------------------------------------------------------
// Public Methods
//--------------------------------------------------------------------------------------------------

- (void)setupControllerForAccessory:(EAAccessory *)accessory_ withProtocolString:(NSString *)protocolString
{
    // 引数チェック
    if (accessory_ == nil) {
        NSLog(@"arguments error: accessory is nil.");
        return;
    }
    if (protocolString == nil) {
        NSLog(@"arguments error: protocolString is nil.");
        return;
    }
    
    // アクセサリ、セッションの両方が存在する場合
    if (_accessory && _currentSession) {
        // 与えられたアクセサリの接続IDが現在と同じ場合、何もしない。
        if (accessory_.connectionID == _currentSession.accessory.connectionID) {
            NSLog(@"session is already connected.");
            return;
        }
        // 接続IDが異なる場合は、現在のセッションをクローズする。
        else {
            [self closeSession];
        }
    }

    _accessory = nil;
    _accessory = accessory_;
    _accessory.delegate = self;

    RELEASE_TO_NIL(_protocolString);
    _protocolString = [protocolString copy];
    NSLog(@"accessory changed. modelNumber:%@, connectionID:%lu", _accessory.modelNumber, (unsigned long)_accessory.connectionID);
}

- (BOOL)openSession
{
    if (_accessory == nil) {
        [self _initAccessoryAndProtocolString];
        if (_accessory == nil) {
            NSLog(@"accessory not found.");
            return NO;
        }
        _accessory.delegate = self;
    }
    
    NSLog(@"accessory found. modelNumber:%@, connectionID:%lu", _accessory.modelNumber, (unsigned long)_accessory.connectionID);
    // ModelNumbeOPH-5000iの場合openSession
    if (![_accessory.modelNumber isEqualToString:OPH5000iAccessoryModelNumber]){
        NSLog(@"ModelNumber is not OPH-5000i.");
        return NO;
    }

    if ( _currentSession && _currentSession.accessory.connectionID == _accessory.connectionID ) {
        NSLog(@"session is already opened.");
        return YES;
    }
    
    // これまで生成されたセッションがあるか
    BOOL exists = NO;
    for (EASession *s in _sessions) {
        if (s.accessory.connectionID == _accessory.connectionID) {
            _currentSession = s;
            exists = YES;
            NSLog(@"session found already opened in past.");
            break;
        }
    }
    
    // 現在のセッションがない場合、または、セッションありかつこれまで生成されていない場合、
    if (_currentSession == nil || !exists) {
        EASession *session = [[EASession alloc] initWithAccessory:_accessory
                                                      forProtocol:OPH5000iProtocolString];
    
        if (!session) {
            NSLog(@"create session failed. session is aleary exists?");
            return NO;
        }
        [_sessions addObject:session];
        _currentSession = session;

        NSLog(@"new session created.");

        [[_currentSession inputStream] setDelegate:self];
        [[_currentSession inputStream] scheduleInRunLoop:[NSRunLoop currentRunLoop]
                                                 forMode:NSDefaultRunLoopMode];
        [[_currentSession inputStream] open];
        
        [[_currentSession outputStream] setDelegate:self];
        [[_currentSession outputStream] scheduleInRunLoop:[NSRunLoop currentRunLoop]
                                                  forMode:NSDefaultRunLoopMode];
        [[_currentSession outputStream] open];
        
        NSLog(@"created session:%@", _currentSession);
        return YES;
    }
    else {
        NSLog(@"create session fail by accessory. modelNumber:%@, connectionID:%lu", _accessory.modelNumber, (unsigned long)_accessory.connectionID);
        return NO;
    }
}

- (void)closeSession
{
    if (_currentSession) {
        if (_writeData) {
            [_writeData setLength:0];
        }
        if (_readData) {
            [_readData setLength:0];
        }
        RELEASE_TO_NIL(_writeData);
        RELEASE_TO_NIL(_readData);
        NSLog(@"current session closed.");
    }
    else {
        NSLog(@"current session already closed.");
    }
}

- (BOOL)writeData:(NSData *)data
{
    RELEASE_TO_NIL(_writeData)
    if (_writeData == nil) {
        _writeData = [[NSMutableData alloc] init];
    }
    #define MAX_OPH_RECEIVE_SIZE 2036
    if (data.length > MAX_OPH_RECEIVE_SIZE){
        NSLog(@"over size error.");
        return NO;
    }
    [_writeData appendData:data];
    [self _writeData];
    return YES;
}

- (void)writeACK
{
    RELEASE_TO_NIL(_writeData)
    _writeData = [[NSMutableData alloc] init];
    uint8_t ack[1] = {0x06};
    [self writeData:[NSData dataWithBytes:ack length:sizeof(ack)]];
}

- (void)writeNAK
{
    _writeData = [[NSMutableData alloc] init];
    uint8_t ack[1] = {0x15};
    [self writeData:[NSData dataWithBytes:ack length:sizeof(ack)]];
}

#pragma mark - EAAccessoryDelegate
//--------------------------------------------------------------------------------------------------
// EAAccessoryDelegate
//--------------------------------------------------------------------------------------------------

/** EAAccessoryが切断された場合に呼び出されます。
 */
- (void)accessoryDidDisconnect:(EAAccessory *)accessory_
{
    EASession *removableSession = nil;
    for (EASession *session in _sessions) {
        if (session.accessory.connectionID == accessory_.connectionID) {
            removableSession = session;
            break;
        }
    }
    if (removableSession) {
        [_sessions removeObject:removableSession];
        if ([removableSession isEqual:_currentSession]) {
            [[_currentSession inputStream] close];
            [[_currentSession outputStream] close];
            _currentSession = nil;
        }
        NSLog(@"session closed and removed when accessory did disconnected.");
    }
}

#pragma mark - NSStreamDelegateEventExtensions
//--------------------------------------------------------------------------------------------------
// NSStreamDelegateEventExtensions
//--------------------------------------------------------------------------------------------------

/** NSStreamのイベントハンドラです。
 * @param aStream
 * @param eventCode
 */
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
{
    switch (eventCode) {
        case NSStreamEventNone:
            break;
        case NSStreamEventOpenCompleted:
            break;
        case NSStreamEventHasBytesAvailable:
            [self _readData];
            break;
        case NSStreamEventHasSpaceAvailable:
            [self _writeData];
            break;
        case NSStreamEventErrorOccurred:
            break;
        case NSStreamEventEndEncountered:
            break;
        default:
            break;
    }
}

#pragma mark - Private Methods
//--------------------------------------------------------------------------------------------------
// Private Methods
//--------------------------------------------------------------------------------------------------

/** セッションにデータを書き込みます。
 * _sessionのoutputStreamに書き込めるスペースがある場合、かつ、書込バッファ(_writeData)にデータが存在する場合に
 * _sessionへの書込を続けます。
 */
- (void)_writeData
{
    while (([[_currentSession outputStream] hasSpaceAvailable]) && ([_writeData length] > 0)) {
        NSInteger bytesWritten = [[_currentSession outputStream] write:[_writeData bytes]
                                                             maxLength:[_writeData length]];
        
        NSLog(@"Written Data : %@ , bytesWritten = %ld", _writeData, (long)bytesWritten);
        
        if (bytesWritten == -1) {
            NSLog(@"Write error");
            break;
        }
        else if (bytesWritten > 0) {
            [_writeData replaceBytesInRange:NSMakeRange(0, bytesWritten) withBytes:NULL length:0];
        }
    }
}

/** セッションのデータを読み込みます。
 * _sessionのinputStreamにデータが存在する場合、読込バッファ(_readData)への読込を続けます。
 */
- (void)_readData
{
#define EAD_INPUT_BUFFER_SIZE 2048
    uint8_t buf[EAD_INPUT_BUFFER_SIZE] = {0};
    
    while ([[_currentSession inputStream] hasBytesAvailable]) {
        NSInteger bytesRead = [[_currentSession inputStream] read:buf maxLength:EAD_INPUT_BUFFER_SIZE];
        if (_readData == nil) {
            _readData = [[NSMutableData alloc] init];
        }
        
        [_readData appendBytes:(void *)buf length:bytesRead];

        // inputStream のバッファに蓄積させるためにスリープして待機する
        [NSThread sleepForTimeInterval:0.1];
    }

    if ([_readData length] != 0) {
        [self receivedData:_readData];
    }
}

- (void)receivedData:(NSData *)readdata
{
    NSLog(@"receivedData : _readData : %@",_readData);
    id<OPHBluetoothServiceDelegate> delegate =
    (id<OPHBluetoothServiceDelegate>)self.delegate;
    //outputSteamの受信完了後、OPH-5000iにACKを返信する必要がある
    [self writeACK];
    if ([delegate respondsToSelector:@selector(bluetoothService:receivedData:)]) {
        [delegate bluetoothService:self receivedData:readdata];
    }
    [self performSelector:@selector(clearReadData)];
}

- (void)clearReadData
{
    [_readData setLength:0];
}

/** EAAccessoryManager から接続中のEAAccessoryを取得し、プロパティに設定します。
 */
- (void)_initAccessoryAndProtocolString
{
    NSArray *connectedAccessories =
    [[NSMutableArray alloc] initWithArray:
      [[EAAccessoryManager sharedAccessoryManager] connectedAccessories]];
    
    if (connectedAccessories == nil || connectedAccessories.count == 0) {
        return;
    }
    
    for (EAAccessory *accessory in connectedAccessories) {
        NSArray *protocolStrings = [accessory protocolStrings];
        if ([protocolStrings count] > 0) {
            NSString *key = accessory.serialNumber;
            if ([key length] > 0) {
                NSUserDefaults *userDefault = [NSUserDefaults standardUserDefaults];
                NSString *termName = [userDefault objectForKey:key];
                if (termName == nil) {
                    [userDefault setObject:[accessory modelNumber] forKey:key];
                }
                NSString *protocol = protocolStrings[0];
                if (([protocol isEqualToString:OPH5000iProtocolString]) && ([termName isEqualToString:OPH5000iAccessoryModelNumber])){
                    [self setupControllerForAccessory:accessory
                                   withProtocolString:protocol];
                    break;
                }
            }
        }
    }
}

+ (OPHBluetoothService *)sharedController
{
    static OPHBluetoothService *sessionController = nil;
    if (sessionController == nil) {
        sessionController = [[OPHBluetoothService alloc] init];
    }
    return sessionController;
}
@end

関連事項

最終更新日:2021/10/29