[3/29_C-Day有什麼?]系列之七:leJOS Wifi 遙控多台樂高EV3 機器人

手機遙控樂高機器人在以往 NXT 的時代,只能用藍牙點對點,手機端是 master,樂高機器人則是 slave。手機端能連多少台藍牙裝置是根據胎手機而定,不過一般來說兩台是沒問題的。今天如果改用 Wifi 的話就不會受限於Android 手機的藍芽配對裝置數量了。本文說明如何透過 Wifi 來控制兩台樂高 EV3 機器人,同步或非同步控制都可以,請看以下影片。

延伸閱讀:

[leJOS] 準備開機用 SD記憶卡 – 用 Java 程式控制樂高EV3機器人

這是使用 App Inventor 來控制兩台樂高 NXT 機器人:

接著來看程式碼,這是 Android 端的src,重點在於建立兩個 socket 來對兩台機器人發送指令(line 179,205):

Wifi 遙控,Android 端程式
package com.cavedu.ev3_socketremote;


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;

public class MainActivity extends ActionBarActivity implements OnClickListener {

//Device 1
Socket clientSocket1;
BufferedReader inbound;
PrintWriter outbound;

String sendStr = "";

Button Connect, FWD, BACK, LEFT, RIGHT, STOP;
EditText deviceIP, devicePort;

//Device 2
Socket clientSocket2;
BufferedReader inbound2;
PrintWriter outbound2;

String sendStr2 = "";

Button Connect2, FWD2, BACK2, LEFT2, RIGHT2, STOP2;
EditText deviceIP2, devicePort2;

//初始化
void init(){
FWD = (Button) findViewById(R.id.FWD);
BACK = (Button) findViewById(R.id.BACK);
RIGHT = (Button) findViewById(R.id.RIGHT);
LEFT = (Button) findViewById(R.id.LEFT);
STOP = (Button) findViewById(R.id.STOP);

FWD.setOnClickListener(this);
BACK.setOnClickListener(this);
LEFT.setOnClickListener(this);
RIGHT.setOnClickListener(this);
STOP.setOnClickListener(this);

deviceIP = (EditText) findViewById(R.id.device1_ip);
devicePort = (EditText) findViewById(R.id.device1_port);
Connect = (Button) findViewById(R.id.Connect);

FWD2 = (Button) findViewById(R.id.FWD2);
BACK2 = (Button) findViewById(R.id.BACK2);
RIGHT2 = (Button) findViewById(R.id.RIGHT2);
LEFT2 = (Button) findViewById(R.id.LEFT2);
STOP2 = (Button) findViewById(R.id.STOP2);

FWD2.setOnClickListener(this);
BACK2.setOnClickListener(this);
LEFT2.setOnClickListener(this);
RIGHT2.setOnClickListener(this);
STOP2.setOnClickListener(this);

deviceIP2 = (EditText) findViewById(R.id.device2_ip);
devicePort2 = (EditText) findViewById(R.id.device2_port);
Connect2 = (Button) findViewById(R.id.Connect2);
}

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

init(); //初始化

//設定前進、後退、左轉、右轉按鈕Enabled為Flase 在未連線時無法點選
setDeviceButton(false);
setDevice2Button(false);

//按下Connect使Device1連線
Connect.setOnClickListener(new OnClickListener() {
public void onClick(View v) {

//Device 1 Socket Client連線
if (Connect.getText().equals("Connect")) {

//建立Socket連線
Thread t1 = new Thread(runnable);
t1.start();

}

//Device 1 Socket Client斷線
else if (clientSocket1.isConnected()) {
try {
Log.w("AndyDebug", "ClientSocket1 Close");
clientSocket1.close();
Connect.setText("Connect");
setDeviceButton(false);
} catch (IOException e) {
e.printStackTrace();
}
}
}
});

//按下Connect使Device2連線
Connect2.setOnClickListener(new OnClickListener() {
public void onClick(View v) {

//Device 1 Socket Client連線
if (Connect2.getText().equals("Connect")) {
Thread t1 = new Thread(divice2runnable);
t1.start();
}

//Device 2 Socket Client斷線
else if (clientSocket2.isConnected()) {
try {
Log.w("AndyDebug", "ClientSocket1 Close");
clientSocket2.close();
Connect2.setText("Connect");
setDevice2Button(false);
} catch (IOException e) {
e.printStackTrace();
}
}
}
});

}

//Device 1 建立Socket連線
Runnable runnable = new Runnable() {
public void run() {
try {

String ip = deviceIP.getText().toString();
int port = Integer.parseInt(devicePort.getText().toString());

clientSocket1 = new Socket(ip, port); //建立Socket連線

if (clientSocket1.isConnected()) {
runOnUiThread(new Runnable() {
public void run() {
Connect.setText("Close");
setDeviceButton(true);
}
});
}

Log.d("AndyDebug", "is?" + clientSocket1.isConnected());

//Socket 接收
inbound = new BufferedReader(new InputStreamReader(
clientSocket1.getInputStream()));

//Socket 發送
outbound = new PrintWriter(clientSocket1.getOutputStream(),
true);

} catch (IOException ioe) {
System.err.println("IOException: " + ioe);
runOnUiThread(new Runnable() {
public void run() {
Connect.setText("Connect");
setDeviceButton(false);
}
});
}
}
};

//Device 1 透過Socket收發資料
Runnable runnable2 = new Runnable() {
public void run() {
try {

String sss = sendStr + "\r\n";

char send[] = sss.toCharArray();

Log.d("AndyDebug", "送出: " + sss);

outbound.println(send); //送出資料

String inputLine = "";

//接收資料
while ((inputLine = inbound.readLine()) == null) {
}
Log.d("AndyDebug", "收到:" + inputLine);

} catch (Exception e) {
System.err.println("IOException: " + e);
}
}
};

//Device 2 建立Socket連線
Runnable divice2runnable = new Runnable() {
public void run() {
try {

String ip = deviceIP2.getText().toString();
int port = Integer.parseInt(devicePort2.getText().toString());

clientSocket2 = new Socket(ip, port); //建立Socket連線

if (clientSocket2.isConnected()) {
runOnUiThread(new Runnable() {
public void run() {
Connect2.setText("Close");
setDevice2Button(true);
}
});
}

Log.d("AndyDebug", "clientSocket2"+clientSocket2.toString());

//Socket 接收
inbound2 = new BufferedReader(new InputStreamReader(
clientSocket2.getInputStream()));

//Socket 發送
outbound2 = new PrintWriter(clientSocket2.getOutputStream(),
true);

} catch (IOException ioe) {
System.err.println("IOException: " + ioe);
runOnUiThread(new Runnable() {
public void run() {
Connect2.setText("Connect");
setDevice2Button(false);
}
});
}
}
};

//Device 2 透過Socket收發資料
Runnable device2runnable2 = new Runnable() {
public void run() {
try {

String sss = sendStr2 + "\r\n";
Log.e("AndyDebug", sendStr2);
char send[] = sss.toCharArray();

Log.d("AndyDebug", "送出: " + sss);

outbound2.println(send); //送出資料

String inputLine = "";

//接收資料
while ((inputLine = inbound2.readLine()) == null) {
}
Log.d("AndyDebug", "收到:" + inputLine);

} catch (Exception e) {
System.err.println("IOException: " + e);
}
}
};

@Override
public void onClick(View v) {
switch (v.getId()) {

case R.id.FWD:
sendStr = "FWD";
sendCommand();
break;

case R.id.BACK:
sendStr = "BACK";
sendCommand();
break;

case R.id.LEFT:
sendStr = "LEFT";
sendCommand();
break;

case R.id.RIGHT:
sendStr = "RIGHT";
sendCommand();
break;

case R.id.STOP:
sendStr = "STOP";
sendCommand();
break;
case R.id.FWD2:
sendStr2 = "FWD";
sendCommand2();
break;

case R.id.BACK2:
sendStr2 = "BACK";
sendCommand2();
break;

case R.id.LEFT2:
sendStr2 = "LEFT";
sendCommand2();
break;

case R.id.RIGHT2:
sendStr2 = "RIGHT";
sendCommand2();
break;

case R.id.STOP2:
sendStr2 = "STOP";
sendCommand2();
break;
default:
break;
}
}

private void sendCommand() {
Thread t2 = new Thread(runnable2);
t2.start();
}
private void sendCommand2() {
Thread t2 = new Thread(device2runnable2);
t2.start();
}

private void setDeviceButton(boolean b) {
FWD.setEnabled(b);
BACK.setEnabled(b);
LEFT.setEnabled(b);
RIGHT.setEnabled(b);
STOP.setEnabled(b);
}

private void setDevice2Button(boolean b) {
FWD2.setEnabled(b);
BACK2.setEnabled(b);
LEFT2.setEnabled(b);
RIGHT2.setEnabled(b);
STOP2.setEnabled(b);
}
}

樂高機器人端的程式,使用 leJOS 編寫。一樣是去判斷 socket 收到的內容之後執行對應的動作即可:

Wifi 遙控,機器人端程式
import java.io.BufferedReader;

import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

import lejos.hardware.Button;
import lejos.hardware.Key;
import lejos.hardware.KeyListener;
import lejos.hardware.lcd.LCD;
import lejos.hardware.motor.Motor;

class LeJOS_WiFi {

static int angle = 270; // 馬達轉動的角度

private static boolean OutServer = false;
private static ServerSocket server;
private final static int ServerPort = 1234; // 要監控的port

public static void main(String[] args) {

//按下ESCAPE鍵離開程式
Button.ESCAPE.addKeyListener(new KeyListener() {
public void keyReleased(Key k) {
System.exit(0);
}
public void keyPressed(Key k) {}
});

print("Start");

Thread socketServer_thread = new Thread(socketServer_runnable);
socketServer_thread.start();

while (true);
}

//建立SocketServer
public static void SocketServer() {
try {
server = new ServerSocket(ServerPort);

} catch (java.io.IOException e) {
print("Socket Error!");
print("IOException :" + e.toString());
}
}

static Runnable socketServer_runnable = new Runnable() {
public void run() {
SocketServer();

Socket socket;

print("Socket Server OK!");

while (!OutServer) {
socket = null;
try {
synchronized (server) {
socket = server.accept();
}
print(socket.getInetAddress().toString()); //印出目前連線設備的ip

//Socket接收
BufferedReader inbound = new BufferedReader(
new InputStreamReader(socket.getInputStream()));

//Socket發送
PrintWriter outbound = new PrintWriter(
socket.getOutputStream(), true);

while (true) {
String data = "";

outbound.println("Success");

while ((data = inbound.readLine()) == null)
;

if (data.length()>=3)
print("getData:" + data);

if (data.equals("FWD")) {
go();
outbound.print(data + " OK");
} else if (data.equals("BACK")) {
back();
outbound.print(data + " OK");
} else if (data.equals("LEFT")) {
left(angle);
outbound.print(data + " OK");
} else if (data.equals("RIGHT")) {
right(angle);
outbound.print(data + " OK");
} else if (data.equals("STOP")) {
stop();
outbound.print(data + " OK");
}
}

} catch (java.io.IOException e) {
print("Socket Error");
print("IOException :" + e.toString());
Thread socketServer_thread = new Thread(socketServer_runnable);
socketServer_thread.start();
}
}
}
};

//前進
private static void go() {
Motor.A.forward();
Motor.B.forward();
}

//後退
private static void back() {
Motor.A.backward();
Motor.B.backward();
}

//左轉
private static void left(int angle) {
Motor.A.rotate(-angle, true);
Motor.B.rotate(angle, true);
}

//右轉
private static void right(int angle) {
Motor.A.rotate(angle, true);
Motor.B.rotate(-angle, true);
}

//停止
private static void stop() {
Motor.A.stop();
Motor.B.stop();
}

//在EV3螢幕印出字串
private static void print(String str) {
LCD.clear();
LCD.drawString(str, 0, 3);
}

}

 

3 thoughts on “[3/29_C-Day有什麼?]系列之七:leJOS Wifi 遙控多台樂高EV3 機器人

  1. 舜盈 says:

    您好,我參照了您的程式語言
    運行在手機端和EV3上
    可是利用手機連線時日誌檔出現了如下通知
    IOException: java.net.SocketException: socket failed: EACCES (Permission denied)
    可以幫助我理解這些問題嗎

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *