ブログの説明
パソコンやアンドロイドなどの情報を収集しまとめているブログです。

記事内に張られている画像はクリックすると見やすいサイズの画像が表示されるようになっています。
上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。
アクセスカウンター
検索フォーム
カレンダー
09 | 2017/10 | 11
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31 - - - -

スポンサーサイト

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

ルンバのROIを使っての制御Ⅱ(Arduinoソフトウェア編)

関連記事
ルンバのROIを使っての制御でArduinoでコントロールしてるところを紹介しましたが、今回はルンバ制御用のArduinoプログラムの紹介です。


【現在までに実装している機能】
現在までに実装している機能以下の通り
速度指定前進
速度指定後退
その場での(左右)
コーナリング旋回(左右)
オドメトリ
掃除機構のオンオフ(回転数は100%)
バッテリ容量とバッテリ残量の取得
【オドメトリ】
オドメトリはArduinoの電源投入後から常に計算しています。
ですが、オドメトリをPCに送信されると困る場合もあるため、計算したオドメトリをPCに送信するかしないか選べるようになっています。
また、オドメトリの座標はリセットできるようにしました。
オドメトリの計算に使用しているパラメータはルンバ770のものにしています。
ルンバの個体やシリーズの違いによってパラメータが違う可能性があるので、都合に合わせて変更してください。


【PCからのコマンド】

行動パターン 行動コード(1Byte) 指定子1 指定子2 合計容量[Byte]
停止 s 1
前進 w 回転速度(4Byte)
-500~+500
一文字目は必ず+が0 -が1とする
5
後退 x 回転速度(4Byte)
-500~+500
一文字目は必ず+が0 -が1とする
5
その場右回転 d 回転速度(4Byte)
-500~+500
一文字目は必ず+が0 -が1とする
5
その場左回転 a 回転速度(4Byte)
-500~+500
一文字目は必ず+が0 -が1とする
5
右旋回 e 回転速度(4Byte)
-500~+500
一文字目は必ず+が0 -が1とする
旋回半径(5Byte)
0~2000
一文字目は必ず+が0 -が1とする
10
左旋回 q 回転速度(4Byte)
-500~+500
一文字目は必ず+が0 -が1とする
旋回半径(5Byte)
0~2000
一文字目は必ず+が0 -が1とする
10
エンコーダのカウンタ値(右車輪) c 1
エンコーダのカウンタ値(左車輪) C 1
ブラシ動作開始 b 1
ブラシ動作終了 B 1
バッテリ容量(現在) p 1
バッテリ容量(最大値) P 1

【ソースコード】
似たような関数がたくさんあったりして、まだまだコンパクトにできそうですが、とりあえずそのまま載せておきます
#include <math.h>
//無線シールドのピン配置になっているのでdefineコメントにしては残しておく
//#define TX_PIN 12
//#define RX_PIN 13
#define RoombaSerial Serial1

static float g_odometry_x = 0;   //オドメトリのX座標
static float g_odometry_y = 0;   //オドメトリのY座標
static float g_odometry_Angl = 0;//オドメトリの角度

//オドメトリの更新時間
static const int UPDATE_TIME_MSEC = 50;

//********************定数を定義********************
static const float ENCODER_MM_PAR_PULSE = 0.448;
//static const int   L = 250;//車輪間距離(中央-中央):235 (外-外):250
static const float   L = 238.0952;//車輪間距離(中央-中央):235 (外-外):250  770は238.0952らしい

static const int RIGHT = 0;
static const int LEFT = 1;
//バッテリなどで使用
static const int CURRENT = 0;
static const int MAX = 1;

//前回のエンコーダー値
static int g_beforEncoderR;
static int g_beforEncoderL;
int odometryInit()
{
	g_beforEncoderR = encoderOder(0);//右エンコーダー初期化
	g_beforEncoderL = encoderOder(1);//左エンコーダー初期化
}


void setup()
{
	RoombaSerial.begin(115200);
	Serial.begin(9600);

	// Start
	Serial.println("Start");
	RoombaSerial.write((byte)128);   // Start -&gt; Passive Mode
	delay(50);
	RoombaSerial.write((byte)130);   // Safe Mode
	delay(50);
	// Passive Mode
	//RoombaSerial.write((byte)128);   // Start -&gt; Passive Mode

	odometryInit();
}

//戻り値はエンコーダーの値
//引数:0は右車輪 1は左車輪
int encoderOder(int encoderNum)
{
	int bufSize;//バッファに貯まっているサイズ
	int temp;//シリアル通信できたもの
	unsigned int count;//エンコーダのカウンタ
	bufSize = RoombaSerial.available();

	RoombaSerial.write((byte)142);  
	if( encoderNum == RIGHT ) RoombaSerial.write((byte)43);   //右モーターの番号
	else                      RoombaSerial.write((byte)44);   //左モーターの番号

	while( RoombaSerial.available() < 2 )//2byte来るまで無限ループ
	{ }

	if( RoombaSerial.available() >= 2 )//
	{
		//unsigned char temp;
		temp = RoombaSerial.read();
		count = temp * 256;
		//Serial.print( temp );
		//Serial.print( " , " );
		temp = RoombaSerial.read();
		count += temp;
		//Serial.print( temp );
		//Serial.print( " , " );
		//Serial.print( count );
		//Serial.print( "\n" );
	}
	return count;

}


//戻り値は更新時間
unsigned long odometry()
{
	//変化量
	float dX , //⊿X
		dY ,//⊿Y
		dAngle ,//⊿Y
		dL ,//左車輪の移動量
		dR;//右車輪の移動量

	int tempR,tempL , counterR , counterL;
	tempR = encoderOder(0);//右エンコーダー
	tempL = encoderOder(1);//左エンコーダー
	unsigned long updateTime = millis();

	//前回の計測からカウンタが一瞬で半分以上(32768)変化したらオーバーフローしている
	if(      tempR - g_beforEncoderR < -32768 ) counterR = ((long)tempR + (long)65536 - (long)g_beforEncoderR); //オーバーフローした場合
	else if( tempR - g_beforEncoderR >  32768 ) counterR = ((long)tempR - (long)65536 - (long)g_beforEncoderR); //マイナスになった場合
	else  counterR = tempR - g_beforEncoderR;

	if(      tempL - g_beforEncoderL < -32768 ) counterL = ((long)tempL + (long)65536 - (long)g_beforEncoderL); //オーバーフローした場合
	else if( tempL - g_beforEncoderL >  32768 ) counterL = ((long)tempL - (long)65536 - (long)g_beforEncoderL); //マイナスになった場合
	else  counterL = tempL - g_beforEncoderL;

	//  Serial.print(tempL - g_beforEncoderL);
	//  Serial.print(" , ");
	//  Serial.print(tempR - g_beforEncoderR);
	//  Serial.print("\n");
	g_beforEncoderR = tempR;//右エンコーダーの情報を更新
	g_beforEncoderL = tempL;//左エンコーダーの情報を更新

	dR = counterR * ENCODER_MM_PAR_PULSE;
	dL = counterL * ENCODER_MM_PAR_PULSE;

	dAngle = (double)(dL-dR)/L;
	g_odometry_Angl += dAngle/M_PI*180.0;


	dX = ( dL + dR )/2.0 * cos( (g_odometry_Angl/180)*M_PI );//⊿X
	dY = ( dL + dR )/2.0 * sin( (g_odometry_Angl/180)*M_PI );//⊿Y

	g_odometry_x += dX;
	g_odometry_y += dY;
	return updateTime;
}


void loop()
{
	int command;
	static boolean odometryMode = false;
	if( Serial.available() > 0 )//データが来ていたら
	{
		static int speedNum = 100;//速度
		int radiusNum = 0;
		command = Serial.read();
		//速度指定
		if( '0' <= command && command <= '9' )
		{
			speedNum = command - '0';
			//スピード番号が1~9は10%~90%に相当する
			//0は100%に相当する
			if( speedNum == 0 )
			{
				speedNum = 10;
			}

			speedNum = 500 * speedNum/10;
		}
		else
		{
			switch( command )
			{
			case 's':stopRoomba();           break;
			//前進
			case 'w':
				speedNum = serialReadSpeed();//速度を受信する
				//Serial.print("速度:");
				//Serial.print(speedNum);
				//Serial.print("\n");
				foward( speedNum );     break;
			//後退
			case 'x':
				speedNum = serialReadSpeed();//速度を受信する
				backward(speedNum);     break;
			//右ターン
			case 'd':
				speedNum = serialReadSpeed();//速度を受信する
				turnRight( speedNum );  break;
			//左ターン
			case 'a':
				speedNum = serialReadSpeed();//速度を受信する
				turnLeft( speedNum );   break;
			//左コーナリング
			case 'q':
				speedNum = serialReadSpeed();//速度を受信する
				radiusNum = serialReadRadius();//旋回半径を設定
				corneringLeft( speedNum , 100 );break;
			//右コーナリング
			case 'e':
				speedNum = serialReadSpeed();//速度を受信する
				radiusNum = serialReadRadius();//旋回半径を設定
				corneringRight( speedNum , 100 );break;


			case 'b':motorsPWMControl( true );  break;
			case 'B':motorsPWMControl( false ); break;


			case 'c':Serial.print( encoderOder(RIGHT) );
				Serial.print( "\n" );
				break;
			case 'C':Serial.print( encoderOder(LEFT) );
				Serial.print( "\n" );
				break;
			

			//バッテリ容量
			case 'p':Serial.print( getBattery( CURRENT ) );
					 Serial.print( "\n" );
				     break;
			case 'P':Serial.print( getBattery( MAX ) );
					 Serial.print( "\n" );
				     break;

			case 'o'://小文字oならオドメトリを送り始める
				odometryMode = true;   break;
			case 'O': //大文字Oならオドメトリを送るのをやめる
				odometryMode = false;  break;
			case 'r'://座標リセットコマンド
				g_odometry_x = 0;   //オドメトリのX座標
				g_odometry_y = 0;   //オドメトリのY座標
				g_odometry_Angl = 0;//オドメトリの角度
				break;
			}
		}
	}

	static unsigned long odometryUpdateTime = millis();//時間初期化
	//前回の時間から30msec超えていたら
	if( ( millis() >= (odometryUpdateTime + 30)) )
	{
		odometryUpdateTime = odometry();
		///odometryUpdateTime //= millis();

		//もしオドメトリ表示モードだったら
		if( odometryMode )
		{
			Serial.print( odometryUpdateTime );       Serial.print(",");
			Serial.print( g_odometry_x );   Serial.print(",");
			Serial.print( g_odometry_y );   Serial.print(",");
			Serial.print( g_odometry_Angl );Serial.print("\n");
		} 

	}
}
//*********************以下通信関係**********************
//速度設定
int serialReadSpeed()
{
	int speedNum = 0 //戻り値となる
		, readByte = 0;
	boolean signFlag;//符号がどっち trueならプラス、falseならマイナス
	int temp;

	while( true )
	{
		if(Serial.available() > 0)
		{
			temp = Serial.read();
			//数字かどうかチェック
			//数字だったら処理する
			if(  ('0' <= temp) && (temp <= '9') ) 
			{
				switch( readByte )
				{
				case 0://Serial.print( temp-'0' ); Serial.print("\n");
					   if( (temp - '0') == 0 ) signFlag = true;
					   else signFlag = false;
					   break;
				case 1://Serial.print( temp-'0' ); Serial.print("\n");
					   speedNum += (temp-'0') * 100;
					   break;
				case 2://Serial.print( temp-'0' ); Serial.print("\n");
					   speedNum += (temp-'0') * 10;
					   break;
				case 3://Serial.print( temp-'0' ); Serial.print("\n");
					   speedNum += (temp-'0') ;
					   break;
				}
				readByte++;
			}
		}
		//4文字受信したならばループ終了
		if( readByte >= 4 ) break;
	}
	//負数だったならば
	if( !signFlag ) speedNum *= (-1 );
	//速度上限と下限の設定
	if( speedNum > 500 ) speedNum = 500;
	else if( speedNum < (-500) ) speedNum = -500;

	return speedNum;
}
//旋回半径設定
int serialReadRadius()
{
	int radiusNum = 0 //戻り値となる
		, readByte = 0;
	boolean signFlag;//符号がどっち trueならプラス、falseならマイナス
	int temp;

	while( true )
	{
		if(Serial.available() > 0)
		{
			temp = Serial.read();
			//数字かどうかチェック
			//数字だったら処理する
			if(  ('0' <= temp) && (temp <= '9') ) 
			{
				switch( readByte )
				{
				case 0://Serial.print( temp-'0' ); Serial.print("\n");
					   if( (temp - '0') == 0 ) signFlag = true;
					   else signFlag = false;
					   break;
				case 1://Serial.print( temp-'0' ); Serial.print("\n");
					   radiusNum += (temp-'0') * 1000;
					   break;
				case 2://Serial.print( temp-'0' ); Serial.print("\n");
					   radiusNum += (temp-'0') * 100;
					   break;
				case 3://Serial.print( temp-'0' ); Serial.print("\n");
					   radiusNum += (temp-'0') * 10;
					   break;
				case 4://Serial.print( temp-'0' ); Serial.print("\n");
					   radiusNum += (temp-'0') ;
					   break;
				}
				readByte++;
			}
		}
		//4文字受信したならばループ終了
		if( readByte >= 5 ) break;
	}
	//負数だったならば
	if( !signFlag ) radiusNum *= (-1 );
	//速度上限と下限の設定
	if( radiusNum > 2000 ) radiusNum = 500;
	else if( radiusNum < (-2000) ) radiusNum = -500;

	return radiusNum;
}
//*********************以下ルンバコントロール**********************

void motorsPWMControl( boolean mode)
{

	RoombaSerial.write((byte)144);   // Drive

	if( mode )
	{
		RoombaSerial.write((byte)127);  // Velocity: 0x00c8 = 200
		RoombaSerial.write((byte)127);
		RoombaSerial.write((byte)127);  // Radius: 0x8000 = Straight
	}
	else
	{
		RoombaSerial.write((byte)0);  // Velocity: 0x00c8 = 200
		RoombaSerial.write((byte)0);
		RoombaSerial.write((byte)0);  // Radius: 0x8000 = Straight
	}
}

//前進命令
void foward(int speedNum)
{
	// Forward
	//Serial.println("Forward");

	//試しにbyte型配列を使ってみる
	byte sendSpeedData[2];
	sendSpeedData[0] = ( (byte *) ( (void*)(&speedNum) ) )[0];
	sendSpeedData[1] = ( (byte *) ( (void*)(&speedNum) ) )[1];


	RoombaSerial.write((byte)137);   // Drive
	//速度指定
	//配列の後ろから送信する
	RoombaSerial.write(sendSpeedData[1]);   // Speed
	RoombaSerial.write(sendSpeedData[0]);   // Speed
	RoombaSerial.write((byte)0x00);  // Velocity: 0x00c8 = 200
	RoombaSerial.write((byte)0x00);
}


//後退命令
void backward(int speedNum)
{
	speedNum*=(-1);//バックなので速度をマイナスにする
	byte sendSpeedData[2];
	sendSpeedData[0] = ( (byte *) ( (void*)(&speedNum) ) )[0];
	sendSpeedData[1] = ( (byte *) ( (void*)(&speedNum) ) )[1];

	// Backward
	//Serial.println("Backward");
	RoombaSerial.write((byte)137);   // Drive
	//速度指定
	//配列の後ろから送信する
	RoombaSerial.write(sendSpeedData[1]);   // Speed
	RoombaSerial.write(sendSpeedData[0]);   // Speed

	RoombaSerial.write((byte)0x80);  // Radius: 0x8000 = Straight
	RoombaSerial.write((byte)0x00);
}

//左その場回転
void turnLeft(int speedNum)
{
	byte sendSpeedData[2];
	sendSpeedData[0] = ( (byte *) ( (void*)(&speedNum) ) )[0];
	sendSpeedData[1] = ( (byte *) ( (void*)(&speedNum) ) )[1];
	//Serial.println("Turn");
	RoombaSerial.write((byte)137);   // Drive
	//速度指定
	//配列の後ろから送信する
	RoombaSerial.write(sendSpeedData[1]);   // Speed
	RoombaSerial.write(sendSpeedData[0]);   // Speed
	RoombaSerial.write((byte)0x00);  // Radius: 0x0001 = Turn in place counter-clockwise
	RoombaSerial.write((byte)0x01);
}

//右その場回転
void turnRight(int speedNum)
{
	byte sendSpeedData[2];
	sendSpeedData[0] = ( (byte *) ( (void*)(&speedNum) ) )[0];
	sendSpeedData[1] = ( (byte *) ( (void*)(&speedNum) ) )[1];
	// Turn in place clockwise
	//Serial.println("Turn");
	RoombaSerial.write((byte)137);   // Drive
	//速度指定
	//配列の後ろから送信する
	RoombaSerial.write(sendSpeedData[1]);   // Speed
	RoombaSerial.write(sendSpeedData[0]);   // Speed
	RoombaSerial.write((byte)0xff);  // Radius: 0x0001 = Turn in place clockwise
	RoombaSerial.write((byte)0xff);
}
//停止
void stopRoomba()
{
	// Stop
	//Serial.println("Stop");
	RoombaSerial.write((byte)137);   // Drive
	RoombaSerial.write((byte)0x00);
	RoombaSerial.write((byte)0x00);
	RoombaSerial.write((byte)0x00);
	RoombaSerial.write((byte)0x00);
}
//右コーナリング旋回
void corneringRight(int speedNum , int radius)
{
	byte sendSpeedData[2];
	sendSpeedData[0] = ( (byte *) ( (void*)(&speedNum) ) )[0];
	sendSpeedData[1] = ( (byte *) ( (void*)(&speedNum) ) )[1];

	radius *= (-1 );//右旋回は半径が負数
	byte sendRadiusData[2];
	sendRadiusData[0] = ( (byte *) ( (void*)(&radius) ) )[0];
	sendRadiusData[1] = ( (byte *) ( (void*)(&radius) ) )[1];

	// Turn in place clockwise
	//Serial.println("Turn");
	RoombaSerial.write((byte)137);   // Drive
	//速度指定
	//配列の後ろから送信する
	RoombaSerial.write(sendSpeedData[1]);   // Speed
	RoombaSerial.write(sendSpeedData[0]);   // Speed
	RoombaSerial.write(sendRadiusData[1]);   // Speed
	RoombaSerial.write(sendRadiusData[0]);   // Speed
	//RoombaSerial.write((byte)0xff);  // Radius: 0x0001 = Turn in place clockwise
	//RoombaSerial.write((byte)0xff);
}
//左コーナリング旋回
void corneringLeft(int speedNum , int radius)
{
	byte sendSpeedData[2];
	sendSpeedData[0] = ( (byte *) ( (void*)(&speedNum) ) )[0];
	sendSpeedData[1] = ( (byte *) ( (void*)(&speedNum) ) )[1];

	byte sendRadiusData[2];
	sendRadiusData[0] = ( (byte *) ( (void*)(&radius) ) )[0];
	sendRadiusData[1] = ( (byte *) ( (void*)(&radius) ) )[1];

	// Turn in place clockwise
	//Serial.println("Turn");
	RoombaSerial.write((byte)137);   // Drive
	//速度指定
	//配列の後ろから送信する
	RoombaSerial.write(sendSpeedData[1]);   // Speed
	RoombaSerial.write(sendSpeedData[0]);   // Speed
	RoombaSerial.write(sendRadiusData[1]);   // Speed
	RoombaSerial.write(sendRadiusData[0]);   // Speed
}

unsigned int getBattery(int mode)
{
	int bufSize;//バッファに貯まっているサイズ
	int temp;//シリアル通信できたもの
	unsigned int capacity;//バッテリ容量
	bufSize = RoombaSerial.available();

	RoombaSerial.write((byte)142);  
	if( mode == CURRENT  ) RoombaSerial.write((byte)25);   //右モーターの番号
	else                   RoombaSerial.write((byte)26);   //左モーターの番号

	while( RoombaSerial.available() < 2 )//2byte来るまで無限ループ
	{ }

	if( RoombaSerial.available() >= 2 )//
	{
		temp = RoombaSerial.read();  capacity = temp * 256;
		temp = RoombaSerial.read();  capacity += temp;
	}

	return capacity;
}

COMMENT

No title

このサンプルを実行すると,'Serial1'was not declared in this scopeとでて検証できないのですが,何か特別なライブラリでも使用しているのでしょうか?
環境はVista32bit,Arduino1.0.4です

Re: No title

> このサンプルを実行すると,'Serial1'was not declared in this scopeとでて検証できないのですが,何か特別なライブラリでも使用しているのでしょうか?
> 環境はVista32bit,Arduino1.0.4です

使用しているマイコンボードはArduino Unoではないですか?
このサンプルはArduino Mega用にコーディングしました。
Unoにはハードウェアでサポートされているシリアル通信が1系統しかないので、このサンプルを動かすにはソフトウェアシリアルとかを使う必要があると思います。
RoombaSerialをdefineしてあるのはその辺を簡単に切り替えられるようにする為です。

No title

このプログラムで,62,63行目のところなんですが,RoombaSerial.writeでOpcodeを送信しているのは理解できましたが,エンコーダの値の取得ができないため,センサ値を取得する際のPacketIDの送信に関しても,Opcodeの送信と同様の書き方で送信できるのでしょうか?

Re: No title

> このプログラムで,62,63行目のところなんですが,RoombaSerial.writeでOpcodeを送信しているのは理解できましたが,エンコーダの値の取得ができないため,センサ値を取得する際のPacketIDの送信に関しても,Opcodeの送信と同様の書き方で送信できるのでしょうか?

encoderOder関数でエンコーダ値を取得できませんか?
他のセンサ値についても同じような記述で取得できますよ。
ただし、ルンバは15msecのサイクルで処理しているため、データを1個取得するのに15msecかかります。
まとめて取り出したいときはQuery Listを使った方が早いですよ。

No title

昨日は回答して頂きありがとうございました.
センサ値の取得ができない原因が,68行目や71行目の部分の
RoombaSerial.available()やRoombaSerial.read()のところで
Roombaからの通信が来ていないことがわかりました.
現在,キーボード操作により前進や後退などの操作ができているため,こちらからの命令はRoombaに届いていると考えています.
Roombaからの応答がない問題について何か解決方法はないでしょうか?

Re: No title

> 昨日は回答して頂きありがとうございました.
> センサ値の取得ができない原因が,68行目や71行目の部分の
> RoombaSerial.available()やRoombaSerial.read()のところで
> Roombaからの通信が来ていないことがわかりました.
> 現在,キーボード操作により前進や後退などの操作ができているため,こちらからの命令はRoombaに届いていると考えています.
> Roombaからの応答がない問題について何か解決方法はないでしょうか?

Roombaは正しい命令が届いている場合必ず応答してきます。
応答がない場合命令がキチンと送られていないことが考えられます。
確かめる手段として、ルンバに送っている命令を他のシリアル通信インターフェースを使ってPCで表示してみるのはどうですか?

No title

こんにちは。ルンバの自己位置推定の参考にさせていただいております。

このプログラムを使うと、ルンバが動いていなくてもx,yの座標が徐々にずれていってしまいます。
調べてみると、動いていないときもエンコーダの値が変化してしまっているようです。

動画をみると、そのようなブレはなく、かなり正確にオドメトリを取得できているように見えます。

何か解決策があれば教えていただけると助かります。

Re: No title

もしかしてソフトウェアシリアルを使っていないですか?
僕が為した限りでは、ソフトウェアシリアルでは正常に通信できませんでした。
僕の予想ではボーレートが高すぎてソフトウェアシリアルでは処理しきれていないのではないかと思います。
なので、僕はハードウェアレベルでシリアル通信を複数系統サポートしているArduino Megaを使って制御してます。

> こんにちは。ルンバの自己位置推定の参考にさせていただいております。
>
> このプログラムを使うと、ルンバが動いていなくてもx,yの座標が徐々にずれていってしまいます。
> 調べてみると、動いていないときもエンコーダの値が変化してしまっているようです。
>
> 動画をみると、そのようなブレはなく、かなり正確にオドメトリを取得できているように見えます。
>
> 何か解決策があれば教えていただけると助かります。

No title

早速のご回答ありがとうございます。

ご指摘との通りUno用にリコードして、ソフトウェアシリアルを使っています。
一度Megaを使って試してみます。

管理人のみ閲覧できます

このコメントは管理人のみ閲覧できます

EDIT COMMENT

非公開コメント

最新トラックバック
ブロとも申請フォーム
QLOOKアクセス解析
上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。