Продолжаем управлять платой ChipKIT WF32 со смартфона Android напрямую через WiFi. В прошлый раз
написали прошивку для управляемого робота - платы ChipKIT WF32. Сейчас сделаем пульт - напишем управляющее приложение для Android.
Управление платой ChipKIT WF32 со смартфона Android через WiFi from
1i7 on
Vimeo.
Управляющий клиент на Android
Т.к. наш пульт на Android после подключения к роботу-плате переходит в активный режим отправки команд, будем называть приложение-пульт управляющим клиентом (англ. Master - хозяин), а управляюемого робота-плату - подчинённым сервером (в английских терминах Slave - раб).
Создаём новый проект приложения для Android:
chipkit-server-wifi/AndroidTcpClientMaster.
В манифесте сразу разрешить приложению выходить в интернет:
RobotCarPult/AndroidManifest.xml xmlns:android="
http://schemas.android.com/apk/res/android"
package="edu.nntu.robotpult"
android:versionCode="1"
android:versionName="1.0" >
...
android:name="android.permission.INTERNET"/>
...
Весь рабочий код в одном классе экрана
AndroidTcpClientMaster/src/edu/nntu/robotpult/RobotPultActivity.java package edu.nntu.robotpult;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
public class RobotPultActivity extends Activity {
Константы для протокола:
public static final String CMD_LEDON = "ledon";
public static final String CMD_LEDOFF = "ledoff";
public static final String CMD_PING = "ping";
Ip-адрес робота-платы и порт:
public static final String DEFAULT_SERVER_HOST = "192.168.43.117";
public static final int DEFAULT_SERVER_PORT = 44114;
Максимальное время ожидания ответа от сервера - если ответ на команду не получен за 5 секунд, считаем, что связь разорвана:
/**
* Таймаут для чтения ответа на команды - сервер должет прислать ответ на
* запрос за 5 секунд, иначе он будет считаться отключенным.
*/
private static final int SERVER_SO_TIMEOUT = 5000;
Максимальное время бездействия пользователя (5 секунд), по истечении которого пульт отправляет роботу команду ping, чтобы тот со своей стороны не разорвал соединение.
/**
* Максимальное время неактивности пользователя, если пользователь не
* отправлял команды на сервер роботу 5 секунд, приложение само отправит
* команду ping, чтобы держать подключение открытым.
*/
private final long MAX_IDLE_TIMEOUT = 5000;
Определим состояния приложения в смысле подключения к роботу-плате: отключено, подключается, подключено, ошибка подключения.
private enum ConnectionStatus {
DISCONNECTED, CONNECTING, CONNECTED, ERROR
}
Всякие переменные: сокет и потоки ввода-вывода, статус подключения, информация о подключении и сообщение последней ошибки.
private Socket socket;
private OutputStream serverOut;
private InputStream serverIn;
private ConnectionStatus connectionStatus = ConnectionStatus.DISCONNECTED;
private String connectionInfo;
private String connectionErrorMessage;
Обратные вызовы (кол-бэки) для получения оповещений о статусе выполнении команд:
/**
* Обратный вызов для получения результата выполения команды, добавленной в
* очередь на выполнение в фоновом потоке.
*/
private interface CommandListener {
/**
* Команды была выполнена на устройстве, получен ответ.
*
* @param cmd
* выполненная команда
* @param reply
* ответ от устройства
*/
void onCommandExecuted(final String cmd, final String reply);
}
Команды на сервер будем отправлять одну за одной из фонового потока. В очереди может быть только одна команда; до тех пор, пока она не выполнена, другие команды не добавляются. Для помещения команды в очередь нужно устновить значение переменной nextCommand, для получения сообщений о ходе выполнения команды установить значение nextCommandListener.
/**
* "Очередь" команд для выполнения на сервере, состоящая из одного элемента.
*/
private String nextCommand;
private CommandListener nextCommandListener;
Подключение к роботу в методе connectToServer:
- Передаем имя хоста и порт;
- Все сетевые операции по определению считаются долгими, поэтому обязательно выполняются в фоновом потоке, чтобы не блокировать интерфейс (в старших версиях Андроид система сама запрещает делать сетевые вызовы из потока интерфейса, выбрасывая исключение в том случае, если например поместить открытие сокета в обработчик нажатия кнопки без создания фонового потока);
- Создаём сокет, открываем потоки ввода-вывода;
- Запускаем процедуру отправки команд на робота-плату startServerOutpuWriter().
/**
* Подлключиться к серверу и запустить процесс чтения данных.
*/
private void connectToServer(final String serverHost, final int serverPort) {
// Все сетевые операции нужно делать в фоновом потоке, чтобы не
// блокировать интерфейс
new Thread() {
@Override
public void run() {
try {
debug("Connecting to server: " + serverHost + ":"
+ serverPort + "...");
setConnectionStatus(ConnectionStatus.CONNECTING);
socket = new Socket(serverHost, serverPort);
// Подключились к серверу:
// Установим таймаут для чтения ответа на команды -
// сервер должет прислать ответ за 5 секунд, иначе он будет
// считаться отключенным (в нашем случае это позволит
// предотвратить вероятные зависания на блокирующем read,
// когда например робот отключился до того, как прислал
// ответ и сокет не распрознал это как разрыв связи с
// выбросом IOException)
socket.setSoTimeout(SERVER_SO_TIMEOUT);
// Получаем доступ к потокам ввода/вывода сокета для общения
// с сервером (роботом)
serverOut = socket.getOutputStream();
serverIn = socket.getInputStream();
debug("Connected");
// TODO: разобраться, почему на этой строке может подвисать
connectionInfo = socket.getInetAddress().getHostName()
+ ":" + socket.getPort();
debug("Connection info: " + connectionInfo);
setConnectionStatus(ConnectionStatus.CONNECTED);
// Подключились к серверу, теперь можно отправлять команды
startServerOutputWriter();
} catch (final Exception e) {
socket = null;
serverOut = null;
serverIn = null;
debug("Error connecting to server: " + e.getMessage());
setConnectionStatus(ConnectionStatus.ERROR);
connectionErrorMessage = e.getMessage();
e.printStackTrace();
}
}
}.start();
}
Фоновый поток отправки команд роботу. Берем команду из переменной nextCommand, отправляем на сервер, получаем ответ, отчитываемся о выполнении с nextCommandListener.onCommandExcecuted, разрешаем установить значение новой команды в nextCommand.
Здесь же автоматически добавляем к отправке команду ping, если пользователь сам ничего не нажимал продолжительное время (MAX_IDLE_TIMEOUT) 5 секунд.
/**
* Фоновый поток отправки данных на сервер: получаем команду от пользователя
* (в переменной nextCommand), отправляем на сервер, ждем ответ, получаем
* ответ, сообщаем о результате, ждем следующую команду от пользователя.
*/
private void startServerOutputWriter() {
new Thread() {
@Override
public void run() {
try {
long lastCmdTime = System.currentTimeMillis();
while (true) {
String execCommand;
if (nextCommand != null) {
execCommand = nextCommand;
} else if (System.currentTimeMillis() - lastCmdTime > MAX_IDLE_TIMEOUT) {
execCommand = CMD_PING;
} else {
execCommand = null;
}
if (execCommand != null) {
// отправить команду на сервер
debug("Write: " + execCommand);
serverOut.write((execCommand).getBytes());
serverOut.flush();
// и сразу прочитать ответ
final byte[] readBuffer = new byte[256];
final int readSize = serverIn.read(readBuffer);
if (readSize != -1) {
final String reply = new String(readBuffer, 0,
readSize);
debug("Read: " + "num bytes=" + readSize
+ ", value=" + reply);
if (nextCommandListener != null) {
nextCommandListener.onCommandExecuted(
execCommand, reply);
}
} else {
throw new IOException("End of stream");
}
// очистим "очередь" - можно добавлять следующую
// команду.
nextCommand = null;
nextCommandListener = null;
lastCmdTime = System.currentTimeMillis();
} else {
// на всякий случай - не будем напрягать систему
// холостыми циклами
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
}
} catch (final Exception e) {
debug("Connection error: " + e.getMessage());
e.printStackTrace();
}
debug("Server output writer thread finish");
disconnectFromServer();
}
}.start();
}
Добавить команду в очередь на выполенние. Новую команду нельзя добавить пока очередь переполнена (у нас очередь состоит из одного элемента, поэтому пока не выполнена текущая команда).
/**
* Поставить комнаду в очередь для выполнения на сервере. При переполнении
* очереди новые команды игнорируются. (в простой реализации в очереди может
* быть всего один элемент).
*
* @param cmd
* @param cmdListener
* @return true, если команда успешно добавлена в очередь false, если
* очередь переполнена и команда не может быть добавлена.
*/
private boolean sendCommand(final String cmd,
final CommandListener cmdListener) {
if (nextCommand == null) {
nextCommand = cmd;
this.nextCommandListener = cmdListener;
return true;
} else {
return false;
}
}
Установить статус подключения - задать статус и обновить интерфейс:
private void setConnectionStatus(final ConnectionStatus status) {
System.out.println("setConnectionStatus: " + status);
this.connectionStatus = status;
handler.post(new Runnable() {
@Override
public void run() {
updateViews();
}
});
}
Обновим состояние интерфейса в зависимости от текущего статуса подключения (например, если мы отключены, появляется кнопка "Подключиться", если подключены, то она пропадает):
/**
* Обновить элементы управления в зависимости от текущего состояния.
*/
private void updateViews() {
switch (connectionStatus) {
case DISCONNECTED:
txtStatus.setText(R.string.status_disconnected);
btnConnect.setVisibility(View.VISIBLE);
btnConnect.setEnabled(true);
break;
case CONNECTED:
txtStatus.setText(getString(R.string.status_connected) + ": "
+ connectionInfo);
btnConnect.setVisibility(View.GONE);
btnConnect.setEnabled(false);
break;
case CONNECTING:
txtStatus.setText(R.string.status_connecting);
btnConnect.setVisibility(View.VISIBLE);
btnConnect.setEnabled(false);
break;
case ERROR:
txtStatus.setText(getString(R.string.status_error) + ": "
+ connectionErrorMessage);
btnConnect.setVisibility(View.VISIBLE);
btnConnect.setEnabled(true);
break;
default:
break;
}
if (ConnectionStatus.CONNECTED.equals(connectionStatus)) {
btnCmdLedOn.setEnabled(true);
btnCmdLedOff.setEnabled(true);
} else {
btnCmdLedOn.setEnabled(false);
btnCmdLedOff.setEnabled(false);
}
}
Отключиться от робота - закрыть все потоки и сокет, почистить переменные:
/**
* Отключиться от сервера - закрыть все потоки и сокет, обнулить переменные.
*/
private void disconnectFromServer() {
try {
if (serverIn != null) {
serverIn.close();
}
if (serverOut != null) {
serverOut.close();
}
if (socket != null) {
socket.close();
}
} catch (final IOException e) {
e.printStackTrace();
} finally {
serverIn = null;
serverOut = null;
socket = null;
// очистить "очередь" команд
nextCommand = null;
nextCommandListener = null;
debug("Disconnected");
setConnectionStatus(ConnectionStatus.DISCONNECTED);
}
}
Подключаемся к роботу, когда экран приложения выходит на первый план:
@Override
protected void onResume() {
super.onResume();
connectToServer(DEFAULT_SERVER_HOST, DEFAULT_SERVER_PORT);
}
Отключаемся, когда экран уходит с переднего плана (сворачивается или скрыт другим приложением):
@Override
protected void onPause() {
super.onPause();
disconnectFromServer();
}
Подключаем пульт на Андроид Ётафон к роботу на плате ChipKIT WF32
Загружаем
в плату ChipKIT WF32 прошивку и ждем некоторое время (может быть секунд 15-20), пока она не подключится к точке доступа WiFi (
точку доступа WiFi для платы нужно тоже не забыть включить).
На смартфоне запускаем приложение Пульт для Робота и смотрим, чтобы текущий статус был обозначен как Подключен (если робот не успел подключиться к точке доступа, не был включен или произошла еще какая-то проблема, приложение покажет сообщение об ошибке и кнопку для повторной попытки подключения).
Пульт подключен к роботу, отправляем команды.
Включим лампочку: нажмем кнопку "Включить лампочку", к роботу уйдет команда ledon, светодиод загорится:
Выключим лампочку: нажмем кнопку "Выключить лампочку", к роботу уйдет команда ledoff, светодиод погаснет:
Подождем ничего не нажимая - убедимся, что в области отладочных сообщений каждые 5 секунд начнут появятся сообщения об отправке команды ping:
исходники занятия,
подсветка синтаксиса.