Oglądaliście „Raport mniejszości”? W filmie główny bohater obsługuje komputer przy pomocy gestów – fragmenty możecie zobaczyć na tym filmie:
W roku, gdy film ten był prezentowany, taki interface wydawał się futurystyczny i nieosiągalny, a tymczasem już w 2008 powstał prototyp interfejsu sterowanego właśnie w ten sposób:
Oczywiście skonstruowanie go wykracza poza budżet domowego projektu, ale chcę pokazać, jak można wykorzystać trochę elektroniki i komórkę, aby osiągnąć ciekawy efekt urządzenia sterowanego gestami. Być może jest to rozwiązanie mało praktyczne, ale za to będzie trochę fajerwerków :)
Części
Będziemy potrzebować następujących komponentów:
- Raspberry Pi 3;
- Sterownika PWM PCA9685 – ja korzystam z układu sygnowanego logiem Adafruit – https://nettigo.pl/products/sto-kanalowy-sterownik-serwomechanizmow-z-interfejsem-i2c;
- Układu pan-tilt – możemy kupić gotowy (koszt ok. 80 PLN – https://botland.com.pl/chwytaki-uchwyty-gimbale/2546-uchwyt-do-serw-pantilt-kit-serwa-dagu.html) lub zbudować samodzielnie; będą nam wówczas potrzebne dwa serwa modelarskie (ceny od 9 PLN wzwyż)
- Zasilanie do serw 5V – korzystam z akumulatora LiPo 2S i BEC 5V – wbudowanego w ESC do silników bezszczotkowych (bo akurat mam taki pod ręką), ale może być na przykład taki: https://abc-rc.pl/ubec-3a-5v
- Kilka przewodów ze złączami goldpin
Pierwotnie myślałem też o tym, żeby skorzystać z jakiegoś IMU (akcelerometr + żyro + magnetometr), ale potem doszedłem do wniosku, że będzie to niepotrzebnie komplikować projekt – współcześnie każdy telefon komórkowy wyposażony jest w taki moduł. Dlatego sterowanie odbywać się będzie właśnie z poziomu telefonu komórkowego – napiszemy w tym celu odpowiednią aplikację.
Schemat połączeń
Schemat możemy znaleźć na poniższym obrazku. Należy zwrócić szczególną uwagę, żeby pin VCC Raspberry (+3,3V) podłączyć do pinu VCC na płytce i nie pomylić go z V+, bo ten ostatni połączony jest z linią +5V zasilania serw – można w najgorszym przypadku uszkodzić Raspberry!
Połączenia wyglądają mniej więcej, jak na poniższym zdjęciu.
Programowanie
Część elektroniczna w przypadku tego projektu jest tą łatwiejszą – trochę więcej pracy będziemy musieli włożyć w oprogramowanie Raspbery i komórki.
Sterowanie serwami
Sterowanie serwami z poziomu Raspberry nie jest specjalnie trudnym zagadnieniem, ale można się trochę naciąć. Raspberry potrafi sprzętowo generować sygnał PWM, ale niestety tylko na jednym pinie. Co więcej, standardowa biblioteka RPi.GPIO, przy pomocy której możemy korzystać ze złącz GPIO, potrafi generować PWM tylko programowo – objawi się to zauważalnym „szarpaniem” serwa. Możemy skorzystać z innej biblioteki – wiringpi, która (o ile skrypt uruchomimy z uprawnieniami roota) potrafi już wykorzystać sprzętowe generowanie PWMa, jednak wciąż będziemy ograniczeni do jednego pinu, czyli sterować będziemy tylko jednym serwem, a potrzebujemy dwóch. Dlatego też do zrealizowania tego projektu potrzebujemy płytki sterującej serwami; alternatywnym rozwiązaniem może być wykorzystanie do tego celu Arduino, o ile tylko odpowiednio go oprogramujemy.
Aby skorzystać z układu PCA9685, musimy najpierw doinstalować odpowiednią bibliotekę. W tym celu najpierw instalujemy na Raspberry dwa pakiety:
1 2 |
sudo apt-get install python-smbus sudo apt-get install i2c-tools |
Drugi z pakietów zawiera narzędzia, które przydają się podczas pracy z urządzeniami podłączonymi poprzez szynę I2C.
Pamiętajmy oczywiście o włączeniu obsługi I2C w konfiguracji Raspberry:
Teraz możemy wywołać polecenie, które pozwoli na sprawdzić, czy moduł jest prawidłowo podłączony do Raspberry:
1 |
sudo i2cdetect -y 1 |
Powinniśmy otrzymać następujący wynik:
1 2 3 4 5 6 7 8 9 10 11 |
~ $ sudo ic2detect -y 1 0 1 2 3 4 5 6 7 8 9 a b c d e f 00: -- -- -- -- -- -- -- -- -- -- -- -- -- 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 40: 40 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 70: 70 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- |
Możemy teraz doinstalować do Pythona bibliotekę obsługi płytki PCA9685. Wpisujemy po kolei:
1 2 3 4 5 |
sudo apt-get install git build-essential python-dev cd ~ git clone https://github.com/adafruit/Adafruit_Python_PCA9685.git cd Adafruit_Python_PCA9685 sudo python setup.py install |
Alternatywnie możemy ściągnąć źródła z Internetu – nie trzeba wtedy doinstalowywać gita.
Serwer TCP/IP
Najciekawszym, moim zdaniem, sposobem zdalnego sterowania zbudowanym urządzeniem jest protokół TCP/IP – dzięki temu sterowane urządzenie może znajdować się na jednym końcu miasta, a my – na drugim. W ten sposób można skonstruować robota z ramieniem sterowanym gestami lub kamerę monitoringu, która umożliwi podgląd obrazu na ekranie telefonu. Raspberry Pi w wersji 3 ma wbudowany moduł Wifi, co jeszcze bardziej ułatwi komunikację przez Internet. Co więcej, zaprogramowanie serwera TCP w Pythonie jest wyjątkowo łatwe – chyba jak wszystko w tym języku (https://xkcd.com/353/).
Kod źródłowy serwera na Raspberry wygląda następująco:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
# -*- coding: utf-8 -*- import socket import sys import time import Adafruit_PCA9685 # Częstotliwość sterowania PWM FREQUENCY = 50 # Czas jednego cyklu CYCLE_TIME = 1 / float(FREQUENCY) # Procentowy czas cyklu dla położenia minimalnego - 0,55 ms # Można ustawić 0,5ms, ale jest to wartość graniczna - dla # niektórych serw może to być już za dużo i zajedziemy silnik SERVO_MIN_PERCENT = 0.00055 / CYCLE_TIME # Procentowy czas cyklu dla położenia maksymalnego - 2,45ms # Jak wyżej - możemy ustawić na 2,5ms SERVO_MAX_PERCENT = 0.00245 / CYCLE_TIME # Wartość minimalna dla 4096 kroków SERVO_MIN = int(4096 * SERVO_MIN_PERCENT) # Wartość maksymalna dla 4096 kroków SERVO_MAX = int(4096 * SERVO_MAX_PERCENT) # Tworzymy obiekt pwm (przyjmie domyślnie adres 0x40) pwm = Adafruit_PCA9685.PCA9685() pwm.set_pwm_freq(FREQUENCY) # Funkcja ustawia serwo na zadanym kanale na wybrany kąt (0~180 stopni) def setAngle(channel, angle): pwm.set_pwm(channel, 0, int(SERVO_MIN + (float(angle) / 180.0) * (SERVO_MAX - SERVO_MIN))) # Funkcja przetwarza komendę odebraną z klienta. Komendy są postaci: # PPPP|RRRR|YYYY; # Gdzie: # P to wartość pitch (-180~180) # R to wartość roll (-180~180) # Y to wartość yaw (-180~180) def processCommand(command): angles = command.split("|") angles = list(map(lambda x: max(-90, min(90, -int(x.strip()))) + 90, angles)) # Wyświetlamy wartości odebrane z klienta do celów debuggowania print("P: {0}, Y: {1}".format(angles[0], angles[2])) # Sterujemy serwami setAngle(14, int(angles[0] / 10) * 10) setAngle(15, int(angles[2] / 10) * 10) # Funkcja obsługuje nadchodzące połączenie def handleConnection(connection): buffer = "" while True: data = connection.recv(32) if data: buffer = buffer + data commandEnd = buffer.find(";") if (commandEnd != -1): command = buffer[0:commandEnd] buffer = buffer[commandEnd+1:] processCommand(command) else: break # Po zakończeniu połączenia ustawiamy serwa w pozycjach # zerowych setAngle(14, 90) setAngle(15, 90) # Główna funkcja programu def main(): # Tworzymy socket TCP sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # Wiążemy socket z portem 10000 server_address = ('', 10000) sock.bind(server_address) # Oczekujemy na nadchodzące połączenia sock.listen(10) try: while True: print('Waiting for connection...') connection, client_address = sock.accept() try: print('Connection accepted from: ', client_address) handleConnection(connection) finally: print('Closing connection...') connection.close() except KeyboardInterrupt: print('Ctrl+C received, closing...') # Uruchamiamy program main() |
Najnowszą wersję kodu możecie ściągnąć z repozytorium: https://gitlab.com/spook/looker-raspberry.git
Serwer UDP
Tę samą transmisję możemy przeprowadzić również poprzez protokół UDP. Tym razem serwer będzie nieco krótszy:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
# -*- coding: utf-8 -*- import socket import sys import time import Adafruit_PCA9685 # Częstotliwość sterowania PWM FREQUENCY = 50 # Czas jednego cyklu CYCLE_TIME = 1 / float(FREQUENCY) # Procentowy czas cyklu dla położenia minimalnego SERVO_MIN_PERCENT = 0.00055 / CYCLE_TIME # Procentowy czas cyklu dla położenia maksymalnego SERVO_MAX_PERCENT = 0.00245 / CYCLE_TIME # Wartość minimalna dla 4096 kroków SERVO_MIN = int(4096 * SERVO_MIN_PERCENT) # Wartość maksymalna dla 4096 kroków SERVO_MAX = int(4096 * SERVO_MAX_PERCENT) # Tworzymy obiekt pwm (przyjmie domyślnie adres 0x40) pwm = Adafruit_PCA9685.PCA9685() pwm.set_pwm_freq(FREQUENCY) def setAngle(channel, angle): pwm.set_pwm(channel, 0, int(SERVO_MIN + (float(angle) / 180.0) * (SERVO_MAX - SERVO_MIN))) def processCommand(command): angles = command.split("|") angles = list(map(lambda x: max(-90, min(90, -int(x.strip()))) + 90, angles)) print("P: {0}, Y: {1}".format(angles[0], angles[2])) setAngle(14, int(angles[0] / 10) * 10) setAngle(15, int(angles[2] / 10) * 10) def main(): # Create TCP/IP socket sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # Bind the socket to the port server_address = ('', 10000) sock.bind(server_address) print("Waiting for data...") buffer = "" try: while True: data, addr = sock.recvfrom(32); buffer = buffer + data commandEnd = buffer.find(";") if (commandEnd != -1): command = buffer[0:commandEnd] buffer = buffer[commandEnd+1:] processCommand(command) except KeyboardInterrupt: print('Ctrl+C received, closing...') # Start program main() |
Aplikacja na komórkę
Aplikację na komórkę napisałem w Javie – oczywiście jest zbyt długa, by jej kod zamieszczać tu w całości, natomiast można go ściągnąć z repozytorium: https://gitlab.com/spook/looker-android.git . Może on też być dobrą bazą dla innej aplikacji sterującej Raspberry przy pomocy TCP/IP.
Aplikacja prezentuje się następująco:
Jeżeli ktoś nie ma możliwości skompilowania aplikacji samodzielnie, załączam do artykułu plik .apk do zainstalowania na urządzeniu.
Efekt
Efekty możemy podziwiać na poniższych zdjęciach (korzystałem tam ze starszej wersji aplikacji mobilnej).
Wnioski
- IMU komórki jest wystarczający do opisanego rozwiązania, ale nie jest też zbyt dokładny. Jeżeli zależy nam na dokładności, warto skorzystać z osobnego urządzenia (na przykład Raspberry Pi Zero) i dedykowanego układu IMU
- Możliwość sterowania gestem (położeniem telefonu) daje mnóstwo frajdy. Zachęcam do spróbowania!
Brakuje tylko filmu.
Ależ proszę :) https://www.youtube.com/edit?o=U&video_id=E5zDh3NLaaE
A ściślej: https://www.youtube.com/watch?v=E5zDh3NLaaE
To nie Python jest wolny, a po prostu tak się serwera nie robi.
Do sterowania (szczególnie jeśli nie masz informacji zwrotnej) służy protokół UDP a nie TCP.
Użycie RPi plus jakichś dodatkowych płytek to moim zdaniem przerost formy nad treścią – do tego wystarczy ESP8266 bez żadnych dodatkowych firdymałów – podłączasz pin bezpośrednio do serwa.
Masz czwórkę przede wszystkim za pomysł.
Dzięki za komentarz!
Napisałem jako alternatywę serwer UDP i rozszerzyłem aplikację androidową – teraz można wybrać, czy dane będą wysyłane przez TCP czy UDP. Jeszcze nie testowałem z podłączonymi serwami, ale po logach wygląda obiecująco – transmisja faktycznie przyspieszyła.
Natomiast co do użytego kontrolera – to jest kwestia potrzeb. Jeżeli zaprezentowany pan/tilt będzie projektem samym w sobie, to faktycznie można użyć znacznie prostszego rozwiązania. Ale jeżeli chcemy np. zamontować kamerę i przekazywać obraz albo podłączyć taki pan-tilt do większego robota, to moim zdaniem Raspberry będzie jak znalazł.
Jeśli miałbym tam montować kamerę, to wolałbym tam wsadzić silniki bezszczotkowe, a nie serwa.
A tak poza tym, to wolałbym użyć jakiegoś gotowego kontrolera gimbala.
Mimo wszystko pomysł ciekawy.
A nie możesz zastosować binarnej transmisji? Wtedy wystarczą dwa bajty wysyłane przez aplikację na andku, i analizujesz po prostu pojedyncze datagramy?
Mam jesxcze pytanko: czy ta aplikacja na andka może działać w tle? Bo myślę o zdalnie sterowanej gąsienicówce, VLC wyświetlałby mi obraz z kamery, a taka aplikacja mogłaby sterować pojazdem.
Aplikacja na Andka jest przystosowana do transmisji danych binarnych (klasy pchające dane przez sieć mogą wysłać String albo byte[]), natomiast obecnie dane wysyłam tekstowo dla prostoty debuggowania. Myślałem o transmisji binarnej, ale dobrze byłoby ją zabezpieczyć jakimś strażnikiem albo sumą kontrolną, bo jeżeli cokolwiek się rozjedzie podczas transmisji, to zaczną się dziać cuda :)
Co do drugiego pytania: przesyłanie danych działa na osobnym wątku, więc aplikacja powinna działać w tle tak długo, jak długo system nie ubije jej z powodu braku zasobów (co oczywiście jest mało prawdopodobne, ale możliwe). Natomiast miej na uwadze, że nie pisałem jej z założeniem pracy w tle, więc być może trzeba ją będzie odrobinę do tego przystosować.
Hm… czy ta aplikacja na pewno dobrze działa?
Zainstalowałem apk, wpisałem IP i port… jedynym przejawem życia aplikacji jest “cannot connect” w trybie TCP. Na serwerze nie ma żadnych śladów ruchu (przynajmniej tcpdump nic nie pokazuje, żadnych durnych firewalli nie mam).
Cóż mogę powiedzieć – SOA#1, czyli “u mnie działa” :)
Zmodyfikowałem aplikację w taki sposób, że wyświetla teraz przyczynę problemów z połączeniem – w komunikacie będzie teraz np. “Cannot connect! Error: No route to host”. Ściągnij jeszcze raz, zainstaluj, spróbuj, zobaczymy co dalej.
W moim przypadku sprawdzam zawsze, czy przyczyna nie leży po stronie sieci – stawiam router Wifi na jednej komórce, a potem łączę się do niej drugą i Raspberry – w ten sposób mam prawie najczystsze możliwe połączenie, bez firewalli i tak dalej.
Przed chwilą dla pewności sprawdziłem aplikację w opisanej konfiguracji (telefon robiący za router Wifi) na serwerze TCP – działa bez problemu.
No i wszystko jasne – pole gdzie wpisujesz adres IP to tylko dekoracja, mogłeś nas uprzedzić.
Faktycznie, serwer uruchomiony na 192.168.43.34 działa bardzo ładnie – tyle że ja bym chciał mieć inny adres serwera :(
Nie mogłem uprzedzić, bo nie tak to miało działać :) Czeski błąd, zabrakło jednego znaku. Nawiasem, port też był na sztywno. Poprawiłem oba bugi – spróbuj teraz, zamieściłem nową wersję.
Przydało by się jeszcze jakieś uśrednianie zrobić, żeby ta głowica tak nie skakała jak czujnik zwróci jakieś nieprawdziwe dane czy złapie zakłócenia.
Myślałem o tym, żeby oprócz uśrednienia dorzucić jeszcze medianę. W ten sposób sterowanie minimalnie się wprawdzie opóźni, ale powinien zniknąć problem z błędem pomiaru oraz ewentualnymi skokami (co widać na filmiku). Pobawię się w wolnej chwili.
Przy UDP jeśli uda Ci się osiągnąć jakąś sensowną prędkość możesz po prostu odrzucić podejrzaną wartość (zbyt duża różnica) i dopiero jeśli następna wartość będzie podobna uwzględnić ją. Wszelkie próby uśredniania doprowadzą i tak do “myszkowania” głowicy.
Przy 10 komunikatach na sekundę odrzucenie jednego będzie praktycznie niezauważalne.
To nie jest takie proste – a jeżeli po tej “podejrzanej” przyjdzie kolejna “podejrzana” i kolejna i kolejna? Musiałbym zrobić jakiś dodatkowy mechanizm na wypadek, gdyby użytkownik faktycznie zrobił gwałtowny ruch. Wprowadzanie progów (różnica, którą uznaję za “podejrzaną”) zawsze powoduje kłopoty. Mediana ma tę zaletę, że odrzuci “podejrzane” wartości, ale jednocześnie jeżeli użytkownik zrobi gwałtowny ruch, to po krótkim czasie zostanie on zaakceptowany (mediana odrzuci jedną-dwie wartości, a uśrednienie upłynni nieco ruch). Siądę w najbliższym czasie i przetestuję, bo wprowadzenie takiej modyfikacji powinno znacznie upłynnić ruch pan-tilta.
A chcesz kod? :)
Jeśli aplikacja mi zadziała to będę eksperymentować. Zobaczymy co z tego wyjdzie…