Struktura programa
Cilj
Kako osmisliti program za izvršavanje više grupa zadataka? Naučit ćemo što su "stanja". Osnova su za bilo koji složeniji program.
Stanja
Stanja okoline je često potrebno razdvojiti i nazvati posebnim imenima. Naše ponašanje može ovisiti o tome kad "smo u autu", "gledamo TV", "smo na radnom mjestu". Za svako od ovih "stanja" imamo poseban skup naučenih ponašanja i već znamo da odmah trebamo prilagoditi sve akcije tom stanju.
Kako je program slika ponašanja u okolini, tako se i u programu moraju posebno opisati takva stanja. Pogledajmo 2 stanja u takmičenju "Robocup Junior Line". Jedno je "praćenje linije", drugo je "prostor evakuacije".
U "praćenju linije" robot, pogađate, prati liniju. U "prostoru evakuacije" prati zid. Oprostite što nismo našli bolju sliku, ali zid je ipak neka veza. Bilo je i boljih, ali su se plaćale pa...
Ovo je bio najkraći primjer. U praćenju linije se pojavljuje "obilazak prepreke", u "prostoru evakuacije" niz drugih stanja. Počnimo s ovim jednostavnim primjerom i pogledajmo kako bismo napisali program.
Početna funkcija
void RobotLine::rcjLine() {
//...
}
Prvo odlučimo gdje počinje izvršavanje našeg programa. Možemo izabrati jednu od niza predviđenih funkcija: loop(), loop1(), loop2(),... Rješenje je potpuno u redu, ali ipak ostavimo ove funkcije za 10 testnih programića koje možemo pokrenuti na brzinu.
Možemo napisati novu funkciju - ali sad nećemo jer to još ne znate.
Prihvatit ćemo 3. rješenje: koristiti jednu od postojećih funkcija, koja je ostavljena u kodu baš s namjerom da se koristi za početak rješavanja Rescue Line arene: "rcjLine()". Pokrenut ćemo ju kasnije isto kao i loop(): naredbom iz menije ili putem tipkala. Ako Vam je draži loop() - možete ostati na njemu.
Kao što vidite, "rcjLine()" je vrlo sličan "loop()" funkciji. Različito je jedino ime i možda kod između početne i završne vitičaste zagrade, koji možete u potpunosti obrisati.
Najgore rješenje
void RobotLine::rcjLine() {
if (lineAny()){
// Tu biste upisali niz naredbi za praćenje linije.
} else{
// Ovdje idu naredbe za kretanje u zoni evakuacije.
}
}
Prvo, najgore rješenje. Recimo da iz jednog stanja u drugo prebacujemo u skladu s tim vidi li robot liniju.
Zamislimo da ste pokrenuli rcjLine() iz menija i upisali umjesto komentara naredbe za praćenje linije i kretanje u zoni evakuacije. Zapamtite, kao i loop, ovaj se program izvršava tisuće puta, ne samo jednom. U svakom bi prolazu pogledao je li na liniji. Ako jest, pratio bi liniju. Ako nije, kretao bi se u zoni evakuacije.
Vjerojatno ste naslutili što radi nova funkcija "
lineAny()": ako ijedan senzor robota vidi liniju, vraća istinu. Ako nijedan, laž.
Robot bi stvarno izvršavao željeno. Cilj je postignut, ali, uz sad još nevidljivu, žrtvu: "špageti kod" - komplicirano i teško razmrsivo klupko. Kod za sva stanja bi bio utrpan u jednu funkciju. Na ovaj način je moguće programirati, no ne bismo Vam savjetovali. Svako stanje bi trebalo imati svoj zasebni dio koda. Pogledajmo kako to ostvariti.
Funkcije stanja
void RobotLine::lineFollow() {
//...
}
...
void RobotLine::evacuationZone(){
//...
}
Ovo nije doslovan kod iz programa, ali pretraživanjem istog (npr. Ctrl-F) po tekstu "::evacuationZone" i "::lineFollow" ćete ih lako pronaći. Ako imaju naredbe u "tijelu funkcije" (prostoru između početne vitičaste zagrade i završne), slobodno ga obrišite.
Ponovimo, funkcija je niz naredbi, koji čini logičku cjelinu. Funkcija se može "pozvati" u kodu - u tom času će ona izvršiti sve svoje naredbe, počevši s prvom.
Naš je cilj kako izvršavati ove funkcije ("pozvati ih") baš u času kad to bude potrebno. Npr., kad senzor linije vidi da je ispod njega linija, program bi se trebao prebaciti u stanje "praćenje linije" i početi izvršavati za to danu funkciju: "lineFollow()".
Bolje rješenje
void RobotLine::rcjLine() {
static uint32_t lastMs = millis();
if (lineAny())
lastMs = millis();
if (millis() - lastMs > 2000)
evacuationZone();
else
lineFollow();
}
Idemo mi, u skladu s naučenim, na bolje rješenje. Recimo da je robot u stanju "praćenje linije" kad vidi liniju ili ju je vidio prije manje od 2 sekunde. Na taj način mu se daje prilika da pređe prekinutu liniju i nastavi praćenje.
U kodu je to ostvareno na način da je uvedena nova varijabla
lastMs. Vidimo da je tipa jako velikog cijelog broja i da ima prefiks "static", što znači da u nju možemo pohraniti broj milisekundi i da će se to dogoditi samo kad prvi put program izvrši naredbu.
Zamislimo sad da se program izvršava, znači "rcjLine()" se opetovano izvršava tisuće puta. Kad se prvi put izvrši, "lastMs" će poprimiti trenutni broj milisekundi. Nakon toga slijedi "if". Ako robot jest na liniji, "lastMs" će poprimiti trenutni broj milisekundi - što ovdje nije bitno jer je to već spremljeno u prethodnoj liniji. Međutim, kako će vrijeme odmicati, dok god je robot na liniji, "lastMs" će i dalje pokazivati trenutno vrijeme.
Pogledajmo što sljedeći "if" radi u tom slučaju. Razlika "millis() - lastMs" će biti 0 (ponekad 1), u svakom slučaju nije veće od 2000. Znači, biti će izvršen "else" dio - bit će "pozvana" funkcija "lineFollow()". "Poziva" se tako da se njeno ime jednostavno upiše u kod. Kad program dođe do te linije, izvrši njeno tijelo.
Zamislite sad da je robot sišao s linije. "rcjLine()" se i dalje izvršava, ali prvi "if" više ne ažurira varijablu "lastMs". Razlika "millis() - lastMs" će postajati sve veće. Sve dok, nakon 2 sekunde, ne postane veća od 2000. U tom času će robot izvršiti funkciju "evacuationZone()" u 2. "if-u".
Ovo je bio idejni kod. Npr. robot neće pratiti liniju jer nismo još ništa upisali u "lineFollow()".
Najbolje rješenje
Nažalost, komplicirano je. Ostanimo zasad na prethodnom rješenju.
Eksperti,
Zašto opisano rješenje nije najbolje?
Problem je što se funkcija mora vratiti. Kad pozovemo funkciju, ona se izvrši i program nastavi ići dalje na mjestu poziva. Ovo je ograničenje.
Željeli bismo slobodno prelaziti iz jednog stanja u drugo. Npr., ako robot "prati liniju", nakon što vidi prepreku, ide u stanje "obilazak prepreke" (pozivom odgovarajuće funkcije). Nakon što je funkcija gotova, mora se vratiti u stanje "prati liniju".
Klasa RobotLine ima za ovu svrhu "akcije", koje nećemo proučavati u ovom uvodnom programu za robotiku.
Zadatak: proučite nogometaša.
Proučite program za jednog robota, npr. za nogometaša. Pogledajte koje se akcije tamo koriste stanja i kako se prelazi u sljedeće.
Primjedbe
Projekt "Uvod u robotiku" sufinanciran je iz Europskog socijalnog fonda, poziv "Jačanje kapaciteta organizacija civilnoga društva za popularizaciju STEM-a".
Relevantne stranice:
Sadržaj vježbe za virtualne radionice isključiva je odgovornost Hrvatskog društva za robotiku.