Budapesti Műszaki és Gazdaságtudományi Egyetem Méréstechnika és Információs Rendszerek Tanszék
Autóirányítás neuroheadset segítségével Önálló laboratórium zárójegyzőkönyv 2012/13. II. félév
Balogh Márton (XW45WE) III. évfolyam villamosmérnök szakos hallgató BSc Beágyazott információs rendszerek ágazat
Konzulens: Mészáros Tamás mestertanár (Méréstechnika és Információs Rendszerek Tanszék)
-1-
Az eredetileg kitűzött feladat: A feladat célkitűzése egy olyan rendszer megalkotása, amely lehetővé teszi egy autónak néhány utasítás továbbítását, amit az agyi mintákat figyelő neuroheadset jelei alapján választ ki a rendszer. A feladat két fő részre bomlik: 1) A félév első fele elsősorban irodalomkutatás. Egyfelől a CAN busz megismerése, ami magában foglalja a protokoll ismeretét, a buszt kezelő hardverfajták megismerését, valamint részletes ismeretek azt autókon belüli jelekről (A Bosch laboratórium virtuális autója sok segítséget nyújthat hozzá.). Másfelől, az EEG szenzor működésének háttere, és a neuroheadset szoftverének megismerése, betanítása (Megkeresendő, hogy melyik érzéseket/gondolatokat ismeri fel legkönnyebben a szoftver, illetve figyelembe venni, hogy melyikre könnyebb gondolni. Például fékezéshez ne egy nyugodt kép társuljon, amire nem tud az ember gondolni, ha bajban van.) . 2) A félév második fele a megvalósításé, amikor az előzetes ismeretek alapján kiválasztásra kerülnek az esetleges hardver összetevők, és megírásra kerül a neuroheadset programját, és a CAN buszt vezérlő egységét összekapcsoló program, amely értelmezi a headset programjától kapott jelet, és az annak megfelelő utasításokat továbbítja a CAN busz vezérlőjének. Az ehhez kilátásba helyezett nyelv a C++, de rendelkezésre álló eszközök adottságaitól függően, ez módosulhat. Ha az időbe belefér, akkor a feladat kibővíthető olyan funkciókkal, hogy például figyeli történt-e ütközés, és ha igen, akkor lementi az azt megelőző agyi mintát, vagy egy speciális mintát beállítani, ami egyfajta második zárként/jelszóként funkcionál az autó beindításakor.
-2-
Tartalomjegyzék: Borító ............................................................................................. 1 Az eredetileg kitűzött feladat ......................................................... 2 A félév közi munka ........................................................................ 4 A neuroheadset ................................................................................... 4 A CAN hálózat ................................................................................... 6 A megvalósítás ................................................................................... 7 Kiegészítési, folytatási lehetőségek ................................................... 14
Felhasznált Irodalom ..................................................................... 14
-3-
A félév közi munka: A neuroheadset: A feladatkiírásnak megfelelően irodalomkutatással kezdtem, ezen belül is a neuroheadsetről. A tanszék tulajdonában lévő Emotiv Epoc neuroheadset egy 14 érzékelőből, és 2 referenciából álló EEG (Elektro Enkefalográf). Az EEG működése potenciálmérésen alapul, azáltal hogy a 16 elektródát a fejbőrhöz nyomják, (amit az Emotiv esetén rugalmas műanyag lapok biztosítanak) és ezek közötti feszültségeket rögzítik. A mért értékeket egy vezeték nélküli kapcsolaton át egy USB sitck-nek továbbítja, majd az a számítógépes szoftvernek, amely a jelfeldolgozást végzi. A szoftver több funkciókészletbe (u.n. suite) van sorolva. Ezek: Expressiv Suite: Ez a vett jelekből az arcmimikára, nonverbális kommunikációra vonatkozó információkat dolgozza fel. Affectiv Suite: A felhasználó aktuális érzelmi állapotára következtet az agyi hullámformák alapján. Cognitiv Suite: Betanítható bizonyos gondolatok felismerésére, és azonosításuk esetén egy 3D-s kocka mozgatására. [1] Én a 3-ból a Cognitiv Suitot használtam az önálló labor során. Célom az volt, hogy a program által megengedett maximális számú 4 mintát betanítsam a programnak. Ez többé-kevésbé sikerült, de a felismerendő minták számával növekedésével drasztikusan nehezedik a tanítás. Az első mintát még mindenki be tudta tanítani akivel találkoztam, a negyediket viszont én is csak úgy tudtam, hogy a legközelebbi alkalommal amikor leültem elé, újra kellett kezdenem a folyamatot, mert nem tudtam visszaadni pontosan azokat a gondolatokat amikre előzőleg körülbelül rátanult. A hibák többféleképpen jelentkezhetnek, volt hogy nem ismerte fel jelet, volt hogy mást detektált mint amire gondoltam és ritkán de olyan is, hogy semleges jelre detektálást jelzett. A négy jelre azért volt szükségem, hogy az irányítás során ezekhez rendeljem az autó mozgását. Egyik az előre vagyis a gáz, ezt a Push mintához szántam, a másik a fék amit a Pull-hoz rendeltem. A maradék kettő pedig a kormányállást határozza meg, a Right és a Left mintákhoz párosítva. A jobb irányíthatóság jegyében ezeket az értékeket nem a jel erősségétől tettem függővé, mert azt nehéz tudatosan befolyásolni, hanem ha egy bizonyos értéket meghalad, akkor lassan kezd változni az érték, így az intenzitás helyett a gondolat időtartama
-4-
határozza meg mennyire módosuljon a jármű mozgása. Ezekre majd a program működésénél visszatérek részletesebben. A tanítási módszereket illetően számos tanáccsal, és módszerrel találkoztam, és ezek közül volt ami hasznosnak bizonyult, volt ami nem. Lényeges, hogy kipihenten álljon neki az ember, mert a tevékenység koncentrációt igényel, és határozottan fárasztó. További teendő, hogy kellő mennyiségű semleges jelet tanítsunk a programnak, hogy ezzel is elkerüljük a hamis detektálásokat. A tanítás közben érdemes mellőzni a fizikai mozgást, mivel ezek erős jeleket generálnak amire könnyen rátanul a rendszer, és nem a kívánt jelet fogja érzékelni. A legnagyobb kérdés mégis, hogy mire gondol a tanítást végző személy. Az Emotiv útmutatója szerint a képernyőn látható kocka három dimenziós mozgatása a amivel a programot tanítani ajánlott, de számos egyéb módszer akad. A tanszéken hétféle módszert gyűjtöttek össze a tanításra: 1) Képzelj el tipikus cselekvéseket, melyek egymástól jól elkülönülnek, emellett érezhető hatással vannak rád (pl. zuhansz, autót tolsz) 2) Koncentrálj a végtagjaid mozgatására anélkül, hogy valóban megmozdulnának az izmaid (pl. fel akarod emelni a jobb kezed) 3) Koncentrálj a fejed megfelelő pontjaira (pl. előre: orr, jobbra: jobb fül, balra: bal fül) 4) Képzeld el hogy elfordítod a fejed, jobbra, balra, előre döntöd 5) Képzelj el egy folyadékgömböt (masszát),koncentrálj arra, hogy eltolod a megfelelő irányokba 6) Gondolj egy szóra, mond ki fejben elnyújtva, esetleg ismételgesd (pl. húúúúúzni húúúúúúúzni) 7) Koncentrálj egy személyre, aki érzelmeket vált ki belőled (pl. szereted, undorodsz tőle) [2]
Kép a Cognitiv Suite-ról [1] A fenti módszereket kipróbáltam és arra jutottam, hogy szinte mind alkalmas (3-as például nem ment valami jól) a tanításra, de mind csak 1-2 mintáig megbízható. Úgy igyekeztem
-5-
megoldani ezt a problémát, hogy egyrészt összekombináltam néhányat, mint az egyest és a hatost, vagyis elképzeltem a cselekvést, és közben gondoltam egy hozzá kapcsolódó szóra. Ez a detektálást úgy láttam megkönnyíti, de könnyebben is keveri össze a mintákat. Arra azt találtam ki, hogy minden mintának jellegre is más gondolatot választok ki, mint például egy koncentrálást igénylő, egy megnyugtató, vagy egy cselekvésre irányuló, egy érzésre irányuló. Úgy láttam az ilyen mértékben különbözőeket jobban el tudta különíteni egymástól. Összefoglalva azt mondhatnám, hogy ígéretes terület, de a felismerési módszereket még jelentősen csiszolni kell ahhoz, hogy kellően nagy számú mintán, kellően gyorsan, és kellően megbízhatóan lehessen detektálni a kognitív jeleket.
A CAN hálózat: Miután a megépíteni kívánt rendszer ember felőli részével megismerkedtem, ideje volt a gép oldaláról is elindulni, ez pedig a CAN busz működésének megismerésével kezdődött. A CAN aránylag hosszú múltra tekintet vissza, mivel a nyolcvanas évek elején készült el, és a mai napig fejlesztik. A rövidítés Controller Area Network-öt jelet, és már kezdetben is az autóiparba szánták. Ez több kritériumot is jelent a hálózattal szemben: Egyrészt sebességi kritériumot jelent, mivel az összes vezérlőegység egy buszon van, és számos feladat időkritikus, át kell érjen az információ nem sokkal a feladat megjelenése után. A CAN busz ehhez differenciális jelet használ, vagyis a két vezeték alkotja a buszt, amin eleve 2,5 V körüli felszültség van, ez a recesszív állapot. A domináns állapotban a két vezeték jelét 1,5 V illetve 3,5 V körüli értékre húzzák az adó egységek. Ezzel a módszerrel 1 Mbps sebesség is elérhető a hálózaton, bár az autókban tipikusan 500 Kbps-ot használnak. Kiépítési költség is kritikus szempont, hiszen 100as nagyságrendű MCU található egy komolyabb autóban, és ha mind rajta van a buszon, az komoly árnövekedést okozhat. Az összeköttetéshez a CAN hálózatban 2 vezeték szükséges, ami nem jelent súlyos vezetékezési költséget. Érdemes megemlíteni, hogy a legkisebb egységek nem CANen, hanem egy még olcsóbb hálózaton, a LIN-en keresztül kommunikálnak, de a nekem lényeges egységek a CAN-en vannak. Az Arbitráció minden multi master jellegű buszon kérdés, és a CAN esetében különösen, hiszen sok egység kommunikál, és ha adatvesztéssel jár az ütközés, akkor életbe vágó információk veszhetnek el, vagy érkezhetnek késve. Ezt úgy orvosolták, hogy az arbitráció során veszteségmentes legyen az ütközés. Az üzenet a start bit után egy ID mezővel indul, ami nem a megszólaló egységet azonosítja hanem az üzenetet, így egyébként önmagában is információt hordoz. Egyszerre elkezdi adni az összes master jelölt az üzenet IDjét, és folyamatosan olvassa vissza, hogy mi látható a buszon. Ha recesszív jelet ad, de a bsuzon domináns jel látható, akkor nem verseng tovább, mert valamelyik másik üzenet fontosabb. Ez azt is jelenti, hogy a kisebb értékű ID jelenti a fontosabb (nagyobb prioritású) üzenetet, tekintve, hogy a domináns érték jelenti a nullát. Úgyszintén lényeges a megbízhatóság, és ezt az üzenetek végén található 15 bites CRC mező biztosítja, illetve a protokoll több féle hibát definiál, amelyek esetén a detektáló egységek hibakeretet küldenek ki. Ez hat darab nullás bitből (és 8 egyesből) áll, ami önmagában hibának számít, mivel az órajel szinkronizációhoz 5 azonos értékű bit esetén kötelezően beszúrnak utána egy ellenkező értékűt normális működés esetén, amit aztán a vevő oldalon eldobnak. [4] -6-
A CAN üzenet frame (keret) felépítése [4] Végül nem kellett közvetlenül vezérelnem a CAN busz adatforgalmát, mert a számítógépről sem közvetlenül kapcsolódtam a CAN hálózatra, hanem a National Instruments által gyártott NI USB-8473 eszközzel, amely a számítógéphez USB csatlakozóval illeszthető, CAN port. Ehhez azonban meg kellett ismerkednem előbb a NI-CAN API-jával, illetve az APIkkal úgy általában. Az API működéséhez includolni kell a Nican.h headert, továbbá a NI-CAN telepítési könyvtárában található nicanmsc.lib-et megadni a linkernek. [3]
A megvalósítás: Az eszközök tehát adottak, és a számítógéppel is megvan a kapcsolat, amely a két rendszer összeillesztését biztosítja, a következő lépés a szoftver megírása. A program alap felépítése egyszerű: Az elején inicializálással indul, ahol inicializálásra kerülnek a változók, és interfacek, és thread(ek). A NI-CAN inicializálása, és későbbi használata is a dokumentációban megnevezett helyen található, API-t bemutató példaprogramok alapján készült. A jelenlegi verzióban egy külön thread található, amely a neuroheadsettől jövő adatokat dolgozza fel paralel a főciklussal. Jelen megoldásban a neuroheadsetes összeköttetéshez az EmoKey alkalmazást használom, ami az EmoEngine által detektált értékek alapján billentyűlenyomásokat szimulál a számára megadott programban, a beállításainak megfelelően. Én 8 billentyűt definiáltam, ezek a következők:
W: 30% fölötti Push detektálás esetén adja ki. T: 10% alatti Push detektálás esetén adja ki. S: 30% fölötti Pull detektálás esetén adja ki. G: 10% alatti Pull detektálás esetén adja ki. A: 30% fölötti Left detektálás esetén adja ki. F: 10% alatti Left detektálás esetén adja ki. D: 30% fölötti Right detektálás esetén adja ki. H: 10% alatti Right detektálás esetén adja ki.
Látható, hogy az érzékelésben van hiszterézis, tehát bizonytalan, ingadozó kognitív érték esetén sem fogja rángatni a járművet.
-7-
Az inicializáló részt követi a főciklus, ami először frissíti a Mozgás osztály Neuroheadset változóit a neuroheadsetről jövő adatok EmoEngine threadjének értéke alapján, majd korrigálja a CAN üzenet változóinak tartalmát a Mozgás osztály szerint. A Emotiv API EmoKey változók értékei az idővel lassan nőnek, hogy a mértéküket a gondolat jobban befolyásolható hosszával lehessen Kapcsolattartó szabályozni az intenzitásuk helyett. Ez program alól kivételt jelent a fék, amely azonnal változik, illetve nullára veszi a gázt. A CAN buszon a hexadecimális NI-CAN API számrendszer béli 100-as ID-jü üzenet hordozza az irányításra vonatkozó USB / CAN információkat a motor számára. Ezek a gázpedál állása, a fékpedál állása, a kormánykerék helyzete, és a váltó állása. TORCS Valódi autó Előbbi kettő egy-egy nyolcbites előjel szimulátor CAN hálózata nélküli számként van tárolva, a kormánykerék helyzetét egy tizenhat bites előjeles szám hordozza ami little edian elrendezésű, vagyis az első két bit az alsó byte-ja a számnak, végül pedig az automata váltó állása amely egy, kettő, négy, vagy nyolc értéket vesz fel állástól függetlenül. Az én esetemben a váltó nyolcbites értéke mindig fixen nyolc, mert ez jelenti az előre állapotot. A bevitel szűkössége révén hátramenet, vagy kézifék módok nem állíthatók. A 0x100 üzenet tehát: Az első két byte a kormány, a harmadik byte gáz, a negyedik a fék, az ötödik a váltó, a maradék három byte pedig nulla. Ez utóbbiakra nincs szükség, de a CAN protokollnál pont öt byte adatot nem lehet küldeni. A főciklus miután létrehozta, és azt APIn keresztül elküldte a CAN üzenetet, rátér a beérkezettek feldolgozására. Egy 150 elemű puffer található a programban, amelyet 10 milliszekundumonként ellenőriz a program. A puffer méret választás egyszerűen ellenőrizhető: 500000 [bit/sec] / 100 [1/sec] / 47 [bit] = 106,4 "darab" Tehát legrosszabb esetben, vagyis 0 byte adatmezőjű rövid üzenetek, és 100% busz kihasználtság esetén is jóval 150 alatt marad a beérkező üzenetek száma. A feldolgozási időt itt elhanyagoltam, de látható hogy jelentős rátartás van a rendszerben, tehát az már nem oszt vagy szoroz.
-8-
Screenshot a Torcs autószimulátorból [5] A feldolgozásnál az ütközést akarjuk detektálni, és ezt az autó hirtelen fékeződéséből állapítja meg a szoftver. Ehhez szükség van az autó sebességének ismeretére. Ezt a visszajövő auxiliary speed műszerfal üzenetből állapítom meg, amit a 0x320-as ID hordoz, a bejövő üzenetekből tehát ezt kell kiszűrni, és km/h dimenziójúra alakítani. Az üzenet feldolgozását azonban megnehezíti, hogy a műszerfal valamilyen tulajdonságából adódóan minden második beérkező adat nulla értékű (és néha ez sem állandó). Hogy a nullára csökkenő sebességnél is detektálható legyen az ütközés, a nulla értékű üzenetek kiszűrése nem ideális, a hibás üzenetekre rászinkronizálódni pedig bonyolult, és nem elég robosztus megoldás. Ezért azzal az ütközés érzékelő algoritmussal álltam elő (ahogy az látható alább linkelt forráskódban), hogy egy négy elemű pufferben shifteli a négy utoljára érkezett adatot majd megvizsgálja, hogy a régebbi két érték összege jelentősen nagyobb-e mint az újabb kettő összege. A jelentősen nagyobb számszerűen 35 km/h, ami mérések alapján választott érték, ahol már kisebb ütközés is detektálható, de egy erős fékezésre még nem jelez be. A program futás közben, amikor az autó ferdén ütközik a korlát szélének, a városi 50 km/h körüli sebességen (A kiíratás csak a láthatóságot szolgálja):
-9-
A program a "q" betűre lép ki a főciklusból, és számolja fel az adatstruktúrákat maga után, majd befejezi a futását. A C++ programkód: #include <stdio.h> #include <stdlib.h> #include <windows.h> functions #include
#include <string.h> #include <process.h> #include "Nican.h" and constants
// Include file for printf // Include file for strtol // Include file for Win32 time // // // //
Include Include Include Include
file file file file
for for for for
_getch/_kbhit string handling thread NI-CAN functions
NCTYPE_OBJH TxHandle=0; int ch=0; class Mozgas { public: int kormany; unsigned char gaz; unsigned char fek; unsigned char valto; bool w; bool s; bool a; bool d; Mozgas(){kormany=0;gaz=0;fek=0;valto=0x8;} void gomb(char c) { switch (c) { case 'w' : break; case 's' : break; case 'a' : break; case 'd' : break; case 't' : break; case 'g' : break; case 'f' : break; case 'h' : break; }
w=1; s=1; a=1; d=1; w=0; s=0; a=0; d=0;
- 10 -
if(s) { w=0; gaz=0; fek=0xFF; }else { fek=0x00; } if(w) {if(gaz<=100/*254*/) gaz+=1;} else if(gaz>=1) gaz-=1; if(a && !d) { if (kormany > -32267) kormany-=500; } if(d && !a) { if (kormany < 32267) kormany+=500; //32767=(2^15)-1 } } }Moz; void Keydetect( void* pParams ) { while ( 1 ) { ch = _getch(); } } int main () { //INIT NCTYPE_STATUS NCTYPE_CAN_FRAME NCTYPE_ATTRID NCTYPE_UINT32 NCTYPE_UINT32 char NCTYPE_CAN_STRUCT NCTYPE_UINT32 // int unsigned char int char unsigned int int char
Status; Transmit; AttrIdList[8]; AttrValueList[8]; Baudrate = 500000; Interface[7] = "CAN0"; ReceiveBuf[150]; ActualDataSize=0; XTD = 0; data[8]; id = 0x100; data_length = 8; i; SPbuf[4]; Str[1024];
- 11 -
AttrIdList[0] = AttrValueList[0] AttrIdList[1] = AttrValueList[1] AttrIdList[2] = AttrValueList[2] AttrIdList[3] = AttrValueList[3] AttrIdList[4] = AttrValueList[4] AttrIdList[5] = AttrValueList[5] AttrIdList[6] = AttrValueList[6] AttrIdList[7] = AttrValueList[7]
= = = = = = = =
NC_ATTR_BAUD_RATE; Baudrate; NC_ATTR_START_ON_OPEN; NC_TRUE; NC_ATTR_READ_Q_LEN; 0; NC_ATTR_WRITE_Q_LEN; 1; NC_ATTR_CAN_COMP_STD; 0; NC_ATTR_CAN_MASK_STD; NC_CAN_MASK_STD_DONTCARE; NC_ATTR_CAN_COMP_XTD; 0; NC_ATTR_CAN_MASK_XTD; NC_CAN_MASK_XTD_DONTCARE;
Status = ncConfig(Interface, 8, AttrIdList, AttrValueList); if (Status < 0) {printf("Error in ncConfig."); ncCloseObject(TxHandle); exit(1);} Status = ncOpenObject(Interface, &TxHandle); if (Status < 0) {printf("Error in ncOpenObject."); ncCloseObject(TxHandle);exit(1);} for (i=0; i<8; i++) { data[i]=0; } SPbuf[0]=0; SPbuf[1]=0; SPbuf[2]=0; SPbuf[3]=0; _beginthread( Keydetect, 0, NULL ); printf("CAN initialized.\n"); //LOOP do { Moz.gomb(ch); data[0]=(unsigned char)Moz.kormany; data[1]=(unsigned char)(Moz.kormany>>8); data[2]=Moz.gaz; data[3]=Moz.fek; data[4]=Moz.valto; memcpy (Transmit.Data, data, data_length); Transmit.DataLength = data_length; Transmit.IsRemote = 0; Transmit.ArbitrationId = id; Status= ncWrite(TxHandle, sizeof(Transmit), &Transmit);
- 12 -
if (Status < 0) {ncStatusToString(Status,1024,Str);printf(Str); printf("Error in ncWrite."); ncCloseObject(TxHandle); exit(1);} //READ Status = ncReadMult(TxHandle, sizeof(ReceiveBuf),(void *)ReceiveBuf, &ActualDataSize); if (Status < 0) {printf("Error in ncReadMult."); ncCloseObject(TxHandle);exit(1);} ActualDataSize = ActualDataSize/sizeof(NCTYPE_CAN_STRUCT); //printf("ActualDataSize: %d\n", (int)ActualDataSize); for (i=0; i35) { printf("Utkozes tortent."); SPbuf[0]=0; SPbuf[1]=0; SPbuf[2]=0; SPbuf[3]=0; } } } Sleep(10); } while (ch != 'q'); //EXIT Status = ncCloseObject(TxHandle); if (Status < 0) {printf("Error in ncCloseObject.");exit(1);} return 0; } A működés tesztelése az egyetem I épületének IE316-os laborjában, illetve az IE225-ös Bosch laboratóriumában történt az ott kialakított CAN hálózaton, és Torcs autó szimulátoron, amely egy valódi elemeket is tartalmazó virtuális autó. Elméletileg egy valódi autóban is működhetne, de a kipróbálásának két ellenérve van. Egyrészt nehézkes a beszerelés, illetve elveszítheti az autó a garanciát, ha belepiszkálnak. Másrészt biztonsági okokból ellenjavallt, - 13 -
mivel egyetlen hiba is balesetet okozhat, és ennek okán nem engedélyezett a drive by wire, vagyis a csak vezetéken keresztüli irányítás, és az autók kormánya egyelőre fizikailag hozzá van kötve a kerekekhez. A mérési elrendezés:
TORCS szimulátor
CANalyzer Monitorozó program
Műszerfal a sebességhez
NI-CAN: USB CAN adapter
Motor és váltó vezérlő CAN busz, és lezáró impedancia
A kapcsolatot megvalósító PC
Kiegészítési, folytatási lehetőségek: A neuroheadset API-jának felhasználása a következő lépcsőfok, amelyet az év végén elkezdtem, de már befejezésére nem jutott elég időm. A fontosabb funkció az lenne, hogy az ütközés esetén mentse le az aktuális nyers EEG jeleket, hogy utólag matlab-ban, vagy más program segítségével elemezni lehessen azokat. Az Emotiv API hozzá csatlakoztatásával lehetővé válik, hogy kikerüljük az EmoKey-el létesített, nem túl elegáns összeköttetést, illetve gyorsítsuk a kapcsolatteremtést.
Felhasznált irodalom: [1] Emotiv Software Develpoment Kit: User Manual for Release 1.0.0.4 Emotiv_SDK_User_Manual.pdf [2] Illés Ádám: Emotive Training Test - Sheet1.pdf (letöltve: 2013.05.02) [3] National Instruments Corporation: NI-CAN Hardware and Software Manual (2010.július.) http://www.ni.com/pdf/manuals/370289n.pdf [4] Scherer Balázs, dr Tóth Csaba: Autóipari kommunikációs hálózatok vizsgálata (2008. február) http://www.mit.bme.hu/system/files/oktatas/targyak/8604/BEARLab_M78.pdf [5] Torcs screenshot: (letöltve: 2013.05.23) http://www.linuxlinks.com/portal/content/reviews/Games/Screenshot-TORCS.jpg
- 14 -