<概要>
ここでは浅草ギ研製ロボット神経システムとWiMaster経由でiPhone端末につないでみます。
ロボット神経システムは浅草ギ研で製造販売しているサーボコントローラやセンサー入力ボード群で、1つのシリアル通信経路にいろいろな機能を数珠(じゅず)つなぎにでき、沢山の機能を組み合わせたり、自由に拡張したりするもので、自作ロボットの作成などに向いています。
Xcode(4.6.3)上で作成したiPhone4(6.1.3)のObject-Cアプリを圧縮したものがこちらになります。
WiMaster_test3
<注意>
ここでご紹介するプログラムはあくまでもサンプルプログラムとなります。個別のご質問などにはお答えできませんのでご了承下さい。
機種やOSのバージョン違いや環境の違いなどにより動作しない可能性もあります。また、今後のiPhoneのOSバージョンアップなどに対しての動作も定かではありません。
紹介動画
<ブロック図>
以下、ブロック図とシステム概要です。画像はAndroid端末の図と写真をそのまま流用していますが、端末がiPhoneに変わるだけで、構成は同じです。PCはMACで、開発環境はXcode4.6.3、ARC(プロジェクト作成時にARC使用にチェック)使用です。(Androidの例はこちら)
<接続図>
<プログラム説明>
ここでは、iPhone端末とWiMasterはすでにアソシエーションまで済んでいるとします。この操作についてはWiMasterの説明ページの<動作説明>の部分を参照願います。
通信はSocket通信で行います。
このページの冒頭に、Xcodeのプロジェクトファイルがあるのでダウンロードして解答してください。
画面イメージ
スライダを動かすとサーボが動き、「センサー読み取り」ボタンを押すと#3のセンサーの値をテキストフィールドに表示します。
以下、プログラムの説明をします。ARCを使用しています。
このプロジェクトは次の3つのクラスで構成されています。
1)AppDelegate :プロジェクト作成時に自動で作られる。(説明を割愛します。)
2)ViewController :画面操作や表示
3)WiFiConnect :WiMasterとの通信関連
ViewControllerとWiFiConnectの構成は次のようになります。
サーボを動かすときはソケットを通じてコマンドのバイト列を送ります。
センサーから返ってきたデータはWiFiConnectで受けますが、それをViewControllerのテキストフィールドに表示させるために、<WiFiReceiveDelegate>というデリゲートを用意しました。WiFiConnect側でプロトコル宣言し、ViewController内で実装します。詳しくは後ほど説明します。
■起動〜ソケット接続まで
起動すると、まずViewControllerのViewDidLoadが呼び出され、スライダなどのユーザーインターフェイスを配置し、WiFiConnectクラスを実体化してソケットを開通させています。
ユーザーインターフェイスの配置などは一般的な事項ですので説明を割愛します。この部分はソースを参照願います。
Aの部分から説明します。ViewControllerの起動時にconnectメソッドを呼び出しています。この部分は次のようになっています。
//////////////////////////////////////////////////////////////
// WiMasterと接続
//////////////////////////////////////////////////////////////
-(void)connect
{
_conn = [[WiFiConnect alloc] initWithHostName:@"n.n.n.n"
port:NNNN timeout:30];
_conn.delegate = self; //受信イベント受け取りの為、デリゲートをセット
if([_conn openSocket]){
NSLog(@"openSocket success");
}else{
NSLog(@"openSocket Error");
}
}
n.n.n.nの部分にはIPアドレスが、NNNNの部分にはポート番号が入ります。IPアドレスとポート番号はWiMaster購入時に添付される紙に印刷されていますので、その値を記入します。ここではWiFiConnectクラスを実体化し、_connを作成しています。
実際にSocket接続する部分はWiFiConnect内になります。
BWiFiConnectのコンストラクタではIPとPORT番号を引数に、初期化を行います。
//////////////////////////////////////////////////////////////
// コンストラクタ
//////////////////////////////////////////////////////////////
-(id)initWithHostName:(NSString *)hostName port:(UInt32)port
timeout:(int)timeoutSec{
self = [super init];
if(self){
_hostName = hostNam;
_port = port;
_readStream = nil;
_writeStream = nil;
}
return self;
}
次がSocketを開く部分です。
CiPhoneでソケット通信をする方法はいくつかあるらしいのですが、今回はNSStreamを使っています。iPhoneのWiFi機能とNSStreamは直接接続することができないので、CoreFoundationのクラスでSocketを作った後に、NSStreamに__bridge_transferで渡しています。transferなのでCFStreamはNSStreamに渡した後に(たぶん?)消滅します。
//////////////////////////////////////////////////////////////
// ソケットオープン
//////////////////////////////////////////////////////////////
-(BOOL)openSocket
{
//CFStreamで開いて、NSStreamに渡す
CFReadStreamRef readStream;
CFWriteStreamRef writeStream;
CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, (__bridge
CFStringRef)_hostName, _port, &readStream,&writeStream);
_readStream = (__bridge_transfer NSInputStream*)readStream;
_readStream.delegate = self;
[_readStream scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
_writeStream = (__bridge_transfer NSOutputStream*)writeStream;
_writeStream.delegate = self;
[_writeStream scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
[_readStream open];
[_writeStream open];
return YES;
}
Socketが開いたら、入力ストリーム・出力ストリームそれぞれをRunLoop(イベントを処理するために裏で動いているループ)に登録して、それぞれをオープンします。これでSocket通信の準備ができました。この後、WiMasterの緑LEDが、点滅から点灯(つきっぱなし)に変わります。
■データを送ってサーボを動かす
画面のスライダを動かすと、サーボが動きます。
@スライダを動かすと、onSliderChangeイベントが実行されます。この中でAsendServoPositionを実行してサーボコントローラへ命令を送っています。
//////////////////////////////////////////////////////////////
// サーボ位置を送信
//////////////////////////////////////////////////////////////
-(void) sendServoPosition:(int)num pos:(float)pos psd:(int)spd
{
uint8_t data[10];
data[0]=255; //sync byte
data[1]=3; //board id(AGB65-RSC2 default id)
data[2]=4; //data length
data[3]=2; //com(single servo drive command)
data[4]=num; //servo number
data[5]=pos; //position
data[6]=spd; //speed
[_conn writeData:data length:7];
}
WiFiConnectのインスタンスである_connのwriteDataメソッドに、命令のバイト列を渡すと送信されます。
B実際にSocket通信で送信しているのはWiFiConnect内になります。
//////////////////////////////////////////////////////////////
// データ送信
//////////////////////////////////////////////////////////////
- (bool)writeData:(const void*)data length:(NSUInteger)len
{
BOOL ret = NO;
NSInteger leftlen = len;
if(leftlen <= 0) return YES;
while(TRUE){
NSStreamStatus stat = _writeStream.streamStatus;
if(stat == NSStreamStatusOpen || stat == NSStreamStatusWriting){
if([_writeStream hasSpaceAvailable]){
NSInteger count = [_writeStream write:(data + (len - leftlen))
maxLength:leftlen];
if(count >= 0){
leftlen -= count;
if(leftlen <= 0){
ret = YES;
break;
}
}else{
NSLog(@"writeData error %@",_writeStream.streamError.description);
break;
}
}
}else{
NSLog(@"writeData error %u",stat);
break;
}
}
return ret;
}
NSoutputStreamのインスタンス_writeStreamのwriteメソッドを使って送信しています。
■センサーのデータを受信する
センサー読み取りボタンを押すと、テキストフィールドにセンサーの値が表示されます。
この流れは大きく2つの部分からなります。
1)AGB65-ADC(センサーボード)に対して、センサー読み取り命令を発行
2)結果がiPhoneに返され、イベントが発生
流れ図は次のようになります。@〜Bまでは、サーボを動かす部分とほとんど同じ(命令のバイト列が違うだけ)なので説明を割愛します。
以下、赤字の部分のCから説明します。
Cセンサーボードがセンサー読み取り命令を受信すると、結果を返します。この結果のデータ列はWiMasterを経由してiPhoneに入り、<NSStremaDelegate>のDstream:handleEventメソッドが走ります。
//////////////////////////////////////////////////////////////
// Streamのイベント
//////////////////////////////////////////////////////////////
-(void)stream:(NSStream*)stream handleEvent:(NSStreamEvent)eventCode
{
switch(eventCode){
case NSStreamEventHasBytesAvailable: //なにか受信していた時
if(stream == self.readStream){
[self readBuffer];
}
break;
case NSStreamEventHasSpaceAvailable:
//
break;
・
・
・
}
}
<NSStremaDelegate>を実装すると、通信時のいろいろなイベントが拾えるようになります。その中で、NSStreamEventHasBytesAvailableは、何かを受信したときに発生します。実際の読み込みはreadBufferというメソッドを作りそちらで処理しています。
//////////////////////////////////////////////////////////////
// 受信データ読み込み
//////////////////////////////////////////////////////////////
-(void)readBuffer
{
uint8_t buf[1024];
unsigned int length = [self.readStream read:buf maxLength:1024];
NSMutableData *data = [[NSMutableData alloc] initWithLength:length];
[data appendBytes:buf length:length]; //ここまででdataの中に受信済みデータが入る
//ViewControllerにイベントを渡す
if( [self.delegate respondsToSelector:@selector(WiFiDataReceived:)]
){ //ViewControllerでメソッドが存在するかチェック
[self.delegate WiFiDataReceived:data]; //デリゲートで受信データを渡す
}
}
受信データは、inputStreamのインスタンスであるreadStreamのreasメソッドを使い、MSMutableData型のdataに書き込んでいます。
E受信したデータは、独自で作ったデリゲートである<WiFiReceiveDelegate>を使ってViewControllerに渡されます。この部分が渡すところです。
[self.delegate WiFiDataReceived:data];
このイベントはFWiFiDataReceivedで処理されます。
このイベントのデリゲート<WiFiReceivedDelegate>は、WiFiConnectのヘッダで@protocol宣言されています。この部分は次のようになっています。
@protocol WiFiReceivedDelegate<NSObject>
-(void)WiFiDataReceived:(NSMutableData*)data;
@end
@interface WiFiConnect : NSObject<NSStreamDelegate>
・
・
@property (nonatmic, assign) id<WiFiReceivedDelegate>delegate;
・
・
@end
このデリゲートを通じて、WiFiConnect側からViewControllerのWiFiDataReceived:が呼び出されます。
//////////////////////////////////////////////////////////////
// データ受信イベント(WiFiConnectより)
//////////////////////////////////////////////////////////////
-(void)WiFiDataReceived:(NSMutableData *)data{
NSLog(@"data=%@",data);
//#3のセンサー値を調べる(テスト回路では#3に距離センサーがつながっている)
int result = [self checkSensorValue:data sensorNumber:3];
//画面に表示
[_textField setText:[NSString stringWithFormat:@"%d",result]];
}
//返信dataから指定のサーボ番号の結果値を返す
-(int)checkSensorValue:(NSMutableData *)data sensorNumber:(int)sNum{
int result = -1;
//MutableDataをbyte配列に変換
int len = [data length];
Byte rsvData[len];
[data getBytes:&rsvData length:len];
//指定番号のセンサー値を抽出(AGB65-ADCからの返信を解析)
//ヘッダ長が4バイトなので最後から4手前までのデータを確認
for(int i=0; i<len-4; i++){
//返信フォーマット [255][ID(120-127)][バイト長(17)][命令(1)][P1結果][P2結果]...[P16結果]
if(rsvData[i]==255 && rsvData[i+1]==120 && rsvData[i+2]==17
&& rsvData[i+3]==1){
result=rsvData[i+3+sNum]; //sNumで指定したサーボ番号の結果値
break;
}
}
return result;
}
ここでは、送られてきたデータの中からセンサー情報を抽出し、表示させています。
受信データは、センサーボードから送られてくるデータだけだと思っていましたが、ログを見ると、センサーデータの前に0x00の値が数十個入ることが多いことがわかったので、とりあえず読み取り可能なデータを全て受け取り、その中からセンサーボードのヘッダ情報
[255] [120] [17] [1] の列を確認し、そこからセンサーデータの位置を判別しています。
これで、ソケット通信を使ってWiMaster経由でサーボやセンサーを動かすことができます。ロボット神経システムではDCモータコントローラなど、いろいろな機能のボードがありますが、それらも同じように動かすことができます。
このサンプルプログラムでは、簡単に説明するために、本来は入れるべきエラー処理や非同期処理などを行っていません。実際に使う場合はこれらを参考にして実用的なプログラムを作ってください。
以上
|