WebSocket robottiprojekteissa

Edulliset, sulautetut linux PC:t ovat yleistymässä harrastelijoiden projekteissa. Mietin tässä päivänä parina erästä henkilökohtaista projektia, jossa olisi tarve päästä ohjaamaan eräänlaista telaketjuilla liikkuvaa asela…ööhh “lavettia” verkon yli.

Netistä löytyy runsaasti oppaita, missä kerrotaan “näin ohjaat rasperryn 3.3V GPIO linjoilla…” , mutta vähemmälle on jäänyt se puoli miten käyttäjä tai ulkopuoliset järjestelmät liittyvät tähän rasperryyn.

Lavetin liitännän tulisi olla langaton ja robotin on reagoitava reaaliaikaisesti käskytykseen.

Perinteinen tapa olisi käyttää RC-auton/veneen/lentokoneen elektroniikkaa, tällöin linuxin käytön mielekkyys tulee kyseenalaiseksi. Lavettin voisi lisätä myös USB mikrofonin ja puheentunnistussoftan, tällöin lavetin käskytys rajoittuisi huutoetäisyydelle.

Voittajan valinta olisi tökätä rasperryyn “mokkula” (Google: ppp wvdial) tahi toimivan langattoman lähiverkon tapauksessa wlan tikku. Näin käytössä olisi suht nopea kaksisuuntainen yhteys. DNA:n mokkulan TCP portteihin 2222 ja 500 pitäisi voida ottaa yhteys ulkoa. Käyttösäännöt tietenkin kieltävät julkisen webbiserverin pitämisen, mutta uskoisin että jonkin asel…“lavetin” satunnainen käyttö yksityiskäyttönä menee “testikäyttö” nimikkeen alle. Vaihtoehtona on tietysti käyttää paikallista WLAN tikkua ja paikallista WLAN verkkoa. Suurikokoisen robotin ollessa kyseessä, mukaan voi laittaa wlan tukiaseman jolloin riittää että käyttäjä pysyttelee kantomatkan sisällä.

Yhteyden yli voi sensoritietojen (gps, kallistus, kiihtyvyys) lisäksi voi yrittää lähettää usb-webbikameran kuvaa.(käsittelen joskus myöhemmin webbikamerakuvan lähetystä)

Nämä kaikki siis olettaen että vattu saadaan kiinni internettiin tai ainakin samaan lähiverkkoon käyttäjän kanssa.

[size=150]Kiinni verkossa, mitä sitten?[/size]

Linux kone on verkossa, miten sille lähetetään komentoja?

Perinteinen ajatus olisi tehdä robotin linuxille ohjelma joka kuuntelee TCP sokettia ja kirjoittaaa PC:lle softa, jolla käskytys onnistuu.
Soketteja, säikeitä…hmmm ei paha Ja tietenkin pitää huolta siitä, että vain yksi istunto kerrallansa pääsee käyttämään rautaresurssia eli robotin toimilaitteita. (Soketteja ja säikeitä pelkääville "helppo " ja perinteinen tapa olisi käyttää inetd palvelua… riittää että ohjelma osaa lukea ja kirjoittaa standardivirtoihin. (plus ohjata laitetta))

Nykyään kun nämä lääpittävät puhelimet ja tabletit on muotia, täytyy käyttöliittymä ehdottomasti toteuttaa älypuhelinalustalle. Mikäpä siinä, mielummin lavettia ohjaa kapulalla kuin läppärillä. Androidille, Applelle ja windows8 härpättimille on omat kehitystyökalunsa ja usein porttaaminen alustoiden välillä ei useinkaan mene niinkuin Strömsössä.

Vaihtoehto natiivisovelluksille on HTML5 sovellus, joka pitäisi toimia kaikissa nykyaikaisissa puhelimissa ja PC:ssä. Etuna on myös se että ohjelma toimii ilman asentamisia ja sovelluskauppoja.
(toki on olemassa http://phonegap.com/ yms viritelmiä jolla html5 sovellus kääntyy natiiviksi)

Jo yli kymmenen vuotta sitten oli olemassa esimerkiksi PC:lle webbikamerasoftia, joita pystyi kääntelemään kameraa webbikäyttöliittymän kautta napeilla.
Taustalla oli PHP:llä tai C:llä koodattu CGI ohjelma ja webbiformi joka komensi CGI ohjelmaa GET tai POST komennoilla. Tällöin käyttöliittymäksi riitti pelkkä selain. Tälläiset ratkaisut eivät lavettiin käy, sillä kontrollin on oltava reaaliaikaista. Ei voi olla mitenkään hyväksyttävää että kommunikaatio aloitetaan uudestaan jokaista erillistä komentoa varten.

Tähän on olemassa ratkaisu: Websockets. (Toki on olemassa “comet” tekniikka missä viivytellään yhteyden katkaisua… mutta ohitetaan se tälläkertaa)

[size=150]Websockets[/size]

Websockets tarkoittaa käytännössä tavallisen soketin päälle kasattua yhteyskäytäntöä.
Ja sitä miten javascript ottaa yhteyden tähän sokettiin yms… Websocketilla on mahdollista ottaa ainoastaan yhteys selaimesta (client) serveriinpäin. Selaintenvälistä peer to peer yhteyttä ei siis saa toimimaan ilman välityspalvelinta. Serverin ei tarvitse olla kummoinenkaan, kuten seuraavassa kappaleessa huomaamme.

Ohjelmoijan kannalta avattu webbisoketti on putki jonne voi kirjoittaa ja siitä voi lukea.
Websockettia mainostetaan nopeaksi, johtuen siitä että jokaista viestiä varten http yhteyttä ei avata uudestaan, ja se että websocket viestin mukana menee varsin vähän overheadia.

Websocketin käyttöönotto javascriptistä on varsin yksinkertaista. Ensi luodaan websocket instanssi ja sitten siihen kiinnitetään halutut tapahtumakäsittelijäfunktiot.
Websocketin luonnissa tarvitaan websocket “URL” muotoa

ws://hostinimi:portti/urinimi

Javascript koodina tämä tarkoittaa

[code]var host=location.hostname; //jos sivusto ladataan samalta palvelimelta kuin missä websocket pyörii location.hostname toimii. Muutoin syötä IP
var port = “2222”; //portti
var uri = “/robotti”; //URI= Unifor resource identifier
websocket = new WebSocket(“ws://” + host + “:” + port + uri);

//Kiinnitetään tapahtumakäsittelijät
websocket.onopen = function(evt){onOpen(evt)};
websocket.onclose = function(evt) {onClose(evt)};
websocket.onmessage = function(evt) {onMessage(evt)};
websocket.onerror = function(evt) {onError(evt)};
[/code]
Datan vastaanotto tapahtuu onmessage eventissä lukemalla eventtidataa

function onMessage(evt){ alert("Vastaanotettiin "+evt.data); }

Ja sokettiin kirjoittaminen tapahtuu loitsulla:

websocket.send("Näin helposti viesti kulkee");

[size=150]Tornado[/size]
Websocketin syvällisempää määrittelyä on turha tässävaiheessa avata enempää, varsinkin kun määrittelystä on tulossa vielä uusia versioita.
Webbisokettiserverin saa tehtyä alusta alkaen itse tai voi hyödyntää inetd:tä. Nykyään on kuitenkin olemassa yksinkertaisempia ratkaisuita.

Oma valintani webbisokettiserverin toteutukseen osui “tornado web server” python kirjastoon, jolla on mahdollisuus tehdä yksinkertaisia webbipalveluita.
http://www.tornadoweb.org

Tornado webserverin käytössä on se etu, että sama ohjelma voi toimia palvelimena samasta portista 2222 sekä websocketille että käyttöliittymän webbisivulle.

application = tornado.web.Application([(r'/robotti', Websockethandleriluokka),(r"/", Webbisivuhandleriluokka)])
http_server = tornado.httpserver.HTTPServer(application)
http_server.listen(2222)
tornado.ioloop.IOLoop.instance().start()

Ylläolevassa koodissa asennetaan kaksi handleriä osoitteineen ja serveri käynnistetään.

Websockethandleriluokka on periytetty tornado.websocket.WebSocketHandler luokasta ja sen perityt metodit

  • def open(self):
  • def on_message(self, message):
  • def on_close(self):

Metodit on itsestäänselittäviä, open metodiin avauksessa tapahtuvat, on_message:een messagen vastaanoton koodi ja on_closeen koodi mikä suoritetaan websocketin sulkeutuessa.

Webbipalvelinhandlerin tekeminen on suoraviivaisempi: Määritellään perittyyn luokkaan get funktio uudestaan

class Webbisivuhandleriluokka(tornado.web.RequestHandler): def get(self): self.render("kauko.html");
Näin sivu löytyy selaimella suoraan iposoite:2222/

[size=150]Demo[/size]
Jutun liittenä on kaksi tiedostoa

  • kauko.html, html5 käyttöliittymädemo
  • kaiku.py, python2:lla toteutettu, comet-webbiserveri. Ohjaa “simuloidusti” lavettia.

Serveripuolella on huomattava se, että serveri ei päästä kuin yhden asiakkaan kerrallansa ohjailemaan lavettia.

Websocket handler pitää yllä kahta tilaa

  • lukkotiedostoa, /tmp/lavettikaytossa joka kertoo että joku on käyttämässä lavettia
  • luokan attribuuttia, “kahvarautaan” joka kertoo ollessaan !=None että matalan tason yhteys on tämän instanssin käytössä.

Koodi noudattaa periaatetta, että jos kahva rautaan attribuutti on asetettu, rautaa ohjataan normaalisti (kutsumalla kahvaraudan metodeja, esim .aja_eteenpain();). Jos attribuutti on None, katsotaan onko lukkotiedostoa, jos sitä ei ole voidaan rauta ottaa hallintaan ja asettaa rautakahva attribuutti. Jos lukko on, niin threadi ei voi tehdä muuta kuin ilmoittaa asiakkaalle että rauta on käytössä nyt.

Toivottavasti esimerkkikoodini auttavat alkuun, jos ei muuten niin allekirjoittanutta rasperry-levyn hankinnassa. :mrgreen:

http://www.websocket.org/
http://en.wikipedia.org/wiki/WebSocket

[size=150]Sorsa[/size]
Ruuvipenkin foorumisofta suhtautuu vihamielisesti html liitetiedostoon .txt:ksi nimeämisestä huolimatta “Siirto hylättiin, koska se tunnistettiin mahdolliseksi hyökkäykseksi.”

Clienttipuolen HTML5 softa upotteena. Testattu firefoxissa ja chromessa.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<!-----
Sormiruuvin esimerkki websoketeista
---->
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
	<title>Kauko-ohjain</title>
	<script language="javascript" type="text/javascript"> 
		var komennettu_output;
		var vastaanotettu_output;
		var sokettitila_output;
		function alustus(){
			//Haetaan kahvat käyttöliittymäkomponentteihin
			komennettu_output=document.getElementById("komennettu");
			vastaanotettu_output=document.getElementById("vastaanotettu");
			sokettitila_output=document.getElementById("soketintila");
			//Määritellään webbisoketti
			//var host = "192.168.1.100"; //Voidaan antaa kiinteästi osoite
			//var host = "<?php echo $_SERVER['SERVER_ADDR']; ?>"; //Tai PHP:llä serverin IP
			var host=location.hostname; //jos sivusto ladataan samalta palvelimelta kuin missä websocket pyörii
			
			var port = "2222"; //portti
			var uri = "/robotti";
			websocket = new WebSocket("ws://" + host + ":" + port + uri);
			
			//Kiinnitetään tapahtumakäsittelijät
			websocket.onopen = function(evt){onOpen(evt)};
			websocket.onclose = function(evt) {onClose(evt)};
			websocket.onmessage = function(evt) {onMessage(evt)};
			websocket.onerror = function(evt) {onError(evt)};
		}
		
		function onOpen(evt) {
			sokettitila_output.innerHTML="Auki";
		}
		function onClose(evt) {
			sokettitila_output.innerHTML="Kiinni";
		}
		function onMessage(evt){
			vastaanotettu_output.innerHTML="Vastaanotettiin "+evt.data+" "+aikaleima();
			//websocket.close();
		}
		function onError(evt){
			alert("Websokettivirhe "+evt.data);
			sokettitila_output.innerHTML="Virhe";
		}
		
		
		function aikaleima(){
			var t = new Date();
			return "("+t.getHours()+":"+t.getMinutes()+":"+t.getSeconds()+")";
		}
		
		//Komennot
		function cmd_eteen(){
			komennettu_output.innerHTML="ETEEN"+aikaleima();
			websocket.send("eteen");
		}
		
		function cmd_taakse(){
			komennettu_output.innerHTML="TAAKSE"+aikaleima();
			websocket.send("taakse");
		}
		
		function cmd_seis(){
			komennettu_output.innerHTML="SEIS"+aikaleima();
			websocket.send("stop");
		}
		
		function cmd_oikealle(){
			komennettu_output.innerHTML="OIKEA"+aikaleima();
			websocket.send("oikea");
		}
		
		function cmd_vasemmalle(){
			komennettu_output.innerHTML="VASEN"+aikaleima();
			websocket.send("vasen");
		}
		
	</script>
<body onload="alustus()">

	<h2>Kauko-ohjain</h2>
	<center>
	<h3>Komennot</h3>
	<button id="cmdEteen" type="button" onmousedown="cmd_eteen()" onmouseup="cmd_seis()" onmouseout="cmd_seis()">Eteen</button>
	<p>
	<button id="cmdVasen" type="button" onmousedown="cmd_vasemmalle()" onmouseup="cmd_seis()" onmouseout="cmd_seis()">Vasen</button>
	<button id="cmdOikea" type="button" onmousedown="cmd_oikealle()" onmouseup="cmd_seis()" onmouseout="cmd_seis()">Oikea</button>
	<p>
	<button id="cmdTaakse" type="button" onmousedown="cmd_taakse()" onmouseup="cmd_seis()" onmouseout="cmd_seis()">Eteen</button>
	</center>
	<h3>Soketti:</h3>
	<div id="soketintila"></div>
	<h3>Komento</h3>
	<div id="komennettu"></div>
	<h3>Vastaus</h3>
	<div id="vastaanotettu"></div>

</body>
</html>

Lavetin pythonkoodin tynkä löytyy liitteestä, nimettynä .txt:ksi
kaiku.txt (2.78 KB)