В предыдущих статьях мы разбирались с основами 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 обмен, который пытаться повторить.
Но когда все «грабли» пройдены, работающая система вызывает умиление своей продуманностью и, собственно работой (наконец-то!). Посмотреть можно на видео в начале статьи.

Код проекта выложен на гитхаб

Комментарии (4)


  1. sav6622
    29.03.2016 10:11

    Реальная бытовая техника с данным протоколом в продаже доступна?


    1. marus-ka
      29.03.2016 10:14

      Цитата из первой статьи цикла:

      На сайте альянса есть страница Product Showcase, на которой публикуется список существующего оборудования. Насколько этот список полон и актуален можно только догадываться.


  1. sakutylev
    29.03.2016 11:07

    Не совсем понял зачем хардкодить адрес или присваивать адреса с помощью dhcp, нельзя ли использовать все преимущества zeroconf(тот же mdns, link-local адреса)


    1. marus-ka
      29.03.2016 11:17

      Я не совсем поняла, к кому вопрос. Именно к моей реализации или к самому протоколу Alljoyn?