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

Category:

ChipKIT и Ётафон: двусторонняя связь

Продолжаем дружить ChipKIT и Yotaphone: в прошлый раз выбрали оборудование и научили ChipKIT и Ётафон видеть друг друга, сейчас откроем между ними канал двусторонней связи, чтобы Ётафон мог отправлять ChipKIT'у послания, а ChipKIT мог на них отвечать и заодно мигать лампочкой.

ChipKIT Max32 и Yotaphone from 1i7 on Vimeo.





Откроем канал двусторонней связи на Андроиде

Для общения с устройством используются традиционные для Java потоки ввода и вывода InputStream и OutputStream; специальный ParcelFileDescriptor для доступа и контроля над этими каналами.


    private ParcelFileDescriptor fileDescriptor;
    private FileInputStream accessoryInput;
    private FileOutputStream accessoryOutput;



Открываем аксессуар при помощи полученной ранее ссылки на UsbAccessory и UsbManager'а, сохраняем ссылки на потоки ввода и вывода accessoryInput и accessoryOutput.


    /**
     * Открыть канал коммуникации с указанным аксессуаром.
     *
     * @param accessory
     *     аксессуар для подключения
     */
    private void openAccessory(UsbAccessory accessory) {
        fileDescriptor = usbManager.openAccessory(accessory);
        if (fileDescriptor != null) {
            this.usbAccessory = accessory;
            final FileDescriptor fd = fileDescriptor.getFileDescriptor();

            accessoryInput = new FileInputStream(fd);
            accessoryOutput = new FileOutputStream(fd);

            ...

            debug("openAccessory: connected accessory: manufacturer="
                    + usbAccessory.getManufacturer() + ", model="
                    + usbAccessory.getModel());
        } else {
            debug("openAccessory: Failed to open accessory");
        }
    }




И на Аксессуаре

Заведем несколько глобальных переменных с текущим статусом и буферами для чтения и записи.


BOOL readInProgress = FALSE;
BOOL writeInProgress = FALSE;

char read_buffer[128];
char write_buffer[128];
int write_size;


Всё дальнейшее будет происходить внутри бесконечного цикла loop() при условии, что устройство подключено (т.е. флаг deviceAttached выставлен в TRUE).


void loop() {
    DWORD readSize;
    DWORD writeSize;
    uint8_t errorCode;
  
    // Запускаем периодические задачи для поддержания стека USB в живом и корректном состоянии.
    // Следует выполнять их хотябы один раз внутри цикла или в момент, когда нужно
    // обновить внутреннее состояние контроллера USB хоста.
    USBTasks();

    if(deviceAttached) {
        // Далее операции чтения и записи
        ...
    }
}



Отправим данные с Андроида

Нажимаем кнопку "Включить лампочку" или "Выключить лампочку" - просто запишем нужные байты в поток вывода accessoryOutput и на всякий случай сделаем ему же flush, чтобы они не застряли в каком-нибудь промежуточном системном буфере, а сразу ушли на устройство.


    /**
     * Отправить команду подключенному аксессуару.
     *
     * @param command
     *     команда для отправки
     */
    public void sendCommand(String command) {
        if (accessoryOutput != null) {
            try {
                debug("Write: " + command);

                accessoryOutput.write(command.getBytes());
                accessoryOutput.flush();
            } catch (IOException e) {
                debug("Write error: " + e.getMessage());
                e.printStackTrace();
            }
        }
    }




И прочитаем их на Аксессуаре

На очередном витке loop() вызываем USBAndroidHost.AppRead() с ссылками на текущее подключенное устройство и буфер для записи считываемых данных. Как уже отмечалось выше, большинство вызовов стека USB на контроллере не блокируют выполнение программы, чтобы вписаться парадигму однопоточной системы. В том числе вызов AppRead возвращается сразу независимо от того, пришли данные или нет, и если пришли, прочитаны они полностью или нет. Код возврата говорит о том, что во время операции чтения или произошла ошибка, или процесс чтения инициирован.


        // Чтение данных с устройства Android - ждем команду
        if(!readInProgress) {
            // Вызов не блокируется - проверка завершения чтения через AndroidAppIsReadComplete
            errorCode = USBAndroidHost.AppRead(deviceHandle, (uint8_t*)&read_buffer, (DWORD)sizeof(read_buffer));
            if(errorCode == USB_SUCCESS) {
                // Дождались команду - новую читать не будем, пока не придут все данные,
                // проверять завершение операции будем в следующих итерациях цикла
                readInProgress = TRUE;
            } else {
                Serial.print("Error trying to read: errorCode=");
                Serial.println(errorCode, HEX);
            }
        }



Будем проверять завершено ли чтение на последующих итерациях главного цикла до тех пор, пока USBAndroiHost.appIsReadComplete (который также не является блокирующим) не вернет TRUE. Если чтение завершилось без ошибок, полученные от Андроида данные окажутся записаны в буфер read_buffer, а количество считанных байт - в переменную readSize.


        // Проверим, завершилось ли чтение
        if(USBAndroidHost.AppIsReadComplete(deviceHandle, &errorCode, &readSize)) {
            // Разрешим читать следующую команду
            readInProgress = FALSE;
          
            if(errorCode == USB_SUCCESS) {
                // Считали порцию данных - добавим завершающий ноль
                read_buffer[readSize] = 0;
              
                Serial.print("Read: ");
                Serial.println(read_buffer);
              
                // и можно выполнить команду, ответ попадет в write_buffer
                writeSize = handleInput(read_buffer, readSize, write_buffer);
                              
                // Если writeSize не 0, отправим назад ответ - инициируем
                // процедуру записи для следующей итерации цикла (данные уже внутри write_buffer)
                write_size = writeSize;
            } else {
                Serial.print("Error trying to complete read: errorCode=");
                Serial.println(errorCode, HEX);
            }
        }


Выполним полученную команду - включим или выключим лампочку

Обработаем пришедшие данные в методе handleInput: распознаем команду (ledon или ledoff), выполним действие (включим или выключим светодиод), сформируем ответ.


/**
 * Обработать входные данные - разобрать строку, выполнить команду.
 * @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_LETMEGO) == 0) {
        Serial.println("Command 'letmego': send 'getout' reply");
      
        // Подготовить ответ
        strcpy(reply_buffer, REPLY_GETOUT);
        replySize = strlen(reply_buffer);
    } else {
        Serial.print("Unknown command: ");
        Serial.println(buffer);
      
        // Подготовить ответ
        strcpy(reply_buffer, REPLY_UNKNOWN_CMD);
        replySize = strlen(reply_buffer);
    }
  
    return replySize;
}



Отправим ответ от Аксессуара

Выше, после выполнения команды и формирования ответа в write_buffer, установили переменную write_size в ненулевое значение - это послужит сигналом к инициации процесса отправки ответа при помощи вызова USBAndroidHost.AppWrite (традиционно неблокирующий - он вернется до того, как запись будет завершена).


        // Отправка данных на устройство Android
        if(write_size > 0 && !writeInProgress) {
            Serial.print("Write: ");
            Serial.print(write_buffer);
            Serial.println();
        
            writeSize = write_size;
            // Нужная команда уже в буфере для отправки
            // Вызов не блокируется - проверка завершения чтения через AndroidAppIsWriteComplete
            errorCode = USBAndroidHost.AppWrite(deviceHandle, (uint8_t*)&write_buffer, writeSize);
                      
            if(errorCode == USB_SUCCESS) {
                writeInProgress = TRUE;
            } else {
                Serial.print("Error trying to write: errorCode=");
                Serial.println(errorCode, HEX);
              
                write_size = 0;
            }
        }


И на следующих итерациях главного цикла будем проверять, завершилась ли запись при помощи вызова USBAndroidHost.AppIsWriteComplete. Если он вернул TRUE и код возврата USB_SUCCESS, данные отправлены.


        if(writeInProgress) {
            // Проверим, завершена ли запись
            if(USBAndroidHost.AppIsWriteComplete(deviceHandle, &errorCode, &writeSize)) {
                writeInProgress = FALSE;
                write_size = 0;
  
                if(errorCode != USB_SUCCESS) {
                    Serial.print("Error trying to complete write: errorCode=");
                    Serial.println(errorCode, HEX);
                }
            }
        }



И примем его на Андроиде

Вернемся в метод openAccessory, где мы недавно получили ссылку на поток входных данных accessoryInput. Для того, чтобы постоянно мониторить поток входных данных от аксессуара и при этом не блокировать интерфейс, запустим постоянный цикл чтения данных в фоновом потоке.

Вызов accessoryInput.read будет блокироваться каждый раз до тех пор, пока в потоке не появятся данные, пришедшие от аксессуара, или аксессуар не будет отключен физически.


    private void openAccessory(UsbAccessory accessory) {
        ...
        if (fileDescriptor != null) {
            ...
            accessoryInput = new FileInputStream(fd);
            accessoryOutput = new FileOutputStream(fd);
            final Thread inputThread = new Thread(new Runnable() {

                @Override
                public void run() {
                    byte[] buffer = new byte[READ_BUFFER_SIZE];
                    int readBytes = 0;
                    while (readBytes >= 0) {
                        try {
                            debug("read bytes...");
                            // Этот вызов разблокируется только тогда, когда
                            // аксессуар пришлет какие-то данные или когда
                            // он будет отсоединен физически.
                            // Закрывать IntputStream, FileDescriptor, Accessory
                            // и что угодно еще из Java не поможет
                            // (см обсуждение здесь:
                            // http://code.google.com/p/android/issues/detail?id=20545
                            // ).
                            readBytes = accessoryInput.read(buffer);
                            final String reply = new String(buffer);

                            final String postMessage = "Read: " + "num bytes="
                                    + readBytes + ", value="
                                    + new String(buffer);

                            debug(postMessage);
                            handler.post(new Runnable() {
                                @Override
                                public void run() {
                                    Toast.makeText(USBClientActivity.this,
                                            postMessage, Toast.LENGTH_SHORT)
                                            .show();
                                }
                            });

                            // Поэтому нам нужна специальная команда "letmego",
                            // на которую аксессуар пришлет ответ "getout"
                            // и этот поток сможет завершиться.
                            if (REPLY_GETOUT.equals(reply)) {
                                break;
                            }
                        } catch (final Exception e) {
                            debug("Accessory read error: " + e.getMessage());
                            e.printStackTrace();
                            break;
                        }
                    }
                    debug("Input reader thread finish");
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            updateViews();
                        }
                    });
                }
            });
            inputThread.start();

            debug("openAccessory: connected accessory: manufacturer="
                    + usbAccessory.getManufacturer() + ", model="
                    + usbAccessory.getModel());
        } else {
            debug("openAccessory: Failed to open accessory");
        }
    }



Замечание: Как сказано выше, вызов accessoryInput.read блокируется до тех пор, пока с аксессуара не придут какие-нибудь данные, или он не будет отсоединен физически. Правильно это или нет, но другого простого программного способа снять блок с этого вызова не предусмотрено - вызовы accessoryInput.close() или fileDescriptor.close() не вызовут ожидаемого эффекта (см обсуждение соответствующего отчета об ошибке, висящего с 2011 года). По этой причине для того, чтобы приложение Андроид имело программную возможность корректно завершить работу с аксессуаром не дожидаясь выдергивания провода, в протокол общения была добавлена вспомогательная команда разрыва связи 'letmego', от которой только требуется прислать в ответ строку 'getout': вызов inputReader.read получает данные и снимает блок, а приложение получает возможность завершить фоновый цикл.


Таким образом, нажимая кнопки на экране Ётафона, мы будем включать и выключать светодиодную лампочку, подключенную к плате ChipKIT (при желании, можно заменить лампочку на моточк). Задача в целом выполнена.

Max32+Yotaphone-03-ledon.jpg


Android Accessory-03-connected-reply.png

Добавим только несколько завершающих штрихов.

Получение системных событий от аксессуара и реакция на отключение

Для того, чтобы приложение корректно реагировало на отключение аксессуара (закрывало экран работы с аксессуаром или отключало на нем элементы управления, чтобы пользователь не слал команды в пустоту), мы имеем возможность получать системные события от подсистемы USB при помощи механизма BroadcastReceiver и должным образом их обрабатывать. При отключении аксессуара от смартфона система генерирует событие UsbManager.ACTION_USB_ACCESSORY_DETACHED - получим его в нашем BroadcastReceiver и выполним все необходимые действия.


    private final BroadcastReceiver usbReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (ACTION_USB_PERMISSION.equals(action)) {
                ...
            } else if (UsbManager.ACTION_USB_ACCESSORY_DETACHED.equals(action)) {
                final UsbAccessory accessory = (UsbAccessory) intent
                        .getParcelableExtra(UsbManager.EXTRA_ACCESSORY);
                if (accessory != null && accessory.equals(usbAccessory)) {
                    debug("Broadcast: accessory detached");

                    disconnectFromAccessory();
                    updateViews();
                }
            }
        }
    };


Чтобы наш BroadcastReceiver получал системные события, в том числе UsbManager.ACTION_USB_ACCESSORY_DETACHED, создадим в onCreate правильный фильтр событий IntentFilter и зарегистрируем его с Context.registerReciver.


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...

        final IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
        filter.addAction(UsbManager.ACTION_USB_ACCESSORY_DETACHED);
        registerReceiver(usbReceiver, filter);

        ...
    }


Выдернули провод - приложение отреагировало правильно, лампочка на плате сохранила последнее состояние.

Max32+Yotaphone-04-disconnected.jpg


Android Accessory-04-disconnected.png



в целом всё, небольшое послесловие следует

подсветка синтаксиса
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.
  • 0 comments