1i7 (1i7) wrote,
1i7
1i7

Category:

Управление платой ChipKIT со смартфона Android через WiFi: робот-плата

Сегодня управляем платой ChipKIT WF32 со смартфона Android напрямую через WiFi без посредников в виде Сервера Роботов или шнура USB.


Управление платой ChipKIT WF32 со смартфона Android через WiFi from 1i7 on Vimeo.


ChipKIT WF32-TcpServer-Android.jpg

В примере ниже традиционно помигаем лампочкой. Но из него очень легко вырастает например пульт для Робота Машинки добавлением нескольких специальных команд в протокол и нескольких новых кнопочек на экран приложения.

В целом код во многом похож на код предыдущего занятия Управление ChipKIT из Сервера Роботов, со своими нюансами.

Исходники
прошивка для платы ChipKIT: chipkit-server-wifi/chipkit_tcp_server_slave
приложение Андроид: chipkit-server-wifi/AndroidTcpClientMaster

Пульт для Робота Машинки: Робот Машинка/прошивки/robot_pult



Предварительные приготовления

Для запуска примера необходимо.

0) Нужна плата из серии ChipKIT, которая умеет работать с WiFi (на аналогичных платах Arduino этот код не заработает): ChipKIT Uno32+WiFi Shield, ChipKIT WF32 или ChipKIT Wi-FIRE.

1) Лабораторная работа: Подключение платы ChipKIT WF32 к точке доступа WiFi.

2) Смартфон или планшет с операционной системой Google Android.

3) Плата и устройство Android должны находиться в одной локальной сети. Рекомендую использовать точку доступа на этом же смартфоне Android: это удобно, т.к. на смартфоне легко настраивать параметры сети, также смартфон и плату будет легко переносить между помещениями. Можно также использовать обычной домашний роутер, но в этом случае нужно убедиться, что к нему подключены и плата и смартфон.

Алгоритм
С точки зрения робота (платы ChipKIT WF32):
Плата подключается к точке доступа WiFi и просит назначить ей статический IP-адрес 192.168.43.117, после этого ожидает подключения пульта на порт 44114. После подключения пульта плата принимает команды - включить или выключить лампочку. Если пульт не отправлял команды более 10ти секунд (параметр настраивается), сессия общения считается оконченной, плата разрывает соединение и ожидает подключения нового пульта.

С точки зрения пульта (смартфона Android):
Смартфон подключается к той же сети WiFi, что и плата (если это внешняя точка доступа, то к ней нужно подключиться в настройках WiFi; если точкой доступа для платы является текущий смартфон, то они уже в одной сети, никуда подключаться не нужно). После этого подключается к роботу по адресу 192.168.43.117 на порт 44114 и ожидает ввода команд от пользователя (кнопки на экране). Если пользователь нажимает кнопку (включить или выключить лампочку), пульт отправляет ее роботу по WiFi и показывает ответ. Если пользователь не отправляет команды роботу более 5ти секунд, приложение автоматически отправляет роботу команду ping для того, чтобы поддерживать сессию в активном состоянии. Если пользователь закрывает приложение, каналы связи разрываются; но даже сеанс связи не был завершен корректно, автоматическая отправка пакетов ping прекратится, сервер закроет соединение через 10 секунд.

Несколько роботов в одной сети WiFi
Важный нюанс - это необходимость назначать статический IP-адрес роботу-плате при подключении к точке доступа (по умолчанию в коде прописан 192.168.43.117). Такой подход сразу вводит неприятное ограничение: мы не можем запустить рядом два одинаковых робота, подключающихся к одной и той же точке доступа WiFi, без перепрошивки одного из них кодом с измененным IP-адресом. Однако статический адрес требуется для того, чтобы пульт на Android заранее знал, где в сети находится робот, которым он хочет управлять (этот адрес также прописан в коде приложения Android).

Мы могли бы легко сделать так, чтобы адрес робота задавался в настройках пульта пользователем, а сам робот-плата использовал бы динамический IP-адрес при подключении к точке доступа WiFi. Это позволит находиться множеству роботов в одной сети, но тогда нам придется сделать так, чтобы робот-плата каким-то образом сообщал значение полученного IP-адреса пользователю (например, выводил его на специальном экране), тк. при каждом запуске робота этот адрес может меняться, а пользователю нужно знать, какой адрес вводить в настройки пульта.

Можно придумать разные решения обозначенной проблемы, но т.к. стандартного оптимального подхода мне среди них не видится, а промежуточные компромиссы сильно усложнят пример, на этот раз смиримся с этим органичением.

Протокол
Протокол общения между роботом (плата ChipKIT WF32) и пультом (смартфон Android Ётафон): пульт отправляет команду, робот выполняет команду и присылает ответ; команды и ответы строковые.

Весь полезный функционал определим двумя основными командами:
ledon - включить лампочку;
ledoff - выключить лампочку.

Ответы от робота:
ok - в случае успеха выполнения команды;
dontunderstand - команда не распознана.

Дополнительная команда:
ping - проверить канал связи; ничего не делает, просто присылает ответ ok.

Комнаду ping можно слать от пульта к роботу при неактивности пользователя для того, чтобы поддерживать сессию связи в открытом состоянии, или проверить работоспособность робота без выполнения активных действий.

Подчинённый сервер на ChipKIT WF32

Т.к. наш робот-плата после запуска Tcp-сервера и подключения к нему Tcp-клиента в виде пульта на Android переходит в пассивный режим ожидания команд от пульта, будем называть робота-плату подчинённым сервером (в английских терминах Slave - раб), а пульт на Android - управляющим клиентом (англ. Master - хозяин).

Код подключения к точке WiFi опущен, за подробностями в отдельную статью.

Весь код примера содержится в одном файле: chipkit_tcp_server_slave.pde


Подключаем необходимые библиотеки:


#include <WiFiShieldOrPmodWiFi_G.h>

#include <DNETcK.h>
#include <DWIFIcK.h>



Константы для протокола:


// Протокол общения с Пультом

// Команды, принимаемые от Пульта
const char* CMD_LEDON = "ledon";
const char* CMD_LEDOFF = "ledoff";
const char* CMD_PING = "ping";

// Ответы для Пульта
const char* REPLY_OK = "ok";
const char* REPLY_DONTUNDERSTAND = "dontunderstand";



Номер ножки тестовой лампочки, которую будем включать и выключать:


// Пин для тестовой лампочки
#define LED_PIN 13



Важный момент - мы просим от точки доступа статический IP-адрес, этот же адрес должен быть прописан в коде пульта Android.


// статический IP-адрес для текущего хоста - попросим у
// точки Wifi (иначе Пульт не узнает, куда подключаться)
//IPv4 host_ip = {192,168,117,117};
IPv4 host_ip = {192,168,43,117};


Порт 44114 для входящих подключений от пульта:


// Порт для tcp-сервера 44114
const int tcp_server_port = DNETcK::iPersonalPorts44 + 114;


Ссылки на запущенный на плате Tcp-сервер и подключившийся к нему Tcp-клиент (пульт Android):


// Tcp-сервер (запущен на плате)
TcpServer tcpServer;
// и Tcp-клиент (входящее подключение)
TcpClient tcpClient;



Небольшой нюанс с последствиями. Плата разрывает соединение с клиентом (пультом), если от него не приходило никаких сообщений более 10ти секунд. Чтобы поддерживать сессию открытой более 10ти секунд и при этом не менять состояние робота (не включать и не выключать лампочку), пульт может периодически отправлять команду ping.


// Неактивного клиента отключаем через заданное таймаутом
// количество миллисекунд
int CLIENT_IDLE_TIMEOUT = 10000;
int clientIdleStart = 0;



Буферы для чтения и записи данных при общении с пультом:


// Буферы для обмена данными с клиентом
static char read_buffer[128];
static char write_buffer[128];
int write_size;



В сетапе ничего особенного - включим порт отладки UART, чтобы иметь возможность смотреть отладочные сообщения с платы на компьютере в MPIDE в окне Tools/Serial Monitor, и переведем ножку с тестовой лампочкой в режим вывода:


void setup() {
    Serial.begin(9600);
    Serial.println("Start WiFi network and Tcp server demo");

    pinMode(LED_PIN, OUTPUT);
}



Весь полезный код в главном цикле loop: объявим статусные и вспомогательные переменные, разместим вызов periodicTasks, необходимый для работы Tcp-стека (подробности разъяснял в лабе про подключение к WiFi).

Блок подключения к точке WiFi тоже здесь, но мы его опустим - все главные события будут разворачиваться по ветке else, в которую мы попадем в том случае, если подключение к точке WiFi проведено успешно.


void loop() {
    DNETcK::STATUS networkStatus;
    int readSize;
    int writeSize;
    
    // Держим Tcp-стек в живом состоянии
    DNETcK::periodicTasks();
        
    if(!DWIFIcK::isConnected(conectionId)) {
        ...
    } else ...


К сети подключены, проверим, запущен ли Tcp-сервер:


    } else if(!tcpServer.isListening()) {
        // Запустим TCP-сервер слушать подключения
        
        bool startedListening = false;


Не запущен, запускаем:


        Serial.print("Start listening connection from Pult...");
        tcpServer.startListening(tcp_server_port);


Вызов tcpServer.startListening() по давно известным нам принчинам не является блокирующим, поэтому дождемся успешного запуска сервера или же сообщения о невозможности запуска внутри следующего несложного блока:


        // Подождем, пока сокет начнет слушать подключения
        bool starting = true;
        while(starting) {
            Serial.print(".");
            if(tcpServer.isListening(&networkStatus)) {
                // Начали слушать
                startedListening = true;
                                    
                starting = false;
            } else if(DNETcK::isStatusAnError(networkStatus)) {
                // Не смогли начать слушать из-за ошибки,
                // в этом месте больше не пробуем
                starting = false;
            }
        }
        Serial.println();


В случае успешного запуска сервера, распечатаем его статус в отладочный порт и пойдем на следующий заход loop уже в другую ветку. В случае ошибки запуска подождем 4 секунды и пойдем тоже на следующий заход loop повторять попытку.


        if(startedListening) {
            // Начали слушать подключения от пульта
            Serial.print("Listen connection from Pult on: ");
            printTcpServerStatus();
        } else {
            // Так и не получилось начать слушать подключения
            Serial.print("Failed to start listening, status: ");
            printDNETcKStatus(networkStatus);
            Serial.println();
            
            // Вернем TCP-сервер в исходное состояние
            tcpServer.close();
            
            // Немного подождем и попробуем переподключиться на следующей итерации
            Serial.println("Retry after 4 seconds...");
            delay(4000);
        }


Сервер запущен, ожидаем подключения клиента-пульта:


    } else if(!tcpClient.isConnected()) {
        // Подождем подключения клиента
        
        if(tcpServer.availableClients() > 0) {
            // закроем старого клиента, если он использовался ранее
            tcpClient.close();

            if(tcpServer.acceptClient(&tcpClient)) {
                Serial.println("Got a Connection: ");
                printTcpClientStatus();
                
                // начнем счетчик неактивности
                clientIdleStart = millis();
            }
        }
    } else ...



Пульт подключен, ожидаем команды:


    } else {
        // Пульт подключен - читаем команды, отправляем ответы

        // есть что почитать?
        if((readSize = tcpClient.available()) > 0) {


Пришла команда - считаем ее в буфер read_buffer:


            readSize = readSize < sizeof(read_buffer) ? readSize : sizeof(read_buffer);
            readSize = tcpClient.readStream((byte*)read_buffer, readSize);



Содержимое команды внутри буфера read_buffer, только добавим завершающий ноль, чтобы далее с ней можно было работать как с обычной строкой:


            // Считали порцию данных - добавим завершающий ноль
            read_buffer[readSize] = 0;
            
            Serial.print("Read: ");
            Serial.println(read_buffer);


Распознаем и выполним команду в методе handleInput(), ответ будет записан в буфер write_buffer. Установка ненулевого значения в переменную write_size приведет к тому, что содержимое буфера write_buffer (первые write_size байт) будет записано в сокет далее по циклу.


            // и можно выполнить команду, ответ попадет в write_buffer
            writeSize = handleInput(read_buffer, readSize, write_buffer);
            write_size = writeSize;


И сбросим счетчик неактивности на текущее время:


            // сбросим счетчик неактивности
            clientIdleStart = millis();
        }



Отправим первые write_size байт из буфера write_buffer на пульт через сокет и заодно сбросим счетчик неактивности еще раз на всякий случай:


        if(write_size > 0) {
            Serial.print("Write: ");
            Serial.print(write_buffer);
            Serial.println();
            
            tcpClient.writeStream((const byte*)write_buffer, write_size);
            write_size = 0;
            
            // сбросим счетчик неактивности
            clientIdleStart = millis();
        }


Проверим, сколько времени мы не общались с пультом - если больше 10ти секунд, то рвем соединение.


        if( (millis() - clientIdleStart) > CLIENT_IDLE_TIMEOUT ) {
            Serial.println("Close connection on timeout");
            tcpClient.close();
        }
    }


Главный цикл окончен.

Обработка входных данных - метод handleInput распознает команду (включить лампочку 'ledon', выключить лампочку 'ledoff' или просто прислать ответ на 'ping'), выполняет действие (включает или выключает лампочку), формирует ответ внутри reply_buffer ('ok' в случае успешного выполнения команды, 'dontunderstand', если команда не поддерживается), возвращает размер ответа байтах:


/**
* Обработать входные данные - разобрать строку, выполнить команду.
* @return размер ответа в байтах (0, чтобы не отправлять ответ).
*/
int handleInput(char* buffer, int size, char* reply_buffer) {
    int replySize = 0;
    reply_buffer[0] = 0;
    
    // Включить лампочку по команде "ledon", выключить по команде "ledoff"
    if(strcmp(buffer, CMD_LEDON) == 0) {
        Serial.println("Command 'ledon': turn light on");
        
        // Выполнить команду
        digitalWrite(LED_PIN, HIGH);
        
        // Подготовить ответ
        strcpy(reply_buffer, REPLY_OK);
        replySize = strlen(reply_buffer);
    } else if (strcmp(buffer, CMD_LEDOFF) == 0) {
        Serial.println("Command 'ledoff': turn light off");
        
        // Выполнить команду
        digitalWrite(LED_PIN, LOW);
        
        // Подготовить ответ
        strcpy(reply_buffer, REPLY_OK);
        replySize = strlen(reply_buffer);
    } else if (strcmp(buffer, CMD_PING) == 0) {
        Serial.println("Command 'ping': reply ok");
                
        // Подготовить ответ
        strcpy(reply_buffer, REPLY_OK);
        replySize = strlen(reply_buffer);
    } else {
        Serial.print("Unknown command: ");
        Serial.println(buffer);
        
        // Подготовить ответ
        strcpy(reply_buffer, REPLY_DONTUNDERSTAND);
        replySize = strlen(reply_buffer);
    }
    
    return replySize;
}





пульт Android далее...

исходники занятия, подсветка синтаксиса.
Tags: android, chipkit, типовые задачи
Subscribe

  • Post a new comment

    Error

    default userpic

    Your IP address will be recorded 

    When you submit the form an invisible reCAPTCHA check will be performed.
    You must follow the Privacy Policy and Google Terms of use.
  • 5 comments