wtorek, 22 maja 2018

[34] STM32F4 - Biblioteki HAL - HID, klawiatura, enkoder

W tym poście chciałbym przedstawić projekt klawiatury funkcyjnej wraz z zaimplementowanym enkoderem. Ich zadaniem jest przesyłanie komunikatów jako klawiatura oraz myszka w interfejsie HID.

[Źródło: http://www.st.com/en/evaluation-tools/stm32f4discovery.html]

Opis projektu:


Do projektu wykorzystałem klawiaturę numeryczną 16 klawiszową. Będzie ona przesyłała przykładowe sekwencję znaków ze skrótami dla programu Altium Designer. Zadanie enkodera będzie mniej praktyczne. Jego zadaniem będzie przesuwanie myszki na ekranie komputera. 

Opis funkcji klawiatury:

Do obsługi klawiatury przygotowałem bibliotekę, która pozwala w łatwy sposób na dołączenie nowej klawiatury jak i zdefiniowanie rozmiaru każdej z nich. Obsługa przycisku odbywa się w przerwaniu od timera, w którym następuje skanowanie wierszy w poszukiwaniu stanu niskiego.

Implementacja timera:

  1. static void MX_TIM6_Init(void)
  2. {
  3.     TIM_MasterConfigTypeDef sMasterConfig;
  4.     htim6.Instance = TIM6;
  5.     htim6.Init.Prescaler = 70;
  6.     htim6.Init.CounterMode = TIM_COUNTERMODE_UP;
  7.     htim6.Init.Period = 999;
  8.     if (HAL_TIM_Base_Init(&htim6) != HAL_OK)
  9.     {
  10.       _Error_Handler(__FILE__, __LINE__);
  11.     }
  12.     sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE;
  13.     sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
  14.     if (HAL_TIMEx_MasterConfigSynchronization(&htim6, &sMasterConfig) != HAL_OK)
  15.     {
  16.       _Error_Handler(__FILE__, __LINE__);
  17.     }
  18. }

Tutaj należy pamiętać o odpowiednio skonfigurowanym timerze. Za często wywoływanie sprawdzenia spowoduje częstszą implementacje wciśnięcia przycisku. Co w konsekwencji będzie powodowało przesyłanie większej sekwencji znaków do komputera. Można zmniejszyć dzielnik w timerze gdy czyszczenie ostatnio klikniętego przycisku będziemy wykonywać po wysłaniu wiadomości do komputera. To w większości przypadków powinno rozwiązać taki problem.

Poniżej sposób inicjalizacji oraz opis skanowania przycisków:

  1. static void keypad_EnablePins(void)
  2. {
  3.     /* Column 1 */
  4.     ButtonInitInput(KEYPAD_ROW_1_PORT, KEYPAD_ROW_1_PIN);
  5.     /* Column 2 */
  6.     ButtonInitInput(KEYPAD_ROW_2_PORT, KEYPAD_ROW_2_PIN);
  7.     /* Column 3 */
  8.     ButtonInitInput(KEYPAD_ROW_3_PORT, KEYPAD_ROW_3_PIN);
  9.     /* Column 4 */
  10.     ButtonInitInput(KEYPAD_ROW_4_PORT, KEYPAD_ROW_4_PIN);
  11.     /* Row 1 */
  12.     ButtonInitOutput(KEYPAD_COLUMN_1_PORT, KEYPAD_COLUMN_1_PIN);
  13.     /* Row 2 */
  14.     ButtonInitOutput(KEYPAD_COLUMN_2_PORT, KEYPAD_COLUMN_2_PIN);
  15.     /* Row 3 */
  16.     ButtonInitOutput(KEYPAD_COLUMN_3_PORT, KEYPAD_COLUMN_3_PIN);
  17.     /* Row 4 */
  18.     ButtonInitOutput(KEYPAD_COLUMN_4_PORT, KEYPAD_COLUMN_4_PIN);
  19. }

Powyżej jako piny wejściowe są konfigurowane kolumny. Natomiast jako piny wyjściowe ustawiane są wiersze.

Dokładne ustawienie pinów wygląda następująco:

  1. static void ButtonInitOutput(GPIO_TypeDef *GPIOx, uint16_t Pin)
  2. {
  3.     GPIO_InitTypeDef GPIO_InitStruct;
  4.     PORT_CLOCK_ENABLE(GPIOx);
  5.     GPIO_InitStruct.Pin = Pin;
  6.     GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  7.     GPIO_InitStruct.Pull = GPIO_PULLUP;
  8.     GPIO_InitStruct.Speed = GPIO_SPEED_FAST;
  9.     HAL_GPIO_Init(GPIOx, &GPIO_InitStruct);
  10. }
  11. static void ButtonInitInput(GPIO_TypeDef *GPIOx, uint16_t Pin)
  12. {
  13.     GPIO_InitTypeDef GPIO_InitStruct;
  14.     PORT_CLOCK_ENABLE(GPIOx);
  15.     GPIO_InitStruct.Pin = Pin;
  16.     GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
  17.     GPIO_InitStruct.Pull = GPIO_PULLUP;
  18.     GPIO_InitStruct.Speed = GPIO_SPEED_FAST;
  19.     HAL_GPIO_Init(GPIOx, &GPIO_InitStruct);
  20. }

Jak już wspomniałem wcześniej przerwanie od timera skanuje linie oraz wybiera kliknięty przycisk:

  1. void TIM6_DAC_IRQHandler(void)
  2. {
  3.   /* USER CODE BEGIN TIM6_DAC_IRQn 0 */
  4.   /* USER CODE END TIM6_DAC_IRQn 0 */
  5.   HAL_TIM_IRQHandler(&htim6);
  6.   KeyPad_Update();
  7.   /* USER CODE BEGIN TIM6_DAC_IRQn 1 */
  8.   /* USER CODE END TIM6_DAC_IRQn 1 */
  9. }

Funkcja sprawdzania przycisku:

  1. void KeyPad_Update(void)
  2. {
  3.     static uint16_t timeCounter = 0;
  4.     timeCounter++;
  5.     if (timeCounter >= KEYPAD_READ_INTERVAL && keypadStatusClicked.clickedBtn == KEYPAD_NO_PRESSED)
  6.     {
  7.         timeCounter = 0;
  8.         keypadStatusClicked.clickedBtn = (Keyboard_Btn_Typedef)keyPad_Read();
  9.     }
  10. }


Tutaj następuje sprawdzenie czy przycisk został wciśnięty, jeśli tak to przypisywany jest jego identyfikator do zmiennej. 

Obsługa wciśnięcia przycisku odbywa się w pętli głównej. Głównie dlatego aby przerwanie od klawiatury zostało obsłużone najszybciej jak to tylko możliwe bez zbędnego przedłużania:


  1. void KeyPad_CheckClicked(Keyboard_Btn_Typedef* Keypad_Button)
  2. {
  3.       if (*Keypad_Button != KEYPAD_NO_PRESSED)
  4.       {
  5.           if(*Keypad_Button == Btn_0)
  6.           {
  7.               Usb_Hid_PlacePad();
  8.           }
  9.           else if(*Keypad_Button == Btn_1)
  10.           {
  11.               Usb_Hid_PlaceVia();
  12.           }
  13.           else if(*Keypad_Button == Btn_2)
  14.           {
  15.               Usb_Hid_PlaceInteractiveRoutine();
  16.           }
  17.           else if(*Keypad_Button == Btn_3)
  18.           {
  19.               Usb_Hid_PlaceWire();
  20.           }
  21.           else if(*Keypad_Button == Btn_4)
  22.           {
  23.               Usb_Hid_PlaceBus();
  24.           }
  25.           else if(*Keypad_Button == Btn_5)
  26.           {
  27.               Usb_Hid_PlacePort();
  28.           }
  29.           else if(*Keypad_Button == Btn_6)
  30.           {
  31.               Usb_Hid_ToggleVisibleGrid();
  32.           }
  33.           else if(*Keypad_Button == Btn_7)
  34.           {
  35.               Usb_Hid_PlaceNetLabel();
  36.           }
  37.           else if(*Keypad_Button == Btn_8)
  38.           {
  39.               Usb_Hid_EnableDesignRules();
  40.           }
  41.           else if(*Keypad_Button == Btn_9)
  42.           {
  43.               Usb_Hid_PlacePowerPort();
  44.           }
  45.           else if(*Keypad_Button == Btn_Star)
  46.           {
  47.               Usb_Hid_PlaceText();
  48.           }
  49.           else if(*Keypad_Button == Btn_Hash)
  50.           {
  51.               Usb_Hid_PolygonPour();
  52.           }
  53.           else if(*Keypad_Button == Btn_A)
  54.           {
  55.               Usb_Hid_PlaceString();
  56.           }
  57.           else if(*Keypad_Button == Btn_B)
  58.           {
  59.               Usb_Hid_Place3DBody();
  60.           }
  61.           else if(*Keypad_Button == Btn_C)
  62.           {
  63.               Usb_Hid_FitDocument();
  64.           }
  65.           else if(*Keypad_Button == Btn_D)
  66.           {
  67.               Usb_Hid_PlaceManualJunction();
  68.           }
  69.       }
  70. }

Opis funkcji enkoder:

Na samym początku opiszę strukturę przechowującą wprowadzone dane:

  1. volatile typedef struct {
  2.     uint8_t lastPinAStatus;
  3.     int16_t encoderCounter;
  4.     int32_t rotationFromStart;
  5.     uint16_t maxRotationValue;
  6.     uint16_t minRotationValue;
  7.     uint8_t makeRound;
  8.     GPIO_TypeDef* GPIO_A;
  9.     GPIO_TypeDef* GPIO_B;
  10.     const uint16_t GPIO_PIN_A;
  11.     const uint16_t GPIO_PIN_B;
  12.     const IRQn_Type GPIO_PIN_A_IRQ;
  13. }ENCODER_t;

Idąc od początku:

  • lastPinAStatus - przechowuje ostatni stan pinu A
  • encoderCounter - pozycja licznika na osi 
  • rotationFromStart - ilość przekręcenia licznika w stronę każdej pozycji
  • maxRotationValue - największa wartość jaką można uzyskać po przekręceniu pozycji.
  • minRotationValue - najniższa wartość jaką można uzyskać po przekręcaniu licznika.
  • makeRound - informacja o tym czy po uzyskaniu maksymalnej pozycji ma nastąpić przejście po pozycji najniższej.
  • GPIOA - port GPIO do którego podłączony został jeden z pinów
  • GPIOB - port GPIO do którego został podłączony drugi pin enkodera
  • GPIO_PIN_A - pin pierwszego wyprowadzenia
  • GPIO_PIN_B - pin drugiego wyprowadzenia
  • GPIO_PIN_A_IRQ - przerwanie dla pierwszego wyprowadzenia.

Uruchomienie poszczególnych wyprowadzeń wygląda następująco:

  1. void Encoder_Init_Module(ENCODER_t* encoderStruct)
  2. {
  3.   GPIO_InitTypeDef GPIO_InitStruct;
  4.   Set_Start_Data_Struct(&encoderData);
  5.   ENCODER_PORT_ENABLE(encoderStruct->GPIO_A);
  6.   ENCODER_PORT_ENABLE(encoderStruct->GPIO_B);
  7.   GPIO_InitStruct.Pin = encoderStruct->GPIO_PIN_A;
  8.   GPIO_InitStruct.Pull = GPIO_NOPULL;
  9.   GPIO_InitStruct.Speed = GPIO_SPEED_FAST;
  10.   GPIO_InitStruct.Pull = GPIO_PULLUP;
  11.   GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING;
  12.   HAL_GPIO_Init(encoderStruct->GPIO_A, &GPIO_InitStruct);
  13.   GPIO_InitStruct.Pin = encoderStruct->GPIO_PIN_B;
  14.   GPIO_InitStruct.Pull = GPIO_NOPULL;
  15.   GPIO_InitStruct.Speed = GPIO_SPEED_MEDIUM;
  16.   GPIO_InitStruct.Pull = GPIO_PULLUP;
  17.   GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
  18.   HAL_GPIO_Init(encoderStruct->GPIO_B, &GPIO_InitStruct);
  19.   HAL_NVIC_SetPriority((IRQn_Type)(encoderStruct->GPIO_PIN_A_IRQ), 0x0F, 0x00);
  20.   HAL_NVIC_EnableIRQ((IRQn_Type)(encoderStruct->GPIO_PIN_A_IRQ));
  21. }

Sprawdzanie stanu odbywa się w obsłudze przerwania:

  1. void ENCODER_PIN_A_IRQ()
  2. {
  3.     if(EXTI->PR & EXTI_PR_PR0)
  4.     {
  5.         __disable_irq();
  6.         EXTI->PR = EXTI_PR_PR0;
  7.         EncoderGetRotationValue(&encoderData);
  8.         __enable_irq();
  9.     }
  10. }

Tutaj pin A podłączony jest do GPIO_0 wobec tego sprawdzane jest wywołanie przerwania od tego wyprowadzenia. Jeśli przerwanie zostało poprawnie wywołane to następuje sprawdzenie w którą stronę enkoder został obrócony:

  1. static void EncoderGetRotationValue(ENCODER_t* struct_data)
  2. {
  3.     uint8_t readStateA = 0;
  4.     uint8_t readStateB = 0;
  5.     readStateA = (((struct_data->GPIO_A)->IDR & (struct_data->GPIO_PIN_A)) == 0 ? 0 : 1);
  6.     readStateB = (((struct_data->GPIO_B)->IDR & (struct_data->GPIO_PIN_B)) == 0 ? 0 : 1);
  7.     /* Check difference */
  8.     if (struct_data->lastPinAStatus != readStateA)
  9.     {
  10.         struct_data->lastPinAStatus = readStateA;
  11.         if (struct_data->lastPinAStatus == 0)
  12.         {
  13.             if (readStateB == 1)
  14.             {
  15.                 if(struct_data->encoderCounter > struct_data->minRotationValue)
  16.                 {
  17.                     struct_data->encoderCounter--;
  18.                     struct_data->rotationFromStart++;
  19.                     moveFlag = 1;
  20.                 }
  21.                 else if(struct_data->makeRound == 1)
  22.                 {
  23.                     struct_data->encoderCounter = struct_data->maxRotationValue;
  24.                 }
  25.             }
  26.             else
  27.             {
  28.                 if(struct_data->encoderCounter < struct_data->maxRotationValue)
  29.                 {
  30.                     struct_data->encoderCounter++;
  31.                     moveFlag = 2;
  32.                     struct_data->rotationFromStart++;
  33.                 }
  34.                 else if(struct_data->makeRound == 1)
  35.                 {
  36.                     struct_data->encoderCounter = struct_data->minRotationValue;
  37.                 }
  38.             }
  39.         }
  40.     }
  41. }

W pętli while wykonuje przesuniecie kursowa w zależności od przekręcenia enkodera:

  1. void checkEncoder(void)
  2. {
  3.     if(moveFlag == 1)
  4.     {
  5.         MouseSend(0x00, -10, -10, 0x00);
  6.         moveFlag = 0;
  7.     }
  8.     else if(moveFlag == 2)
  9.     {
  10.         MouseSend(0x00, +10, +10, 0x00);
  11.         moveFlag = 0;
  12.     }
  13. }

Opis funkcji USB:

Całą procedurę ustawienia parametrów USB wykonałem przez CubeMx. Dzięki temu została utworzona funkcja uruchamiająca część sprzętową:

  1. void MX_USB_DEVICE_Init(void)
  2. {
  3.   /* Init Device Library,Add Supported Class and Start the library*/
  4.   USBD_Init(&hUsbDeviceFS, &FS_Desc, DEVICE_FS);
  5.   USBD_RegisterClass(&hUsbDeviceFS, &USBD_HID);
  6.   USBD_Start(&hUsbDeviceFS);
  7. }

Tutaj uruchamiany jest urządzenie, biblioteki, po czym całość jest uruchamiana. Po niej w części głównej programu trzeba dołożyć pewne opóźnienie, które jest konieczne aby urządzenie zostało podłączone do komputera (HAL_Delay(5000); w zupełności wystarczy).

Poniżej opis deskryptorów dla myszki oraz klawiatury:

  1.         0x05, 0x01,                    // USAGE_PAGE (Generic Desktop)
  2.         0x09, 0x06,                    // USAGE (Keyboard)
  3.         0xa1, 0x01,                    // COLLECTION (Application)
  4.         0x85, 0x01,                    //   REPORT_ID (1)
  5.         0x05, 0x07,                    //   USAGE_PAGE (Keyboard)
  6.         0x19, 0xe0,                    //   USAGE_MINIMUM (Keyboard LeftControl)
  7.         0x29, 0xe7,                    //   USAGE_MAXIMUM (Keyboard Right GUI)
  8.         0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
  9.         0x25, 0x01,                    //   LOGICAL_MAXIMUM (1)
  10.         0x75, 0x01,                    //   REPORT_SIZE (1)
  11.         0x95, 0x08,                    //   REPORT_COUNT (8)
  12.         0x81, 0x02,                    //   INPUT (Data,Var,Abs)
  13.         0x95, 0x01,                    //   REPORT_COUNT (1)
  14.         0x75, 0x08,                    //   REPORT_SIZE (8)
  15.         0x81, 0x03,                    //   INPUT (Cnst,Var,Abs)
  16.         0x95, 0x06,                    //   REPORT_COUNT (6)
  17.         0x75, 0x08,                    //   REPORT_SIZE (8)
  18.         0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
  19.         0x25, 0xFF,                    //   LOGICAL_MAXIMUM (101)
  20.         0x05, 0x07,                    //   USAGE_PAGE (Keyboard)
  21.         0x19, 0x00,                    //   USAGE_MINIMUM (Reserved (no event indicated))
  22.         0x29, 0x65,                    //   USAGE_MAXIMUM (Keyboard Application)
  23.         0x81, 0x00,                    //   INPUT (Data,Ary,Abs)
  24.         0xc0,                          // END_COLLECTION
  25.         0x05, 0x01,                    // USAGE_PAGE (Generic Desktop)
  26.         0x09, 0x02,                    // USAGE (Mouse)
  27.         0xa1, 0x01,                    // COLLECTION (Application)
  28.         0x09, 0x01,                    //   USAGE (Pointer)
  29.         0xa1, 0x00,                    //   COLLECTION (Physical)
  30.         0x85, 0x02,                    //     REPORT_ID (2)
  31.         0x05, 0x09,                    //     USAGE_PAGE (Button)
  32.         0x19, 0x01,                    //     USAGE_MINIMUM (Button 1)
  33.         0x29, 0x03,                    //     USAGE_MAXIMUM (Button 3)
  34.         0x15, 0x00,                    //     LOGICAL_MINIMUM (0)
  35.         0x25, 0x01,                    //     LOGICAL_MAXIMUM (1)
  36.         0x95, 0x03,                    //     REPORT_COUNT (3)
  37.         0x75, 0x01,                    //     REPORT_SIZE (1)
  38.         0x81, 0x02,                    //     INPUT (Data,Var,Abs)
  39.         0x95, 0x01,                    //     REPORT_COUNT (1)
  40.         0x75, 0x05,                    //     REPORT_SIZE (5)
  41.         0x81, 0x03,                    //     INPUT (Cnst,Var,Abs)
  42.         0x05, 0x01,                    //     USAGE_PAGE (Generic Desktop)
  43.         0x09, 0x30,                    //     USAGE (X)
  44.         0x09, 0x31,                    //     USAGE (Y)
  45.         0x09, 0x38,                    //     USAGE (Wheel)
  46.         0x15, 0x81,                    //     LOGICAL_MINIMUM (-127)
  47.         0x25, 0x7f,                    //     LOGICAL_MAXIMUM (127)
  48.         0x75, 0x08,                    //     REPORT_SIZE (8)
  49.         0x95, 0x03,                    //     REPORT_COUNT (3)
  50.         0x81, 0x06,                    //     INPUT (Data,Var,Rel)
  51.         0xc0,                          //   END_COLLECTION
  52.         0xc0,                          // END_COLLECTION

Deskryptor jest po prostu tablicą danych przechowującą informacje o dekodowaniu przesłanych danych. Zawiera on informacje o tym ile pakietów ma być interpretowanych, jaki jest ich rozmiar oraz znaczenie każdego bajtu czy bitu.

Przesłanie pojedynczej ramki danych dla klawiatury: 

  1. void KeyboardWriteData(uint8_t SpecialKey, uint8_t FirstKey, uint8_t SecondKey,
  2.                                 uint8_t ThirdKey, uint8_t FourthKey, uint8_t FifthKey, uint8_t SixthKey)
  3. {
  4.     uint8_t buff[9] = {0, 0, 0, 0, 0, 0, 0, 0, 0}; /* 9 bytes long report */
  5.     int i = 0;
  6.     buff[0] = 0x01;
  7.     buff[1] = SpecialKey;
  8.     buff[2] = 0x00;
  9.     buff[3] = FirstKey;
  10.     buff[4] = SecondKey;
  11.     buff[5] = ThirdKey;
  12.     buff[6] = FourthKey;
  13.     buff[7] = FifthKey;
  14.     buff[8] = SixthKey;
  15.     USBD_HID_SendReport(&hUsbDeviceFS, buff, 9);
  16.     HAL_Delay(100);
  17.     buff[0] = 0x01;
  18.     for(i=1;i<9;i++)
  19.     {
  20.         buff[i] = 0x00;
  21.     }
  22.     USBD_HID_SendReport(&hUsbDeviceFS, buff, 9);
  23.     HAL_Delay(50);
  24. }

W funkcji można podać 1 klawiszy funkcyjny oraz 6 klawiszy zwykłych.

Aby wysłać całego stringa wykorzystuje taką funkcję:

  1. void KeyboardSendString(const char *pointerToString){
  2.     while(*pointerToString){
  3.         KeyboardSendSingleChar(*pointerToString);
  4.         pointerToString++;
  5.     }
  6. }
  7. void KeyboardSendSingleChar(uint8_t charToSend)
  8. {
  9.     int charCode;
  10.     if( charToSend > 128 ){ charToSend -=128; }
  11.     charCode = _asciimap[charToSend];
  12.     keyBoardReport.idenID = 1;
  13.     keyBoardReport.keycode[0]=charCode&0x7F;
  14.     keyBoardReport.keycode[1]=0;
  15.     if (charCode & 0x80) { keyBoardReport.specKey |= 0x02; }
  16.     USBD_HID_SendReport(&hUsbDeviceFS, (uint8_t *)&keyBoardReport, sizeof(keyBoardReport));
  17.     HAL_Delay(30);
  18.     memset(keyBoardReport.keycode, 0 , sizeof(keyBoardReport.keycode));
  19.     keyBoardReport.specKey = 0;
  20.     USBD_HID_SendReport(&hUsbDeviceFS, (uint8_t *)&keyBoardReport, sizeof(keyBoardReport));
  21.     HAL_Delay(30);
  22. }

W funkcji powyżej pobierany jest wskaźnik do przesyłanego tekstu, a następnie funkcja przechodzi po wszystkich elementach ciągu i wysyła raport z jednym znakiem do komputera.

Przesłanie danych dla myszki:

  1. void MouseSend(uint8_t Btn, int8_t OsX, int8_t OsY, uint8_t Scroll)
  2. {
  3.     uint8_t buff[5] = {0, 0, 0, 0, 0};
  4.     buff[0] = 0x02;
  5.     //Left 0x01 | Right 0x02 | Midle 0x04
  6.     buff[1] = Btn;
  7.     buff[2] = OsX;
  8.     buff[3] = OsY;
  9.     buff[4] = Scroll;
  10.     HAL_Delay(50);
  11.     USBD_HID_SendReport(&hUsbDeviceFS, buff, 5);
  12.     buff[0] = 0x02;
  13.     buff[1] = 0x00;
  14.     buff[2] = 0x00;
  15.     buff[3] = 0x00;
  16.     buff[4] = 0x00;
  17.     USBD_HID_SendReport(&hUsbDeviceFS, buff, 5);
  18. }

Aby przesłać raport do myszki należy wprowadzić dane dla przycisku, przesunięcia w osiach oraz przesunięcie za pomocą rolki.

Wykorzystanie mikrokontrolera STM32F4 tylko do przedstawionych w poście funkcji jest dużym przerostem formy nad treścią, do tego celu spokojnie można użyć Arduino Pro Micro. Natomiast wykorzystanie STM'a pozwoli na bardzo łatwe rozbudowanie projektu np. o komunikację po TCP, serwer HTTP do ustawiania parametrów, większą ilość przycisków, czy czujniki pomiarowe. 

Cały projekt można pobrać z dysku Google pod tym linkiem.