Inteligentna rękawiczka dla rowerzystów

Inteligentna rękawiczka dla rowerzystów

Lato to doskonały okres na jazdę rowerem i warto przy tym zadbać o bezpieczeństwo, szczególnie wieczorem i w nocy. Rowery nie są wyposażane w kierunkowskazy, ale ich dodanie poprawia widoczność pojazdu na drodze, zwiększając tym samym bezpieczeństwo, nie tylko rowerzysty.

Rower, zgodnie z przepisami (rozporządzenie Ministra Transportu z 6.06.2013 w sprawie warunków technicznych pojazdów oraz ich niezbędnego wyposażenia, Dz. Ustaw 2013 poz. 951.) powinien posiadać: co najmniej jedno światło barwy białej lub żółtej, skierowane do przodu; co najmniej jedno światło odblaskowe barwy czerwonej, o kształcie innym niż trójkąt; co najmniej jedno światło pozycyjne barwy czerwonej, skierowane do tyłu. Nie ma ani słowa o kierunkowskazach, które w warunkach drogowych, szczególnie w mieście, są bardzo przydatne.

Rowerzysta, na ogół, sygnalizuje chęć skrętu poprzez wyciągnięcie ręki w odpowiednim kierunku, jednakże gest ten jest słabo widoczny, szczególnie przy niedostatecznym oświetleniu itp. Problem ten rozwiązuje sygnalizacja manewru kierunkowskazami. Autor opisanego poniżej projektu stworzył właśnie taki system oświetlenia. Składa się on ze specjalnych rękawiczek z sensorami gestów oraz panelu LED, montowanego np. na plecaku, bądź plecach, który wyświetla informacje o kierunku jazdy roweru.

Inspiracją do stworzenia projektu był fakt, że autor dojeżdża codziennie do pracy rowerem. Byłaby to duża przyjemność, gdyby nie to, że mieszka on w jednym z najbardziej zatłoczonych miast Francji, gdzie wypadki między samochodami i rowerzystami zdarzają się dość często. Problem pogłębia brak ścieżek rowerowych i konieczność poruszania się ulicami. Projekt ma pomóc użytkownikom dróg w lepszej komunikacji. „Z mojego punktu widzenia większość niedomówień, jakie widzę między użytkownikami dróg, wynika z tego, że niektórzy użytkownicy dróg źle interpretują zachowanie innych” – opisuje autor, mówiąc o projekcie. „Za pomocą tego urządzenia chcę sprawić, aby użytkownicy dróg lepiej się rozumieli: strzałki wskazują kierunek skrętu, a dodatkowo można wyświetlać tekst”.

Słowo „inteligentna” w nazwie projektu znalazło się nieprzypadkowo. W systemie zastosowano bowiem techniki uczenia maszynowego – sztucznej inteligencji. A rękawiczka? Projekt powstał zimą, więc naturalne było zintegrowanie detektora gestów z zimowymi rękawiczkami. Autor jednak szybko zdał sobie sprawę, że to nie najlepszy pomysł. W miejscu gdzie mieszka, latem jest dosyć gorąco, więc rękawiczka byłaby niekomfortowa. Dlatego cały system finalnie umieszczono w obudowie, montowanej na dłoni, ale nazwa projektu pozostała taka sama.

Zasada działania systemu

Układ rękawiczki bazuje na płytce z rodziny Arduino, która zbiera dane z żyroskopu i akcelerometru. Oprogramowanie Arduino wykorzystuje algorytm uczenia maszynowego, osadzonego na frameworku tinyML, który jest zoptymalizowaną wersją TensorFlow Lite, przeznaczoną do pracy na platformach wbudowanych o ultraniskim poborze energii, takich jak Arduino. Algorytm umożliwia także rozpoznawanie gestów ręki: każdy jej ruch jest analizowany i klasyfikowany (ręka przechylona w lewo, prawo, przód, tył itp.).

Sygnał z Arduino, odpowiedzialny za rozpoznawanie gestów, jest przesyłany przez Bluetooth Low Energy (BLE) do innego mikrokontrolera podłączonego do matrycy diod elektroluminescencyjnych (LED), umieszczonej np. na plecaku. Matryca wyświetla wzory zależne od gestów użytkownika, aby inni użytkownicy drogi wiedzieli co zamierza rowerzysta. Dla przykładu, układ może wyświetlać strzałki w prawo lub w lewo, sygnalizując zamiar skrętu, ale może także wyświetlać tekst.

Projekt inspirowany jest dwoma innymi rozwiązaniami. Jak często bywa z systemami tworzonymi w ekosystemie Arduino, autor nie zaczynał od zera, ale skorzystał z doświadczeń innych, aby pójść o krok dalej i zrobić coś bardziej zaawansowanego. Pierwszym projektem, jest system rozpoznawania gestów na platformie Arduino Nano 33 BLE SENSE, który opisano tutaj: https://bit.ly/2Yod3km. Drugim nie jest konkretny projekt, ale koncepcja paneli LED dla rowerzystów. Istnieje wiele tego rodzaju projektów. Niektóre są plecakami ze zintegrowaną matrycą LED, inne składają się z matrycy, którą można przymocować w dowolnym miejscu. We wszystkich przypadkach panele sterowane są za pomocą klasycznego pilota.

Wymagane elementy

Do stworzenia obudów poszczególnych elementów, potrzebny będzie dostęp do drukarki 3D. Wszystkie elementy drukowane są z PLA, więc drukarki dostępne na rynku powinny sobie poradzić. Dodatkowo, do skręcenia i montażu obudowy potrzebne będą śrubki i nakrętki M3 oraz rzep (obie strony).

Spośród komponentów elektronicznych potrzebne będą:

  • moduł Arduino Nano 33 BLE SENSE,
  • inny moduł z wbudowanym interfejsem BLE (Arduino Nano 33 BLE, Arduino 33 BLE SENSE, Arduino NANO 33 IOT, ESP32, etc.) – autor zdecydował się na zastosowanie ESP32,
  • adresowalny pasek LED (np. WS2812B) – autor zastosował w swoim projekcie 160 diod, do budowy macierzy 20×8 LED,
  • poczwórny translator poziomów (3,3 V do 5 V) – 74AHCT125 lub analogiczny układ z elementów dyskretnych,
  • kondensator 1000 μF.
  • trzy przyciski SPDT,
  • płytka uniwersalna i cienki kabelek do połączeń (np. kynar),
  • bateria 9 V z stosownym klipsem,
  • powerbank z wyjściem USB.

Na rynku jest wiele ciekawych modułów Arduino, jak pokazano na fotografii 1, ale autor zdecydował się na zastosowanie Arduino Nano 33 BLE SENSE, ponieważ jako jedyna posiada wbudowane, odpowiednie czujniki i obsługuje framework Tensorflow Lite. Co ważne, wszystkie płytki z rodziny Arduino Nano 33 (IOT, BLE oraz BLE SENSE) posiadają interfejs Bluetooth, dzięki czemu mogą sprawdzić się jako kontroler matrycy LED, odbierający sygnały Bluetooth.

Fotografia 1. Przykładowe moduły z ekosystemu Arduino

Dodatkowo, przed przystąpieniem do realizacji projektu, warto zapoznać się z podstawami uczenia maszynowego na Arduino. Można zacząć od kursu znajdującego się tutaj: https://bit.ly/2Yod3km. Znajduje się tam wprowadzenie do klasyfikacji gestów z wykorzystaniem algorytmów AI.

Rękawiczka

Ogólny plan pracy nad urządzeniem, z podziałem na etapy, pokazano na rysunku 2. Obejmuje on tak pracę nad sprzętem (elektroniką i jej integracja z rękawiczka oraz obudową), jak i prace nad inteligentnym oprogramowaniem do sterowania światłami.

Rysunek 2. Plan pracy, jaki przyjął autor projektu

Układ elektroniczny, który instaluje się w rękawiczce, jest bardzo prosty. Składa się z trzech elementów (rysunek 3): modułu Arduino Nano 33 BLE SENSE, baterii 9 V do zasilania oraz włącznika (autor zastosował hebelkowy, ale dowolny przełącznik w konfiguracji SPDT będzie się tutaj nadawał). Dzięki temu, że wszystkie wymagane sensory (żyroskop i akcelerator) zintegrowane są na płytce Arduino, nie ma potrzeby podłączania dodatkowych czujników do płytki, co upraszcza cały układ.

Rysunek 3. Schemat elektroniki w rękawiczce

Obudowa dla wykrywacza gestów wykonana została w całości w druku 3D. Składa się z dwóch plastikowych, wydrukowanych elementów i paska z rzepem, który służy do zamontowania urządzenia na dłoni. Obudowę pokazano na fotofrafii 4.

Fotografia 4. Obudowa sensora

Pierwsza żółta część zawiera płytkę Arduino, baterię i przełącznik. Autor dodał wycięcie, które pozwala na ładowanie zastosowanego akumulatora (ogniwo w formie baterii 9 V, ładowane poprzez microUSB). Wycięcia pozwalają również na programowanie modułu Arduino bez konieczności rozbierania obudowy. W części czarnej, przygotowane zostały podobne otwory jak w pierwszej części obudowy. Dodatkowo jest też otwór w miejscu, gdzie znajduje się dioda LED RGB na płytce. Dzięki temu widoczne jest światło diody, mimo że układ zamknięty jest w obudowie.

Algorytm

Zbieranie danych

Uczenie algorytmu musi zacząć się od zebrania danych i stworzenia tzw. zbioru uczącego. Wykorzystany zostanie sam sensor, z wgranym specjalnym oprogramowaniem, które pokazano na listingu 1.

Listing 1. Szkic Arduino IDE, służący do zbierania danych na temat gestów do zbioru uczącego

#include <Arduino_LSM9DS1.h>

const float gyroscopeThreshold = 300;
const int numSamples = 64;
int samplesRead = numSamples;
int nbGesture = 0;

void setup() {
Serial.begin(9600);
while (!Serial);

if (!IMU.begin()) {
Serial.println("Failed to initialize IMU!");
while (1);
}

pinMode(22, OUTPUT);
pinMode(23, OUTPUT);
pinMode(24, OUTPUT);
digitalWrite(22, HIGH);
digitalWrite(23, HIGH);
digitalWrite(24, HIGH);

// wypisanie nagłówka
Serial.println("aX,aY,aZ,gX,gY,gZ");
}

void loop() {
float aX, aY, aZ, gX, gY, gZ;

if (nbGesture <= 20) {
digitalWrite(22, LOW);
}
if (nbGesture >= 21 && nbGesture <= 40) {
digitalWrite(22, HIGH);
digitalWrite(23, LOW);
}
if (nbGesture >= 41 && nbGesture <= 60) {
digitalWrite(23, HIGH);
digitalWrite(24, LOW);
}
if (nbGesture >= 61 && nbGesture <= 80) {
digitalWrite(24, HIGH);
digitalWrite(22, LOW);
}
if (nbGesture >= 81 && nbGesture <= 101) {
digitalWrite(22, HIGH);
digitalWrite(23, LOW);
}
if (nbGesture >= 101 && nbGesture <= 120) {
digitalWrite(23, HIGH);
digitalWrite(24, LOW);
}

if (nbGesture >= 121) {
digitalWrite(22, LOW);
digitalWrite(23, LOW);
digitalWrite(24, LOW);
delay(1000000);
}

// oczekiwanie na ruch
while (samplesRead == numSamples) {
if (IMU.gyroscopeAvailable()) {
// odczyt danych przyspieszenia
IMU.readGyroscope(gX, gY, gZ);

// suma wartości bezwzględnych
float gSum = fabs(gX) + fabs(gY) + fabs(gZ);

// sprawdzenie, czy wartość jest powyżej progu
if (gSum >= gyroscopeThreshold) {
// reset licznika ilości odczytanych próbek
samplesRead = 0;
break;
}
}
}

// sprawdzenie, czy wszystkie wymagane próbki zostały odczytane
while (samplesRead < numSamples) {
// sprawdzenie, czy dostępne są nowe dane
if (IMU.accelerationAvailable() && IMU.gyroscopeAvailable()) {
// odczyt nowych danych
IMU.readAcceleration(aX, aY, aZ);
IMU.readGyroscope(gX, gY, gZ);
samplesRead++;
// wypisanie danych w formacie CSV
Serial.print(aX, 3);
Serial.print(',');
Serial.print(aY, 3);
Serial.print(',');
Serial.print(aZ, 3);
Serial.print(',');
Serial.print(gX, 3);
Serial.print(',');
Serial.print(gY, 3);
Serial.print(',');
Serial.print(gZ, 3);
Serial.println();

if (samplesRead == numSamples) {
// dodanie pustej linii po ostatniej próbce
Serial.println();
nbGesture++;
digitalWrite(22, LOW);
digitalWrite(23, LOW);
digitalWrite(24, LOW);
delay(500);
digitalWrite(22, HIGH);
digitalWrite(23, HIGH);
digitalWrite(24, HIGH);
delay(500);
}
}
}
}

Program posiada prekonfigurowany próg wartości żyroskopu. Po jego przekroczeniu, Arduino zaczyna przesyłać pomiary do komputera, celem rejestracji. Szkic zapala diodę LED na inny kolor co 20 ruchów. W ten sposób wiadomo, kiedy zmieniać wykonywany gest (potrzebne jest wiele powtórzeń tego samego gestu, aby stworzyć zbiór uczący).

W czasie akwizycji nagrywano następujące gesty:

  • ramię skierowane w lewo (typowy gest dla rowerzystów wskazujący skręt w lewo),
  • naciśnięcie hamulca (ruch palców, sięgających do rączki hamulca na kierownicy),
  • pochylenie dłoni do tyłu,
  • pochylenie dłoni do przodu,
  • pochylenie dłoni w lewo,
  • pochylenie dłoni w prawo.

Nic oczywiście nie stoi na przeszkodzie, aby uzupełnić powyższy zestaw o kolejne gesty.

Po zarejestrowaniu wszystkich gestów, ostatnią rzeczą do zrobienia jest skopiowanie danych wyświetlanych na monitorze portu szeregowego i zapisanie ich w plikach z rozszerzeniem .csv (wartości rozdzielone przecinkami). Przykładowy wykres odczytów z żyroskopu dla 20 powtórzeń danego gestu, pokazano na rysunku 5.

Rysunek 5. Przykładowe dane zebrane dla trzech różnych gestów z żyroskopu

Trening

Do szkolenia algorytmu wykorzystany został framework tinyML. Opis szkolenia znacznie wykracza poza ramy tego artykułu. Cały kurs znaleźć można tutaj: https://bit.ly/3dvwfAJ. Autor zmodyfikował tylko kilka rzeczy, względem materiałów z poradnika.

W „Graph Data (optional)”, zmieniona została nazwa pliku na jeden z przygotowanych wcześniej:

filename = "Arm_left.csv"

Następnie należy zmodyfikować poniższy wiersz, aby wykreślić tylko dane z żyroskopu:

#index = range(1, len (df [‘aX’]) + 1)
index = range(1, len (df [‘gX’]) + 1)

W kolejnym kroku należy ująć w komentarz następujące wiersze, aby wyglądały następująco (ponownie po to, by nie używać danych z akcelerometru):

#plt.plot(index, df[‘aX’], ‘g.’, label=’x’, linestyle=’solid’, marker=’,’)
#plt.plot(index, df[‘aY’], ‘b.’, label=’y’, linestyle=’solid’, marker=’,’)
plt.plot(index, df[‘aZ’], ‘r.’, label=’z’, linestyle=’solid’, marker=’,’)
#plt.title("Acceleration")
#plt.xlabel("Sample #")
#plt.ylabel("Acceleration (G)")
#plt.legend()
#plt.show()

W „Parse and prepare the data” dodać należy wszystkie nazwy, których użyto do opisu zebranych danych, zamiast przykładowych wpisów:

#GESTURES = ["punch", "flex",]
GESTURES = ["Arm_left", "Brake", "Hand_back-tilt", "Hand_front-tilt", "Hand_left-tilt", "Hand_right-tilt"]

Finalnie, zmienić należy ilość próbek na gest, jeśli taką samą zmianę dokonano również w szkicu Arduino:

#SAMPLES_PER_GESTURE = 119
SAMPLES_PER_GESTURE = 64

Ostatnią rzeczą do zmiany jest ujęcie w komentarz przyspieszenia w tensorze wejściowym algorytmu do treningu systemu:

tensor += [
#(df[‘aX’][index] + 4) / 8,
#(df[‘aY’][index] + 4) / 8,
#(df[‘aZ’][index] + 4) / 8,
(df[‘gX’][index] + 2000) / 4000,
(df[‘gY’][index] + 2000) / 4000,
(df[‘gZ’][index] + 2000) / 4000
]

Po wprowadzeniu zmian i uruchomieniu całego skryptu, można pobrać gotowy, wyszkolony model, który będzie działał jako klasyfikator wykonywanych gestów. Jeśli zajrzymy do pobranego pliku nagłówkowego (model.h), zobaczymy macierz liczb. Teraz pozostaje tylko zaimplementować model w szkicu Arduino, odpowiedzialnym za sterowanie systemem.

Implementacja Arduino

Ostateczny program kontrolera inteligentnej rękawiczki, to połączenie dwóch innych programów. Pierwszym jest znajdujący się w bibliotece ArduinoBLE przykład, opisany jako LED. Drugi to klasyfikator IMU_Classifier, pochodzacy z przykładów wykorzystania Tensorflow Lite, które obejrzeć można w repozytorium: https://bit.ly/3fQIAkC. Dokładny opis tych programów wykracza poza ramy tego artykułu, ale warto się z nimi zapoznać, czekając aż Arduino IDE skompiluje i załaduje szkic (listing 2) do pamięci mikrokontrolera.

Listing 2. Program kontrolera inteligentnej rękawiczki

#include <ArduinoBLE.h>
#include <Arduino_LSM9DS1.h>
#include <TensorFlowLite.h>
#include <tensorflow/lite/experimental/micro/kernels/all_ops_resolver.h>
#include <tensorflow/lite/experimental/micro/micro_error_reporter.h>
#include <tensorflow/lite/experimental/micro/micro_interpreter.h>
#include <tensorflow/lite/schema/schema_generated.h>
#include <tensorflow/lite/version.h>
#include "model.h" // nasz model
const float gyroscopeThreshold = 300;
const int numSamples = 64;
int samplesRead = numSamples;
// zmienne globalne wykorzystywane przez TensorFlow Lite (TFLM)
tflite::MicroErrorReporter tflErrorReporter;
// pobieranie wszystkich opertorów TFLM; można zastąpić to
// pobieraniem tylko wybranych, by zmniejszyć skompilowanego programu
tflite::ops::micro::AllOpsResolver tflOpsResolver;
const tflite::Model* tflModel = nullptr;
tflite::MicroInterpreter* tflInterpreter = nullptr;
TfLiteTensor* tflInputTensor = nullptr;
TfLiteTensor* tflOutputTensor = nullptr;
// utworzenie statycznego bufora w pamięci dla TFLM
// jego wielkość jest zależna od modelu
constexpr int tensorArenaSize = 8 * 1024;
byte tensorArena[tensorArenaSize];
// macierz do mapowania gestów po indeksach
const char* GESTURES[] = {
"Left arm",
"Brake",
"Hand front rotation",
"Hand back rotation",
"Hand left",
"Hand right"
};
#define NUM_GESTURES (sizeof(GESTURES) / sizeof(GESTURES[0]))
int nbGesture = 0;
int oldNbGesture = 0;
int gestureTriggered;


void setup() {
Serial.begin(115200);
if (!IMU.begin()) {
Serial.println("Failed to initialize IMU!");
while (1);
}
// wypisanie prędkości próbek IMU
Serial.print("Accelerometer sample rate = ");
Serial.print(IMU.accelerationSampleRate());
Serial.println(" Hz");
Serial.print("Gyroscope sample rate = ");
Serial.print(IMU.gyroscopeSampleRate());
Serial.println(" Hz");
Serial.println();
// pobranie reprezentacji bitowej modelu TFLM
tflModel = tflite::GetModel(model);
if (tflModel->version() != TFLITE_SCHEMA_VERSION) {
Serial.println("Model schema mismatch!");
while (1);
}
// utowrzenie interoretera do uruchomienia modelu
tflInterpreter = new tflite::MicroInterpreter(tflModel, tflOpsResolver, tensorArena, tensorArenaSize, &tflErrorReporter);
// alokacja pamięci dla tensorów wejściowego i wyjściowego modelu
tflInterpreter->AllocateTensors();
// pobranie wskaźników na wejściowi i wyjściowy tensor modelu
tflInputTensor = tflInterpreter->input(0);
tflOutputTensor = tflInterpreter->output(0);

pinMode(22, OUTPUT);
pinMode(23, OUTPUT);
pinMode(24, OUTPUT);
digitalWrite(22, HIGH);
digitalWrite(23, HIGH);
digitalWrite(24, HIGH);

// inicjalizacja transceivera BLE
BLE.begin();
Serial.println("BLE Central - LED control");

// start skanowania peryferiów
BLE.scan();
}

void loop() {
BLEDevice peripheral = BLE.available();
if (peripheral) {
// odkryto urządzenie wypisz adres, nazwę lokalbą i serwer
Serial.print("Found ");
Serial.print(peripheral.address());
Serial.print(" '");
Serial.print(peripheral.localName());
Serial.print("' ");
Serial.print(peripheral.advertisedServiceUuid());
Serial.println();
if (peripheral.localName() != "MySmartglove") {
return;
}
BLE.stopScan();
// połącz z peryferiami
Serial.println("Connecting ...");

if (peripheral.connect()) {
Serial.println("Connected");
} else {
Serial.println("Failed to connect!");
return;
}

// atrybuty odkrytych peryferiów
Serial.println("Discovering attributes ...");
if (peripheral.discoverAttributes()) {
Serial.println("Attributes discovered");
} else {
Serial.println("Attribute discovery failed!");
peripheral.disconnect();
return;
}

// pobierz parametry LEDów
BLECharacteristic ledCharacteristic = peripheral.characteristic("e4297ee1-8c88-11ea-bc55-0242ac130003");

if (!ledCharacteristic) {
Serial.println("Peripheral does not have LED characteristic!");
peripheral.disconnect();
return;
} else if (!ledCharacteristic.canWrite()) {
Serial.println("Peripheral does not have a writable LED characteristic!");
peripheral.disconnect();
return;
}

while (peripheral.connected()) {
float aX, aY, aZ, gX, gY, gZ;
// czekaj na zauważalny ruch
while (samplesRead == numSamples) {
if (IMU.gyroscopeAvailable()) {
IMU.readGyroscope(gX, gY, gZ);
// suma wartości bezwzględnych
float gSum = fabs(gX) + fabs(gY) + fabs(gZ);
// sprawdź czy suma jest powyżej progu
if (gSum >= gyroscopeThreshold) {
// zresetuj licznik odebranych próbek
samplesRead = 0;
break;
}
}
}
// sprawdź, czy wszystkie wymagane próbki od czasu ostatniej aktywacji zostały odebrane
oldNbGesture = nbGesture;
while (samplesRead < numSamples) {
// sprawdź, czy dostępne są nowe dane z akcelerometrów i żyroskopu
if (IMU.accelerationAvailable() && IMU.gyroscopeAvailable()) {
// odczytaj dane z akcelerometru i żyroskopu
IMU.readAcceleration(aX, aY, aZ);
IMU.readGyroscope(gX, gY, gZ);
// normalizacja danych z IMU 0..1
// tensor wejściowy
tflInputTensor->data.f[samplesRead * 6 + 0] = (aX + 4.0) / 8.0;
tflInputTensor->data.f[samplesRead * 6 + 1] = (aY + 4.0) / 8.0;
tflInputTensor->data.f[samplesRead * 6 + 2] = (aZ + 4.0) / 8.0;
tflInputTensor->data.f[samplesRead * 6 + 3] = (gX + 2000.0) / 4000.0;
tflInputTensor->data.f[samplesRead * 6 + 4] = (gY + 2000.0) / 4000.0;
tflInputTensor->data.f[samplesRead * 6 + 5] = (gZ + 2000.0) / 4000.0;
samplesRead++;
if (samplesRead == numSamples) {
// inferencja
TfLiteStatus invokeStatus = tflInterpreter->Invoke();
if (invokeStatus != kTfLiteOk) {
while (1);
return;
}
// odczytaj nazwę gestu z tabeli dla modelu
for (int i = 0; i < NUM_GESTURES; i++) {
Serial.print(GESTURES[i]);
Serial.print(": ");
Serial.println(tflOutputTensor->data.f[i], 6);
if ((tflOutputTensor->data.f[i]) > 0.6) {
gestureTriggered = i;
nbGesture++;
}
}
}
}
}
Serial.print("The gesture is :");
Serial.println(GESTURES[gestureTriggered]);
if (oldNbGesture != nbGesture) {
if (gestureTriggered == 0) {
ledCharacteristic.writeValue((byte)0x00);
colors(0);
}
if (gestureTriggered == 2) {
ledCharacteristic.writeValue((byte)0x02);
colors(2);
}
if (gestureTriggered == 3) {
ledCharacteristic.writeValue((byte)0x03);
colors(3);
}
if (gestureTriggered == 4) {
ledCharacteristic.writeValue((byte)0x04);
colors(4);
}
if (gestureTriggered == 5) {
ledCharacteristic.writeValue((byte)0x05);
colors(5);
}
}
}
// peryferia odłączone, skanuj ponownie
BLE.scan();
}
}

int colors (int i) {
if (i == 0) {
for (int it1 = 0; it1 <= 1; it1++) {
digitalWrite(22, LOW);
digitalWrite(23, LOW);
delay(500);
digitalWrite(22, HIGH);
digitalWrite(23, HIGH);
delay(500);
}
}
if (i == 1) {
for (int it1 = 0; it1 <= 1; it1++) {
digitalWrite(22, LOW);
delay(500);
digitalWrite(22, HIGH);
delay(500);
}
}
if (i == 2) {
digitalWrite(23, LOW);
delay(500);
digitalWrite(23, HIGH);
delay(500);
}
if (i == 3) {
digitalWrite(22, LOW);
delay(500);
digitalWrite(22, HIGH);
}
if (i == 4) {
digitalWrite(22, LOW);
delay(500);
digitalWrite(23, LOW);
delay(500);
digitalWrite(22, HIGH);
digitalWrite(23, HIGH);
}
if (i == 5) {
digitalWrite(23, LOW);
delay(500);
digitalWrite(22, LOW);
delay(500);
digitalWrite(22, HIGH);
digitalWrite(23, HIGH);
}
}

Po załadowaniu skompilowanego programu do modułu w rękawiczce, można przejść do konstrukcji części wykonawczej systemu – panelu LED, do zamontowania na plecach rowerzysty.

Panel LED

Elektronika

Autor napotkał problemy podczas przesyłania szkicu – z biblioteki ArduinoBLE do płytki Arduino Nano 33 – obsługującego macierz LED. Zdecydował się zatem na użycie modułu z układem ESP32. Płytka została podłączona jak na rysunku 6.

Rysunek 6. Schemat panelu LED

Ponieważ zarówno Arduino Nano 33 BLE SENSE, jak i ESP32 komunikują się z poziomem logicznym 3,3 V, do układu dodany został translator poziomów 3 V do 5 V (74AHCT125), aby wysterować diody LED RGB w macierzy. Dodano także kondensator 1000 μF do linii zasilania diod LED. Cały układ zasilany jest z powerbanku USB. Matryca LED (nie pokazana na schemacie) i moduł z mikrokontrolerem zasilane są z osobnych wtyczek USB w powerbanku, z uwagi na wysoki pobór prądu przez macierz LED.

Obudowa

Obudowa panelu LED jest modułowa, można dopasować jej wielkość do własnego układu diod LED. Dzięki temu można wydrukować ją nawet na małej drukarce 3D. Pliki, potrzebne do druku, są do pobrania ze strony z projektem na portalu instructables.com. Gotowa, zmontowana obudowa, wraz z diodami RGB, pokazana jest na fotografii 7.

Fotografia 7. Obudowa panelu LED RGB wraz z zainstalowanymi paskami LED-owymi

Firmware panelu LED

Podobnie jak w przypadku innych programów w urządzeniu, także ten powstał na podstawie innych, dostępnych skryptów. Autor wzorował się przykładem BLE_Write z biblioteki Arduino dla BLE ESP32 oraz „MatrixGFXDemo64” z biblioteki „FastLED NeoMatrix”. Finalny szkic, załadowany do modułu ESP32 pokazano na listingu 3.

Listing 3. Listing programu do obsługi matrycy LED na podstawie wysyłanych informacji z detektora gestów

#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
String gestureValue = "0";
int gestureNb = 0;
int old_gestureNb = 0;
BLEServer *pServer = NULL;
bool deviceConnected = false;
unsigned long previousMillis = 0;
const long interval = 1000;
// UUID wygenerowane z wykorzystaniem generatora ze strony:
// https://www.uuidgenerator.net/
#define SERVICE_UUID "e4297ee0-8c88-11ea-bc55-0242ac130003"
#define CHARACTERISTIC_UUID "e4297ee1-8c88-11ea-bc55-0242ac130003"
#define DISABLE_WHITE
#include <Adafruit_GFX.h>
#include <FastLED_NeoMatrix.h>
#include <FastLED.h>
// dithering musi być wyłączony na ESP32 (brak wsparcia)
#ifndef ESP32
#define delay FastLED.delay
#endif
#define PIN 33
#define BRIGHTNESS 64
#define mw 20
#define mh 8
#define NUMMATRIX (mw*mh)
CRGB leds[NUMMATRIX];
// definicja wielkości macierzy diod LED
FastLED_NeoMatrix *matrix = new FastLED_NeoMatrix(leds, mw, mh,
NEO_MATRIX_BOTTOM + NEO_MATRIX_RIGHT +
NEO_MATRIX_ROWS + NEO_MATRIX_ZIGZAG);

void matrix_show() {
matrix->show();
}

// This could also be defined as matrix->color(255,0,0) but those defines
// are meant to work for adafruit_gfx backends that are lacking color()
#define LED_BLACK 0

#define LED_RED_VERYLOW (3 << 11)
#define LED_RED_LOW (7 << 11)
#define LED_RED_MEDIUM (15 << 11)
#define LED_RED_HIGH (31 << 11)

#define LED_GREEN_VERYLOW (1 << 5)
#define LED_GREEN_LOW (15 << 5)
#define LED_GREEN_MEDIUM (31 << 5)
#define LED_GREEN_HIGH (63 << 5)

#define LED_BLUE_VERYLOW 3
#define LED_BLUE_LOW 7
#define LED_BLUE_MEDIUM 15
#define LED_BLUE_HIGH 31

#define LED_ORANGE_VERYLOW (LED_RED_VERYLOW + LED_GREEN_VERYLOW)
#define LED_ORANGE_LOW (LED_RED_LOW + LED_GREEN_LOW)
#define LED_ORANGE_MEDIUM (LED_RED_MEDIUM + LED_GREEN_MEDIUM)
#define LED_ORANGE_HIGH (LED_RED_HIGH + LED_GREEN_HIGH)

#define LED_PURPLE_VERYLOW (LED_RED_VERYLOW + LED_BLUE_VERYLOW)
#define LED_PURPLE_LOW (LED_RED_LOW + LED_BLUE_LOW)
#define LED_PURPLE_MEDIUM (LED_RED_MEDIUM + LED_BLUE_MEDIUM)
#define LED_PURPLE_HIGH (LED_RED_HIGH + LED_BLUE_HIGH)

#define LED_CYAN_VERYLOW (LED_GREEN_VERYLOW + LED_BLUE_VERYLOW)
#define LED_CYAN_LOW (LED_GREEN_LOW + LED_BLUE_LOW)
#define LED_CYAN_MEDIUM (LED_GREEN_MEDIUM + LED_BLUE_MEDIUM)
#define LED_CYAN_HIGH (LED_GREEN_HIGH + LED_BLUE_HIGH)

#define LED_WHITE_VERYLOW (LED_RED_VERYLOW + LED_GREEN_VERYLOW + LED_BLUE_VERYLOW)
#define LED_WHITE_LOW (LED_RED_LOW + LED_GREEN_LOW + LED_BLUE_LOW)
#define LED_WHITE_MEDIUM (LED_RED_MEDIUM + LED_GREEN_MEDIUM + LED_BLUE_MEDIUM)
#define LED_WHITE_HIGH (LED_RED_HIGH + LED_GREEN_HIGH + LED_BLUE_HIGH)

void matrix_clear() {
memset(leds, 0, sizeof(leds));
}

void ScrollText_Left() {
matrix_clear();
matrix->setTextWrap(false); // tekst nie jest zawijany, aby dobrze się przesuwał
matrix->setTextSize(1);
matrix->setRotation(0);
for (int8_t x = 0; x >= -60; x--) {
yield();
matrix_clear();
matrix->setCursor(x, 0);
matrix->setTextColor(LED_ORANGE_HIGH);
matrix->print("<<<<<<<<<<<<<<");
matrix_show();
delay(50);
}
matrix->setCursor(0, 0);
}

void ScrollText_Merci() {
matrix_clear();
matrix->setTextWrap(false); // tekst nie jest zawijany, aby dobrze się przesuwał
matrix->setTextSize(1);
matrix->setRotation(0);
for (int8_t x = 20; x >= -30; x--) {
yield();
matrix_clear();
matrix->setCursor(x, 0);
matrix->setTextColor(LED_PURPLE_HIGH);
matrix->print("Merci");
matrix_show();
delay(70);
}
matrix->setCursor(0, 0);
}

void ScrollText_Hello() {
matrix_clear();
matrix->setTextWrap(false); // tekst nie jest zawijany, aby dobrze się przesuwał
matrix->setTextSize(1);
matrix->setRotation(0);
for (int8_t x = 20; x >= -30; x--) {
yield();
matrix_clear();
matrix->setCursor(x, 0);
matrix->setTextColor(LED_CYAN_HIGH);
matrix->print("Hello!");
matrix_show();
delay(70);
}
matrix->setCursor(0, 0);
}

void ScrollText_Right() {
matrix_clear();
matrix->setTextWrap(false); // tekst nie jest zawijany, aby dobrze się przesuwał
matrix->setTextSize(1);
matrix->setRotation(0);
for (int8_t x = -60; x <= 0 ; x++) {
yield();
matrix_clear();
matrix->setCursor(x, 0);
matrix->setTextColor(LED_ORANGE_HIGH);
matrix->print(">>>>>>>>>>>>>>");
matrix_show();
delay(50);
}
matrix->setCursor(0, 0);
}

void ScrollText_Wait() {
matrix_clear();
matrix->setTextWrap(false); // tekst nie jest zawijany, aby dobrze się przesuwał
matrix->setTextSize(1);
matrix->setRotation(0);
yield();
matrix_clear();
matrix->setCursor(2, 0);
matrix->setTextColor(LED_ORANGE_HIGH);
matrix->print("o");
matrix_show();
delay(500);
yield();
matrix_clear();
matrix->setCursor(2, 0);
matrix->setTextColor(LED_ORANGE_HIGH);
matrix->print("oo");
matrix_show();
delay(500);
yield();
matrix_clear();
matrix->setCursor(2, 0);
matrix->setTextColor(LED_ORANGE_HIGH);
matrix->print("ooo");
matrix_show();
delay(500);
yield();
matrix_clear();
matrix->setCursor(0, 0);
matrix->setTextColor(LED_BLACK);
matrix_show();
delay(500);
}

void ScrollText_Stop() {
matrix_clear();
matrix->setTextWrap(false); // tekst nie jest zawijany, aby dobrze się przesuwał
matrix->setTextSize(1);
matrix->setRotation(0);
yield();
matrix_clear();
matrix->setCursor(2, 0);
matrix->setTextColor(LED_RED_HIGH);
matrix->print("!!!");
matrix_show();
delay(1000);
yield();
matrix_clear();
matrix->setCursor(0, 0);
matrix->setTextColor(LED_BLACK);
matrix->print("STOP");
matrix_show();
delay(500);
}

void ScrollText_Straight() {
uint8_t size = max(int(mw / 8), 1);
matrix->setRotation(3);
matrix->setTextSize(size);
matrix->setTextColor(LED_GREEN_HIGH);
for (int16_t x = -10; x <= 6; x++) {
yield();
matrix_clear();
matrix->setCursor(x, ((mw / 2 - size * 4) + 1));
matrix->print(">");
matrix_show();
delay(100 / size);
}
matrix->setRotation(0);
matrix->setCursor(0, 0);
matrix_show();
}

class MyServerCallbacks: public BLEServerCallbacks {
void onConnect(BLEServer* pServer) {
deviceConnected = true;
Serial.println(" deviceConnected = true;");
};
void onDisconnect(BLEServer* pServer) {
deviceConnected = false;
Serial.println(" deviceConnected = false;");
}
};

class MyCallbacks: public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *pCharacteristic) {
std::string value = pCharacteristic->getValue();
if (value.length() > 0) {
gestureValue = int(value[0]);
gestureNb++;
}
}
};

void setup() {
Serial.begin(115200);
FastLED.addLeds<NEOPIXEL, PIN>( leds, NUMMATRIX ).setCorrection(TypicalLEDStrip);
delay(1000);
matrix->begin();
matrix->setTextWrap(false);
matrix->setBrightness(BRIGHTNESS);
#ifndef DISABLE_WHITE
matrix->fillScreen(LED_WHITE_HIGH);
matrix_show();
delay(5000);
matrix_clear();
#endif
BLEDevice::init("MySmartglove");
BLEServer *pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());
BLEService *pService = pServer->createService(SERVICE_UUID);
BLECharacteristic *pCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_WRITE
);
pCharacteristic->setCallbacks(new MyCallbacks());
pService->start();
BLEAdvertising *pAdvertising = pServer->getAdvertising();
pAdvertising->start();
}

void loop() {

if (deviceConnected) {
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= interval) {
previousMillis = currentMillis;
ScrollText_Straight();
}
Serial.print("gestureValue : ");
Serial.println(gestureValue);
if (gestureNb != old_gestureNb)
{

if (gestureValue == "0" || gestureValue == "4") {
ScrollText_Left();
}
if (gestureValue == "5") {
ScrollText_Right();
}
if (gestureValue == "3") {
ScrollText_Stop();
}
if (gestureValue == "2") {
ScrollText_Merci();
}
old_gestureNb = gestureNb;
}
}
if (!deviceConnected) {
ScrollText_Wait();
pServer->startAdvertising(); // restart advertising
}
}

Podsumowanie

Nadszedł czas, aby przetestować urządzenie. Za każdym razem gdy rozpoznawany jest gest, do panelu LED wysyłany jest sygnał i wyświetlany jest wzór. Dodatkowo, na rękawiczce zapala się dioda LED, informująca o poprawnym rozpoznaniu gestu. Jeśli system nie rozpoznaje poprawnie ruchów dłonią, konieczne może być ponowne wygenerowanie modelu z nowym zbiorem uczącym, szczególnie dla problematycznych ruchów. Jak przyznaje autor, zbiór danych uczących był zbyt mały, by zapewnić w pełni niezawodny model.

Nagranie większej ilości ruchów dałoby lepszy model i mniej błędów w rozpoznawaniu gestów.

Projekt jest dobrym wstępem do świata systemów uczenia maszynowego. Dzięki wykorzystaniu otwartych narzędzi i niedrogiej płytki Arduino, każdy może stosować AI w swoim projekcie, nie tylko do klasyfikowania gestów, ale i dowolnych innych sygnałów. Tylko wyobraźnia jest tutaj granicą.

Nikodem Czechowski, EP

Źródło: https://bit.ly/3hR3WjH

Artykuł ukazał się w
Lipiec 2020
DO POBRANIA
Materiały dodatkowe
Zobacz też
Elektronika Praktyczna Plus lipiec - grudzień 2012

Elektronika Praktyczna Plus

Monograficzne wydania specjalne

Elektronik lipiec 2020

Elektronik

Magazyn elektroniki profesjonalnej

Raspberry Pi 2015

Raspberry Pi

Wykorzystaj wszystkie możliwości wyjątkowego minikomputera

Świat Radio lipiec 2020

Świat Radio

Magazyn użytkowników eteru

APA - Automatyka Podzespoły Aplikacje lipiec 2020

APA - Automatyka Podzespoły Aplikacje

Technika i rynek systemów automatyki

Elektronika Praktyczna lipiec 2020

Elektronika Praktyczna

Międzynarodowy magazyn elektroników konstruktorów

Praktyczny Kurs Elektroniki 2018

Praktyczny Kurs Elektroniki

24 pasjonujące projekty elektroniczne

Elektronika dla Wszystkich lipiec 2020

Elektronika dla Wszystkich

Interesująca elektronika dla pasjonatów