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)