В предыдущих статьях мы разбирались с основами 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. В них надо прописать необходимые строки для установления непосредственно соединения и настроить структуру, соответствующую соединению: указать функции приема/передачи и буферы приема/передачи.
Код для UDP (соединение устанавливается в main)
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");
...
}
Код для TCP
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, но в исходных кодах была еще куча проверок, разобраться, что они там делают и насколько они нужны мне не удалось, поэтому в большинстве оставила тот код.
Прием по TCP
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 обмен, который пытаться повторить.
Но когда все «грабли» пройдены, работающая система вызывает умиление своей продуманностью и, собственно работой (наконец-то!). Посмотреть можно на видео в начале статьи.
Код проекта выложен на гитхаб
sav6622
Реальная бытовая техника с данным протоколом в продаже доступна?
marus-ka
Цитата из первой статьи цикла: