В предыдущих статьях мы разбирались с основами Alljoyn и средствами, помогающими отладке. Пришло время писать код для микроконтроллера. Кратко напомню архитектуру LSF (Lighting Software Framework).
В библиотеке LSF предусмотрено три сущности:
- Thin-лампочка (lamp service),
- Router (lighting controller service),
- «приложение» (lighting sample application).
Thin-лампочка это та часть, которая «крутится» непосредственно в микроконтроллере нашей умной лампочки. Именно ею мы сегодня и займемся. Остальное было весьма подробно описано ранее, очередной раз останавливаться не будем.
Главное, чтобы все действующие лица были в одной локальной сети, а Router был «правильный» (см. первую часть цикла).
Наша конфигурация оборудования
В роли «лампочки» (физически) выступает отладочная плата с samd21 и wifi модулем winc 1500, в роли Router’а — программа в Ubuntu lighting controller service. В качестве управляющего приложения будем использовать sample app LSF на телефоне Android, которое можно скачать с сайта Allseen альянса со страницы соответствующей рабочей группы.
Код для нашей отладки писался на базе кода для arduino (Thin Core) и открытого кода на гитхабе для «лампочки». Как нам тогда казалось, это наиболее близкий к чистому Си и отсутствию операционки пример. Но как позже выяснилось, этот код во многом не доделан и не выполняет даже базовые функции Thin устройства. Так что доделывать/переделывать пришлось много.
Весь код делится на 3 части: поддержка Thin Core Alljoyn, поддержка «лампы» LSF, и все остальное (hal уровень, собственные функции).
Код для Thin Core
Первое, что нужно сделать, это реализовать hal уровень для возможности выхода в сеть. У нас он заключается в поднятии соединения по UDP (для первичного обнаружения Thin устройства в сети Alljoyn с помощью mdns), отправке/приеме данных по UDP, а также поднятии соединения по TCP и приеме/отправки данных для основного общения в сети.
Все эти функции прописываются в файле aj_net.с.
Основной проблемой было то, что функции взаимодействия с сетью в arduino работают по флагу, а в библиотеке для winc по прерыванию, а также для работы библиотеки для winc обязателен постоянный вызов вспомогательной функции опроса флагов, выставляемых модулем. Подробно про работу с winc1500 мы писали в одной из наших предыдущих статей.
Начать модифицировать hal уровень проще всего с функций установки соединения по UDP и TCP. В них надо прописать необходимые строки для установления непосредственно соединения и настроить структуру, соответствующую соединению: указать функции приема/передачи и буферы приема/передачи.
AJ_Status AJ_Net_MCastUp(AJ_NetSocket* netSock) { uint8_t ret = 1; if (ret != 1) { return AJ_ERR_READ; } else { netSock->rx.bufStart = udp_data_rx; netSock->rx.bufSize = sizeof(udp_data_rx); netSock->rx.readPtr = udp_data_rx; netSock->rx.writePtr = udp_data_rx; netSock->rx.direction = AJ_IO_BUF_RX; netSock->rx.recv = AJ_Net_RecvFrom; netSock->tx.bufStart = udp_data_tx; netSock->tx.bufSize = sizeof(udp_data_tx); netSock->tx.readPtr = udp_data_tx; netSock->tx.writePtr = udp_data_tx; netSock->tx.direction = AJ_IO_BUF_TX; netSock->tx.send = AJ_Net_SendTo; } return AJ_OK; } int main(void) { ... // Initialize socket address structure. addr.sin_family = AF_INET; addr.sin_port = _htons(MAIN_WIFI_M2M_SERVER_PORT); addr.sin_addr.s_addr = _htonl(MAIN_WIFI_M2M_SERVER_IP); src_addr.sin_family = AF_INET; src_addr.sin_port = _htons(MAIN_WIFI_M2M_SERVER_PORT); //_htons(52148); src_addr.sin_addr.s_addr = _htonl(MAIN_WIFI_M2M_SERVER_IP); // Initialize Wi-Fi parameters structure. memset((uint8_t *)¶m, 0, sizeof(tstrWifiInitParam)); // Initialize Wi-Fi driver with data and status callbacks. param.pfAppWifiCb = wifi_cb; ret = m2m_wifi_init(¶m); if (M2M_SUCCESS != ret) { printf("main: m2m_wifi_init call error!(%d)\r\n", ret); while (1); } // Initialize socket module socketInit(); registerSocketCallback(socket_cb, NULL); // Connect to router. m2m_wifi_connect((char *)MAIN_WLAN_SSID, sizeof(MAIN_WLAN_SSID), MAIN_WLAN_AUTH, (char *)MAIN_WLAN_PSK, M2M_WIFI_CH_ALL); printf("m2m_wifi_connect!\r\n"); ... }
AJ_Status AJ_Net_Connect(AJ_BusAttachment* bus, const AJ_Service* service) { int ret; if (!(service->addrTypes & AJ_ADDR_TCP4)) { return AJ_ERR_CONNECT; } printf("AJ_Net_Connect()\n"); addr.sin_port = _htons(service->ipv4port); addr.sin_addr.s_addr = _htonl(service->ipv4); printf("AJ_Net_Connect(): ipv4= %x, port = %d\n",addr.sin_addr.s_addr, addr.sin_port); tcp_client_socket = socket(AF_INET, SOCK_STREAM, 0); ret=connect(tcp_client_socket, (struct sockaddr *)&addr, sizeof(struct sockaddr_in)); printf("AJ_Net_Connect(): connect\n"); while(tcp_ready_to_send==0) { m2m_wifi_handle_events(NULL); } printf("AJ_Net_Connect(): connect OK\n"); if (ret == -1) { return AJ_ERR_CONNECT; } else { bus->sock.rx.bufStart = AJ_in_data_tcp; bus->sock.rx.bufSize = sizeof(AJ_in_data_tcp); bus->sock.rx.readPtr = AJ_in_data_tcp; bus->sock.rx.writePtr = AJ_in_data_tcp; bus->sock.rx.direction = AJ_IO_BUF_RX; bus->sock.rx.recv = AJ_Net_Recv; bus->sock.tx.bufStart = tcp_data_tx; bus->sock.tx.bufSize = sizeof(tcp_data_tx); bus->sock.tx.readPtr = tcp_data_tx; bus->sock.tx.writePtr = tcp_data_tx; bus->sock.tx.direction = AJ_IO_BUF_TX; bus->sock.tx.send = AJ_Net_Send; printf("AJ_Net_Connect(): connect() success: status=AJ_OK\n"); return AJ_OK; } printf("AJ_Net_Connect(): connect() failed: %d: status=AJ_ERR_CONNECT\n", ret); return AJ_ERR_CONNECT; }
По UDP нам нужно фактически только отправлять mdns запросы и получать ответ на них. При получении посылки проверяется есть ли что-то на отправку. Если да, то отправляется, после чего вызывается вспомогательная функция обработки флагов. Если флаг успешной отправки установлен (он устанавливается в callback’е), то функция завершает свою работу успешно, иначе возвращает ошибку записи.
AJ_Status AJ_Net_SendTo(AJ_IOBuffer* buf) { int ret; uint32_t tx = AJ_IO_BUF_AVAIL(buf); if (tx > 0) { ret = sendto(rx_socket, buf->readPtr, tx, 0, (struct sockaddr *)&addr, sizeof(addr)); m2m_wifi_handle_events(NULL); if (sock_tx_state != 1) { return AJ_ERR_WRITE; } buf->readPtr += ret; } AJ_IO_BUF_RESET(buf); return AJ_OK; }
При приеме в цикле с выходом по тайм-ауту или получении посылки вызывается обработчик приема и вспомогательная функция обработки флагов. Знатоки arduino заметят, что я использую функцию millis (она была переписана в соответствии с нашими реалиями). Если выход из цикла ожидания посылки произошел по тайм-ауту, то возвращается ошибка чтения, иначе статус AJ_OK.
AJ_Status AJ_Net_RecvFrom(AJ_IOBuffer* buf, uint32_t len, uint32_t timeout) { AJ_Status status = AJ_OK; int ret; uint32_t rx = AJ_IO_BUF_SPACE(buf); unsigned long Recv_lastCall = millis(); while ((sock_rx_state==0) && (millis() - Recv_lastCall < timeout)) { recv(rx_socket, udp_data_rx, MAIN_WIFI_M2M_BUFFER_SIZE, 0); m2m_wifi_handle_events(NULL); } ret=sock_rx_state; if (ret == -1) { printf("AJ_Net_RecvFrom(): read() fails. status=AJ_ERR_READ\n"); status = AJ_ERR_READ; } else { if (ret != -1) { AJ_DumpBytes("AJ_Net_RecvFrom", buf->writePtr, ret); } buf->writePtr += ret; status = AJ_OK; } printf("AJ_Net_RecvFrom(): status=%s\n", AJ_StatusText(status)); return status; }
Теперь перейдем к реализации приема/передачи по TCP. Передача мало чем отличается от передачи по UDP.
AJ_Status AJ_Net_Send(AJ_IOBuffer* buf) { uint32_t ret; uint32_t tx = AJ_IO_BUF_AVAIL(buf); printf("AJ_Net_Send(buf=0x%p)\n", buf); if (tx > 0) { send(tcp_client_socket, buf->readPtr, tx, 0); buf->readPtr += tcp_tx_ready; tcp_tx_ready=0; } AJ_IO_BUF_RESET(buf); return AJ_OK; }
А вот с приемом все немного интереснее. Общий подход, конечно, такой же, как в UDP, но в исходных кодах была еще куча проверок, разобраться, что они там делают и насколько они нужны мне не удалось, поэтому в большинстве оставила тот код.
AJ_Status AJ_Net_Recv(AJ_IOBuffer* buf, uint32_t len, uint32_t timeout) { AJ_Status status = AJ_ERR_READ; uint32_t ret; uint32_t rx = AJ_IO_BUF_SPACE(buf); uint32_t recvd = 0; unsigned long Recv_lastCall = millis(); // first we need to clear out our buffer uint32_t M = 0; if (rxLeftover != 0) { // there was something leftover from before, M = min(rx, rxLeftover); memcpy(buf->writePtr, rxDataStash, M); // copy leftover into buffer. buf->writePtr += M; // move the data pointer over memmove(rxDataStash, rxDataStash + M, rxLeftover - M); // shift left-overs toward the start. rxLeftover -= M; recvd += M; // we have read as many bytes as we can // higher level isn't requesting any more if (recvd == rx) { return AJ_OK; } } if ((M != 0) && (rxLeftover != 0)) { printf("AJ_Net_REcv(): M was: %d, rxLeftover was: %d\n", M, rxLeftover); } while ((tcp_rx_ready==0) && (millis() - Recv_lastCall < timeout)) { recv(tcp_client_socket, tcp_data_rx, sizeof(tcp_data_rx), 0); m2m_wifi_handle_events(NULL); } if (tcp_rx_ready==0) { printf("AJ_Net_Recv(): timeout. status=AJ_ERR_TIMEOUT\n"); status = AJ_ERR_TIMEOUT; } else { memcpy(AJ_in_data_tcp, tcp_data_rx,tcp_rx_ready); uint32_t askFor = rx; askFor -= M; ret=tcp_rx_ready; if (askFor < ret) { printf("AJ_Net_Recv(): BUFFER OVERRUN: askFor=%u, ret=%u\n", askFor, ret); } if (ret == -1) { printf("AJ_Net_Recv(): read() failed. status=AJ_ERR_READ\n"); status = AJ_ERR_READ; } else { AJ_DumpBytes("Recv", buf->writePtr, ret); if (ret > askFor) { printf("AJ_Net_Recv(): new leftover %d\n", ret - askFor); // now shove the extra into the stash memcpy(rxDataStash + rxLeftover, buf->writePtr + askFor, ret - askFor); rxLeftover += (ret - askFor); buf->writePtr += rx; } else { buf->writePtr += ret; } status = AJ_OK; } } tcp_rx_ready=0; return status; }
Еще один важный момент — LocalGUID — уникальный идентификатор устройства (см. вторую часть статьи), который мы честно позаимствовали (с небольшими изменениями) у лампочки, реализованной на линуксе.
Одно из важных исправлений: в исходном коде в запросе mdns ip адрес лампочки задается явно. Если вы не хотите переписывать его у каждого устройства ручками, то надо добавить считывание присвоенного ip адреса (мы будем использовать dhcp для получения адреса в сети) и его запись в пакет. Это делается в файле: aj_disco.c в функции: ComposeMDnsReq(…).
Код для «лампочки»
Приступаем к реализации самой «лампочки» в терминах LSF. Для индикации работы как лампочки будем использовать пользовательский светодиод на отладочной плате. Соответственно в железе у нас будет реализована только возможность включать/выключать светодиод.
В ходе реализации этой части программы было сделано достаточно много изменений, которые «заставили» ее работать. Перечислять их все здесь не будем, отметим только важные моменты.
HAL уровень работы с «лампочкой» мы реализовали в функции OEM_LS_TransitionStateFields, файл OEM_LS_Code.c. Можно было это сделать менее локально, но так как hal уровень поддерживает только включение/выключение, не стали тратить на это много сил и времени.
LampResponseCode OEM_LS_TransitionStateFields(LampStateContainer* newStateContainer, uint64_t timestamp, uint32_t transitionPeriod) { //OEMs should do the following operations just before transitioning the state LampState state; /* Retrieve the current state of the Lamp */ LAMP_GetState(&state); /* Update the requisite fields to new values */ if (newStateContainer->stateFieldIndicators & LAMP_STATE_ON_OFF_FIELD_INDICATOR) { state.onOff = newStateContainer->state.onOff; printf("%s: Updating OnOff to %u\n", __func__, state.onOff); printf("----------------state.onOff=%d-----------------------\n",state.onOff); if (state.onOff==1) { port_pin_set_output_level(LED_0_PIN, LED_0_ACTIVE); } else { port_pin_set_output_level(LED_0_PIN, LED_0_INACTIVE); } } ... }
Если хочется поменять название компании производителя устройства, имя устройства, поддерживаемые языки и другие параметры, то нужно изменить соответствующие строки в начале файла OEM_LS_Provisioning.c.
Все настройки изначально берутся и при изменении записываются в некую NVRAM. Как это реализовать в нашем случае, мы не придумали, поэтому везде где использовалась запись/чтение из NVRAM мы изменили код на работу в наших реалиях.
Перечислить все «грабли», на которые мы наступали просто невозможно (были и тупиковые ветви, часть забылась). Поэтому мы упомянули о главных и способах их «отлова» — брать заведомо работающие приложения (например под linux), и смотреть в WireShark обмен, который пытаться повторить.
Но когда все «грабли» пройдены, работающая система вызывает умиление своей продуманностью и, собственно работой (наконец-то!). Посмотреть можно на видео в начале статьи.
Код проекта выложен на гитхаб
ссылка на оригинал статьи https://habrahabr.ru/post/278363/
Добавить комментарий