Alljoyn: взгляд embedded разработчика. Часть 3: Портируем на МК SAMD21

от автора


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

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

ссылка на оригинал статьи https://habrahabr.ru/post/278363/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *