114
Diplomsko delo univerzitetnega študija Organizacija in management informacijskih sistemov RAZVOJ INTERAKTIVNEGA GLASOVALNEGA SISTEMA S SPLETNIM IN STROJNIM VMESNIKOM Mentor: red. prof. dr. Andrej Škraba Kandidat: Gašper Pintar Kranj, september 2017

RAZVOJ INTERAKTIVNEGA GLASOVALNEGA SISTEMA S … · 2018-08-24 · opremo, ki je cenovno ugodna in prosto dostopna. Programska koda je objavljena in prosto dostopna na portalu GitHub,

  • Upload
    others

  • View
    1

  • Download
    0

Embed Size (px)

Citation preview

Diplomsko delo univerzitetnega študija Organizacija in management informacijskih sistemov

RAZVOJ INTERAKTIVNEGA GLASOVALNEGA SISTEMA S SPLETNIM IN STROJNIM

VMESNIKOM Mentor: red. prof. dr. Andrej Škraba Kandidat: Gašper Pintar

Kranj, september 2017

ZAHVALA Zahvaljujem se mentorju prof. dr. Andreju Škrabi za pomoč, nasvete in usmerjanje pri izdelavi diplomskega dela. Zahvaljujem se tudi družini za podporo in motivacijo.

POVZETEK Tema diplomske naloge je razvoj interaktivnega glasovalnega sistema s strojnim in spletnim vmesnikom, ki za delovanje ne potrebuje povezave v internet in temelji na odprtokodnosti. Za izdelavo programskega vmesnika smo uporabili obstoječe odprtokodne rešitve in jih prilagodili potrebam diplomskega dela. Razvoj sistema je potekal na računalniku z operacijskim sistemom Ubuntu 14.04 LTS 64-bit (Trusty Tahr) Desktop, na katerem teče strežnik glasovalnega sistema. Strojni del, ki se uporablja v namenskih glasovalnih enotah, temelji na odprtokodni platformi NodeMCU, ki za delovanje uporablja mikrokontroler ESP8266. Zbiranje glasov se vrši prek namenskih glasovalnih enot in prek spletnega vmesnika – spletna glasovalna enota. Glasovalne enote skrbijo za zajem glasov glasovalcev in posredovanje prejetih podatkov prek brezžične povezave na strežnik. Za brezžično povezavo med strežnikom in glasovalnimi enotami skrbi namenski brezžični usmerjevalnik. Strežniški del je razvit v programskem jeziku JavaScript in skrbi za komunikacijo z bazo podatkov Redis, obdelavo zajetih podatkov in vzpostavitev spletnega glasovalnega vmesnika za prikaz vprašanj in rezultatov. Spletni vmesnik glasovalne enote je razvit s pomočjo programskega ogrodja AngularJS kot enostranska aplikacija ter omogoča prikaz vprašanj, dodajanje vprašanj, prikaz vseh odgovorov in prikaz statistike.

KLJUČNE BESEDE:

- Glasovalni sistem - glasovanje - NodeMCU - Mikrokontroler - ESP8266 - Odprta koda - node.js

ABSTRACT The topic of this thesis is development of interactive voting system with web and hardware interface. Voting system is open source based and it does not require internet connection for operation. Thesis describes development of software interface, for which we used existing open source solutions and adapted them to the thesis requirements. Development of the system was done on computer with operating system Ubuntu 14.04 LTS 64-bit (Trusty Tahr) Desktop, which is running a server for voting system. Hardware which is used in the dedicated voting units is based on open source platform NodeMCU. NodeMCU runs on microcontroller ESP8266. Votes are collected with dedicated voting units and via web interface – web voting unit. Voting units are responsible for collection of votes and for transmission of received data to server via wireless connection. Dedicated wireless router is responsible for wireless connection between server and voting units. Thesis describes the development of server. It is developed in JavaScript programming language and is responsible for communication with Redis database, processing of acquired data and establishing of web voting interface for displaying questions and results. The web interface of voting unit is developed with AngularJS and operates as “Single page application”. It can display questions, results, statistics and add new questions.

KEYWORDS:

- Voting system - voting - NodeMCU - Microcontroller - ESP8266 - Open source - node.js

KAZALO 1. UVOD ...................................................................................... 1

1.1. OPREDELITEV TEMATIKE ........................................................... 1

1.2. CILJI NALOGE ....................................................................... 1

2. STROJNA IN PROGRAMSKA OPREMA ................................................... 3

2.1. RAZVOJ SISTEMA ................................................................... 3

2.2. NODEMCU ........................................................................... 5

2.3. ARDUINO IDE ........................................................................ 6

2.4. NODE.JS ............................................................................. 7

2.5. SOCKET.IO ........................................................................... 7

2.6. WEBSOCKET ......................................................................... 8

2.7. REDIS ................................................................................. 8

2.8. EXPRESS ............................................................................. 9

2.9. ANGULARJS ......................................................................... 9

2.10. BREZŽIČNI USMERJEVALNIK .................................................... 9

3. PRIPRAVA SISTEMA ZA RAZVOJ REŠITVE ............................................. 12

3.1. NAMESTITEV ARDUINO IDE ....................................................... 12

3.2. NAMESTITEV NODE.JS............................................................. 12

3.3. NAMESTITEV SOCKET.IO .......................................................... 13

3.4. NAMESTITEV BAZE PODATKOV REDIS ........................................... 13

3.5. NAMESTITEV REDIS KOMPONENTE ZA NODE.JS ................................ 14

3.6. NAMESTITEV KNJIŽNICE EXPRESS................................................ 14

4. IZDELAVA GLASOVALNEGA SISTEMA .................................................. 15

4.1. FIZIČNA GLASOVALNA ENOTA .................................................... 15

4.2. PROGRAMSKA KODA ZA NODEMCU .............................................. 17

4.3. STREŽNIK – SERVER.JS ............................................................ 20

4.3.1. ZAPISOVANJE PODATKOV V BAZO PODATKOV REDIS .................... 25

4.4. SPLETNI VMESNIK – WEBPAGE.HTML ............................................ 26

4.4.1. DOSTOP DO SPLETNEGA VMESNIKA ........................................ 27

4.4.2. NAČRT SPLETNEGA VMESNIKA .............................................. 28

4.4.3. KOMUNIKACIJA SPLETNEGA VMESNIKA S FIZIČNIMI GLASOVALNIMI ENOTAMI ................................................................................. 30

4.4.4. PREVERJANJE DOSEGLJIVOSTI SPLETNIH GLASOVALNIH ENOT ........ 33

4.5. ANGULARJS KRMILNIKI – APP.JS ................................................. 35

5. ZAGON SPLETNEGA GLASOVANJA .................................................... 37

6. ZAKLJUČEK .............................................................................. 40

LITERATURA IN VIRI .......................................................................... 41

PRILOGE ....................................................................................... 43

KAZALO SLIK .................................................................................. 44

POJMOVNIK ................................................................................... 46

KRATICE IN AKRONIMI ....................................................................... 46

PROGRAMSKA KODA

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 1

1. UVOD

1.1. OPREDELITEV TEMATIKE

V diplomskem delu obravnavamo tematiko interaktivnega zbiranja glasov na zastavljena vprašanja ter obdelavo in prikaz zbranih podatkov v smiselni obliki. Zajem podatkov poteka s pomočjo glasovalnih enot, katere delimo na namenske (fizične) glasovalne enote in spletne glasovalne enote. Namenske glasovalne enote temeljijo na mikrokontrolerju ESP8266, natančneje na platformi NodeMCU. Na NodeMCU teče programska koda, napisana v programskem jeziku C, ki skrbi za zajem podatkov s priključenih tipk, vklop ustrezne LED diode na podlagi prejema potrditve zapisa odgovora s strani strežnika in brezžično povezavo z usmerjevalnikom. Spletna glasovalna enota je dosegljiva prek spletnega vmesnika. Napisana je s pomočjo programskega ogrodja AngularJS in JavaScript-a. Dostop je omogočen prek mobilne naprave, ki se poveže na namenski usmerjevalnik, ki zagotavlja brezžično omrežje za namen glasovalnega sistema. Zajeti podatki se pošiljajo na namenski Linux Ubuntu 14.04 LTS računalnik, na katerem teče Node.js strežnik, ki omogoča izvedbo strežniškega dela kode napisane v JavaScript-u. Strežnik skrbi za obdelavo prejetih podatkov in vzpostavitev spletnega vmesnika. Obdelava podatkov zajema preverjanje in zapis prejetih podatkov v bazo, branje že zbranih podatkov iz baze in pošiljanje na spletni vmesnik za prikaz. Spletni vmesnik skrbi za prikaz vprašanj, dodajanje novih in brisanje obstoječih vprašanj, prikaz posameznih odgovorov in prikaz statistike odgovorov na posamezno vprašanje. Prek spletnega vmesnika je dosegljiva tudi »spletna glasovalna enota«.

1.2. CILJI NALOGE

Cilj naloge je razvoj odprtokodne rešitve glasovalnega sistema, ki je lahko dostopen vsakemu uporabniku in za delovanje ne potrebuje povezave v svetovni splet. Za uresničitev ciljev smo se odločili za odprtokodni pristop k izdelavi programskega dela naloge. Pri realizaciji smo uporabili odprtokodno strojno opremo, ki je cenovno ugodna in prosto dostopna. Programska koda je objavljena in prosto dostopna na portalu GitHub, strojna oprema pa temelji na WiFi modulu ESP8266, natančneje na odprtokodni platformi NodeMCU. NodeMCU kot modul majhnih dimenzij omogoča:

priklope za tipke, ki služijo za zajem glasov;

priklope za LED diode, ki skrbijo za prikaz povratne informacije o oddanem in prejetem odgovoru;

brezžični vmesnik za povezavo z usmerjevalnikom in posledično s strežnikom;

Hkrati je modul dobro podprt z odprtokotno programsko opremo in je cenovno zelo dostopen.

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 2

WiFi modul ESP8266 je bil primerna platforma za uresničitev zastavljenega cilja. Vse našteto je vplivalo, da smo izbrali in razvili glasovalni sistem okoli platforme NodeMCU ESP8266 (Škraba et al., 2016).

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 3

2. STROJNA IN PROGRAMSKA OPREMA

V rešitvi smo uporabili platformo NodeMCU, ki temelji na ESP8266 in služi za zajem glasov prek nanjo priključenih senzorjev (gumbov), brezžični komunikaciji z usmerjevalnikom in prikazu povratne informacije o prejemu in zapisu oddanega glasu v bazo podatkov. Koda, ki smo jo uporabili za NodeMCU je napisana v programskem jeziku C (https://www.tutorialspoint.com/cprogramming/, 13.5.2017) in skrbi za vzpostavitev brezžične povezave z usmerjevalnikom, zajem glasov in prikaz povratne informacije zapisa oddanega glasu. Strežniški del uporablja Node.js, ki poganja kodo napisano v jeziku JavaScript (https://www.w3schools.com/js/, 13.5.2017) in skrbi za vzpostavitev spletne strani (spletni vmesnik) ter komunikacijo z bazo podatkov Redis. Spletna stran je z uporabo AngularJS izdelana po principu enostranske aplikacije (»single page application«) in služi prikazu vprašanj, dodajanju novih vprašanj, prikazu posameznih odgovorov in njihovemu statističnemu vrednotenju. Vsebuje tudi posebno stran »Spletna glasovalna enota«, ki služi oddaji glasov prek mobilnih naprav.

2.1. RAZVOJ SISTEMA

Glasovalni sistem sestavlja pet ključnih elementov:

strežnik,

administrativni spletni vmesnik,

spletna glasovalna enota,

fizične glasovalne enote in

brezžični usmerjevalnik. Strežnik temelji na odprtokodnem Node.js (različica 4.2.6) okolju, ki z uporabo knjižnice Express (različica 2.5.11) poskrbi za vzpostavitev spletnega strežnika, knjižnica Socket.io (različica 1.4.5) pa skrbi za dvosmerno komunikacijo med strežnikom in spletnim vmesnikom. Spletni strežnik služi za prikaz spletnega vmesnika, katerega delimo na administrativni in uporabniški del. Za dostop do administrativnega dela je potrebna prijava (Slika 1). V njem je možno začeti in ustaviti glasovanje, dodati nova vprašanja in izbrisati obstoječa vprašanja skupaj s povezanimi odgovori, prikazati in brisati posamezne odgovore in prikazati statistiko odgovorov po posameznem vprašanju.

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 4

Slika 1: Prijava v administrativni vmesnik

Slika 1 prikazuje prijavni vmesnik, prek katerega dobimo dostop do administrativnega dela glasovalnega sistema. Uporabniški del je namenjen oddaji odgovora na izpisano vprašanje in je dosegljiv brez prijave. Druga naloga strežnika je komunikacija z bazo podatkov Redis, v katero se zapisujejo vsa vprašanja, odgovori in vmesni rezultati, potrebni za pravilno delovanje glasovalnega sistema. Za komunikacijo z Redis-om se uporablja knjižnica Redis (različica 2.4.2). Fizične glasovalne enote so sestavljene iz platforme NodeMCU, nanjo priključenih tipk in signalnih LED diod ter baterije, ki skrbi za napajanje glasovalne enote. NodeMCU skrbi za brezžično komunikacijo s strežnikom (katera poteka prek brezžičnega usmerjevalnika), zajem odgovorov glasovalcev in prikaz povratne informacije o prejemu in zapisu oddanega odgovora. Koda za delovanje je napisana v programskem jeziku C in na NodeMCU naložena z uporabo Arduino IDE (različica 1.6.12) vmesnika. Komunikacija med strežnikom, fizičnimi in spletnimi glasovalnimi enotami poteka brezžično, za kar poskrbi brezžični usmerjevalnik Asus RT-N16, ki za delovanje uporablja operacijski sistem DD-WRT. Za DD-WRT smo se odločili zaradi zanesljivosti delovanja in enostavnosti konfiguriranja, je pa za delovanje glasovalnega sistema primeren katerikoli brezžični usmerjevalnik.

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 5

Slika 2: Shema sistema

Slika 2 prikazuje, kako se posamezne komponente povezujejo med seboj in tvorijo delujoč sistem.

2.2. NODEMCU

Ker naloga temelji na odprti kodi in lahko dostopni stroji opremi, ki mora biti tudi primerna za opravljanje zastavljene funkcije, smo za osnovno komponento fizičnih glasovalnih enot izbrali platformo NodeMCU V1.0 Amica ESP8266 (http://www.esp8266.com/, 20.4.2017; https://en.wikipedia.org/wiki/ESP8266/, 21.4.2017).

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 6

Slika 3: NodeMCU V1.0 Amica

Slika 3 prikazuje enega od NodeMCU-jev, uporabljenih v diplomskem delu. Oznaka V1.0 pove, da uporabljamo NodeMCU (http://nodemcu.com/, 21.4.2017; https://en.wikipedia.org/wiki/NodeMCU/, 21.4.2017) druge revizije, ki je ožji in zmogljivejši od predhodnika (V0.9). Temelji na čipu ESP8266, ki vsebuje brezžični vmesnik s polno podporo za TCP/IP povezavo in mikrokontroler. Ima 13 digitalnih vhodno/izhodnih priključkov od katerih smo 3 uporabili za priklop tipk in 3 za priklop LED diod.

2.3. ARDUINO IDE

Slika 4 prikazuje odprtokodni vmesnik Arduino IDE (Integrated Development Environment), ki se uporablja za pisanje, urejanje in nalaganje kode ter komunikacijo z mikrokontrolerjem. Vmesnik je napisan v programskem jeziku Java in je na voljo za operacijske sisteme Linux, Windows in Mac OS X. Združljiv je z vsemi trenutno izdanimi Arduino mikrokontrolerji, podpira pa tudi mnoge druge mikrokontrolerje in platforme, zato je njegova uporaba zelo razširjena in priljubljena. Vsebuje urejevalnik besedila za pisanje in urejanje programske kode, sporočilno okno za izpis rezultatov, konzolo za vnašanje ukazov, orodno vrstico z gumbi za pogosto uporabljene funkcije in orodno vrstico z dodatnimi meniji. Programi napisani z Arduino IDE vmesnikom se imenujejo skeči (sketches). Vmesnik vsebuje prednaložene skeče, ki jih lahko uporabimo za delo ali jih po želji prilagodimo svojim potrebam.

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 7

V diplomskem delu smo uporabili Arduino IDE (različica 1.6.12) in skeč iz projekta »Prototype of group heart rate monitoring with NODEMCU ESP8266« (Škraba et al., 2017), katerega smo prilagodili potrebam diplomske naloge. Skeč smo z uporabo Arduino IDE naložili na NodeMCU in služi za vzpostavitev brezžične komunikacije z usmerjevalnikom, zajem glasov in prikaz povratne informacije o prejemu in zapisu oddanega glasu v bazo podatkov.

Slika 4: Arduino IDE

2.4. NODE.JS

Node.js je izvajalno okolje za JavaScript, zgrajeno na Chrome V8 JavaScript pogonu (https://www.w3schools.com/nodejs/, 13.5.2017). Uporablja dogodkovni vhodno/izhodni model, zaradi česar je učinkovit in s strani sistema nezahteven za izvajanje. Omogoča razvoj/izgradnjo razširljivih mrežnih aplikacij. Primarno je zgrajen za delo s pretočnimi vsebinami in vsebinami, ki potrebujejo nizke dostopne čase (latenco). Pri izdelavi diplomskega dela smo uporabili različico 4.2.6. Z Node.js poženemo strežnik, npr. JavaScript datoteko z imenom »server.js«, ki poskrbi za vzpostavitev spletnega strežnika, komunikacijo z bazo podatkov Redis in izvajanje vseh potrebnih vlog za ustrezno delovanje spletnega vmesnika in celotnega glasovalnega sistema. Omogoča simultan dostop do spletnega vmesnika večjemu številu naprav in branje/izpisovanje podatkov v realnem času.

2.5. SOCKET.IO

Socket.IO (https://socket.io/, https://github.com/socketio/socket.io/, https://en.wikipedia.org/wiki/Socket.IO/, 27.4.2017) je v JavaScript-u napisana knjižnica, ki se uporablja za namene takojšnjega spletnega sporočanja. Omogoča

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 8

takojšnjo dvosmerno komunikacijo med spletnim vmesnikom in strežnikom. Sestavljena je iz dveh delov: knjižnice klienta, ki teče v spletnem brskalniku in knjižnice strežnika za Node.js. Enako kot Node.js, tudi Socket.IO uporablja dogodkovni vhodno/izhodni model. V diplomskem delu Socket.IO služi za komunikacijo med strežnikom in spletnim vmesnikom. Akcije, ki jih uporabnik izvede v spletnem vmesniku, kličejo ustrezno funkcijo, ki poskrbi za klic povezane funkcije na strani strežnika prek vtičnika (»Socket«) in morebiten prenos podatkov. Funkcija na strani strežnika izvede vsebovano kodo in po potrebi kliče povezano funkcijo na strani klienta ter morebiten prenos podatkov, ki poskrbi za odziv akcije uporabnika v spletnem vmesniku.

2.6. WEBSOCKET

WebSocket je računalniški komunikacijski protokol, ki podpira polno dvosmerno komunikacijo z uporabo ene TCP povezave (https://www.websocket.org/, https://en.wikipedia.org/wiki/WebSocket/, 13.5.2017). Zasnovan je z namenom implementacije v spletnih brskalnikih in spletnih strežnikih. Možno ga je uporabiti tudi v drugih spletnih aplikacijah, ki temeljijo na komunikaciji v realnem času. Ker komunikacijo lahko pričneta strežnik ali klient, ni potrebe po konstantnem preverjanju strežnika za spremembami podatkov. Komunikacija med strežnikom in klientom poteka prek TCP vrat 80 ali 443, če uporabljamo šifrirano TLS povezavo. Ravno zaradi dvosmerne komunikacije, ki jo lahko prične strežnik ali klient, je WebSocket primeren za uporabo pri realizaciji glasovalnega sistema, saj služi za komunikacijo med fizičnimi glasovalnimi enotami in administrativnim spletnim vmesnikom. Ker strežniku ni potrebno periodično spraševati glasovalnih enot, če so prejele odgovor, se komunikacija zmanjša na minimum. S tem se zmanjša poraba električne energije glasovalnih enot in posledično poviša čas delovanja na baterije.

2.7. REDIS

Redis je odprtokodna baza podatkov, ki se uporablja predvsem v primerih, kjer je potrebno shranjevati oziroma zapisovati velike količine podatkov v kratkem času (http://www.infoworld.com/, 2017). Podpira mnogo različnih podatkovnih struktur in ima nativno vgrajene postopke in sisteme, ki pripomorejo k hitrejšemu in zanesljivejšemu zapisovanju in branju podatkov ter zagotavljanju visoke razpoložljivosti. V diplomski nalogi smo v Redis-u (verzija 3.0.7) uporabili 2 strukturi baz podatkov: Sorted set in Hash. Sorted set ima spremenljivki »vrednost« (value) in »rezultat« (score). Spremenljivka vrednost podpira več oblik zapisa podatkov: golo besedilo, JSON, HEX…; medtem ko spremenljivka rezultat podpira samo zapis številk. Primer uporabe: zapis vprašanj in odgovorov, ki jih medsebojno povezujemo s podatkom zapisanim v spremenljivko rezultat, ki je enak ID-ju posameznega vprašanja.

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 9

Prednost zapisa vprašanj in odgovorov v Sorted set je v tem, da se vrednost v spremenljivki rezultat lahko ponavlja (več zapisov z isto vrednostjo v spremenljivki rezultat). Hash ima spremenljivki »ključ« (key) in »rezultat« (score). Obe spremenljivki podpirata več oblik zapisa podatkov: golo besedilo, JSON, HEX… Primer uporabe: zapis uporabnikov. V spremenljivko ključ zapišemo enoličen podatek o uporabniku (uporabniško ime, e-naslov…), v spremenljivko vrednost pa v JSON obliki zapišemo vse podatke o uporabniku (uporabniško ime, e-naslov, hash vrednost gesla…). Prednost uporabe strukture Hash predstavlja spremenljivka ključ, ki jo lahko uporabimo kot enoličen identifikator za zapisovanje in iskanje posameznih vrednosti.

2.8. EXPRESS

Express je knjižnica za Node.js, ki omogoča postavitev spletnih in mobilnih aplikacij (https://expressjs.com/, 13.5.2017; https://www.npmjs.com/package/express/, 13.5.2017). Knjižnica je zasnovana v minimalističnem načinu, da za osnovno delovanje ne porabi veliko sistemskih virov, ima pa veliko razširitev, ki osnovnemu delovanju knjižnice prinesejo mnoge dodatne funkcionalnosti in zmogljivosti. V spletni nalogi smo knjižnico Express (različica 2.5.11) uporabili za vzpostavitev spletnega strežnika, ki skrbi za prikaz in delovanje spletnega vmesnika.

2.9. ANGULARJS

AngularJS je odprtokodna spletna platforma, ki temelji na JavaScript-u (https://angularjs.org/, 2017). Za razvoj in vzdrževanje platforme skrbita Google in spletna skupnost, katero sestavljajo posamezniki in podjetja. ANgularJS omogoča izdelavo spletnih strani v stilu namiznih aplikacij, kar izboljša uporabnikovo interakcijo s spletno stranjo. Glavna funkcionalnost AngularJS je možnost izgradnje enostranske aplikacije »single-page application«, kar pomeni, da se podatki za prikaz glavne in vseh podstrani lahko prenesejo takoj ob obisku prve strani ali dinamično glede na uporabnikovo interakcijo s spletno stranjo. Prednost delovanja je v tem, da ni potrebe po osveževanju celotne spletne strani in s tem ponovnega prenašanja vseh podatkov za prikaz strani, temveč le posameznih delov (pod)strani. Osveževanje se lahko omeji le na posamezne dele strani, kar pomeni hitrejše delovanje. Za izdelavo spletnega vmesnika smo uporabili AngularJS različice 1.5.8, saj vmesnik vsebuje malo dinamičnih elementov in se tako izognemo nepotrebnemu osveževanju celotnega vmesnika. Osnovni izgled vključno z navigacijsko vrstico se ne spreminja, spreminja se samo vsebina posamezne podstrani.

2.10. BREZŽIČNI USMERJEVALNIK

Za povezavo vseh komponent glasovalnega sistema smo izbrali brezžično povezavo, saj s tem omogočimo večjo mobilnost sistema in uporabnikom možnost uporabe lastnih naprav (mobilni telefon, tablica…), ki imajo možnost brezžične povezave.

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 10

V diplomskem delu smo uporabili usmerjevalnik Asus RT-N16 s sistemom DD-WRT (https://www.dd-wrt.com/site/, 13.5.2017), na katerem smo kreirali dve brezžični povezavi: »glasovanje« in »glasovanje-guest«. Omrežje »glasovanje« je namenjeno za povezavo strežnika in fizičnih glasovalnih enot, »glasovanje-guest« pa za povezavo naprav glasovalcev.

Slika 5: Brezžični omrežji glasovalnega sistema

Slika 5 prikazuje konfiguracijo brezžičnih omrežij na usmerjevalniku. Omrežji sta zaščiteni z različnima gesloma. Geslo za omrežje »glasovanje-guest« se pred začetkom glasovanja javno objavi, da se glasovalci lahko povežejo na omrežje in pripravijo na oddajo glasov. Ker moramo za komunikacijo administrativnega glasovalnega vmesnika in fizičnih glasovalnih enot poznati IP-naslove enot, smo na podlagi MAC-naslovov brezžičnih vmesnikov glasovalnih enot naredili rezervacijo IP-naslovov v DHCP strežniku usmerjevalnika. Rezervacijo IP-naslova smo naredili tudi za strežnik, katerega se javno objavi pred začetkom glasovanja, da lahko glasovalci dostopajo do spletne glasovalne enote.

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 11

Slika 6: DHCP rezervacija IP-naslovov na usmerjevalniku

Slika 6 prikazuje tabelo statičnih DHCP rezervacij za strežnik in fizične glasovalne enote. Za strežnik smo rezervirali IP-naslov 192.168.42.50, za fizične glasovalne enote pa IP-naslove od 192.168.42.70 do 192.168.42.73. Razpon IP-naslovov, ki jih DHCP strežnik avtomatsko dodeljuje napravam glasovalcev, smo nastavili od 192.168.42.100 do 192.168.42.254.

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 12

3. PRIPRAVA SISTEMA ZA RAZVOJ REŠITVE

Prvi korak pri razvoju rešitve je bila izbira operacijskega sistema, v katerem bo potekal razvoj aplikacije. Zaradi prilagodljivosti in dolgoročne podpore smo izbrali operacijski sistem Ubuntu 14.04 LTS 64-bit (Trusty Tahr) Desktop (https://www.ubuntu.com/, 13.1.2017). Ubuntu smo namestili prek zagonskega USB-ja, ki je vseboval sliko sistema. Po zaključeni namestitvi sistema smo izvedli posodobitev sistema in nameščanje programske opreme ter manjkajočih knjižnic, ki jih bomo potrebovali pri delu. Slika 7 prikazuje posodobitev operacijskega sistema, ki jo izvedemo prek terminala s sledečimi ukazi: sudo apt-get update sudo apt-get upgrade

Slika 7: Posodobitev operacijskega sistema

Po zaključku nameščanja posodobitev sprožimo ukaz za nameščanje osnovnih komponent sistema, da se namestijo morebitne manjkajoče komponente (Slika 8) in ukaz za namestitev paketa »npm«, ki ga bomo uporabili za nameščanje modulov Node.js (Slika 9): sudo apt-get install build-essential

Slika 8: Nameščanje osnovnih komponent sistema

sudo apt-get install npm Slika 9: Nameščanje paketa npm

3.1. NAMESTITEV ARDUINO IDE

Za namestitev Arduino IDE vmesnika potrebujemo namestitveno datoteko, ki jo prenesemo z Arduino spletne strani (https://www.arduino.cc/en/main/software/, 27.2.2017). Po prenosu razširimo arhivsko datoteko in izvedemo namestitev z dvoklikom na datoteko install.sh. Prenesli in namestili smo različico 1.6.12.

3.2. NAMESTITEV NODE.JS

Za namestitev Node.js potrebujemo namestitveno datoteko, ki jo prenesemo z Node.js strežnika (http://nodejs.org/dist/v4.2.6/node-v4.2.6.tar.gz/, 27.2.2017). Preneseno datoteko razširimo in izvedemo namestitev v namensko ustvarjeno mapo. Za namensko mapo smo se odločili z razlogom prenosljivosti izdelane rešitve, saj tako lahko zagotovimo enako programsko podporo tudi na drugih napravah. Ukaze za prenos in namestitev vsebuje slika 10.Izvedemo jih v prikazanem vrstnem redu. cd /usr/local/src sudo mkdir node cd node sudo wget http://nodejs.org/dist/v4.2.6/node-v4.2.6.tar.gz

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 13

sudo tar -xzvf node-v4.2.6.tar.gz cd node-v4.2.6 sudo ./configure sudo make sudo make install

Slika 10: Ukazi za prenos in namestitev Node.js

Prenesli in namestili smo različico 4.2.6, kar nam potrdi ukaz za preverjanje nameščene različice Node.js (slika 11): node --version

Slika 11: Ukaz za preverjanje nameščene različice Node.js

3.3. NAMESTITEV SOCKET.IO

Po zaključeni namestitvi Node.js izvedemo namestitev Socket.IO, ki se izvede z uporabo paketa npm (Slika 12). sudo npm install socket.io

Slika 12: Ukaz za namestitev Socket.IO

Namestili smo različico 1.4.5, kar lahko preverimo z ukazom (Slika 13): npm list socket.io

Slika 13: Ukaz za preverjanje nameščene različice Socket.IO

3.4. NAMESTITEV BAZE PODATKOV REDIS

Za nameščanje potrebujemo namestitveno datoteko, ki jo prenesemo s strežnika (http://download.redis.io/redis-stable.tar.gz/, 27.2.2017). Preneseno datoteko razširimo in namestimo, kar prikazuje slika 14: wget http://download.redis.io/redis-stable.tar.gz tar xvzf redis-stable.tar.gz cd redis-stable make

Slika 14: Ukazi za prenos, razširitev in izgradnjo Redis-a iz izvorne kode

Po zaključku izgradnje iz izvorne kode (zaključek ukaza »make«), z ukazom prikazanim na sliki 15, izvedemo test zgrajenega objekta: make test

Slika 15: Ukaz za test zgrajenega objekta za nameščanje Redis-a

Če se test zaključi brez napak, izvedemo namestitev Redisa (Slika 16): sudo make install

Slika 16: Ukaz za namestitev Redis-a

Po uspešni namestitvi zaženemo strežniški del Redis-a, ki skrbi za dostopnost baze podatkov. Zagon naredimo s sledečim ukazom (Slika 17): redis-server

Slika 17: Ukaz za zagon Redis strežnika

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 14

Odpre se terminal Redis strežnika, v katerega vpišemo ukaz za preverjanje delovanja (Slika 18): redis-cli ping

Slika 18: Ukaz za preverjanje delovanja Redis strežnika

Če se je celoten postopek nameščanja Redis-a izvedel brez napak, dobimo odgovor prikazan na sliki Slika 19: $ redis-cli ping PONG

Slika 19: Preverjanje delovanja Redis strežnika in prejet odgovor

Redis je sedaj pripravljen za delovanje. Baz podatkov nam v tem koraku ni potrebno kreirati, saj jih bomo kreirali v programski kodi glasovalnega sistema.

3.5. NAMESTITEV REDIS KOMPONENTE ZA NODE.JS

Komponenta služi za komunikacijo Node.js strežnika z Redis strežnikom in jo namestimo z uporabo paketa npm (Slika 20): sudo npm install redis

Slika 20: Ukaz za nameščanje Redis kompomente za Node.js

3.6. NAMESTITEV KNJIŽNICE EXPRESS

Namestitev knjižnice Express izvedemo z uporabo paketa npm, kar naredimo z ukazom (Slika 21): npm install express

Slika 21: Nameščanje knjižnice Express

Namestili smo različico 2.5.11.

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 15

4. IZDELAVA GLASOVALNEGA SISTEMA

Sprva smo za strojni del fizičnih glasovalnih enot uporabili Arduino UNO, ki smo ga tekom testiranj nadomestili z NodeMCU. Prednosti NodeMCU v primerjavi z Arduinom je brezžični vmesnik, katerega NodeMCU vsebuje že tovarniško in ni potrebe po dodatni komponenti za komunikacijo z brezžičnim usmerjevalnikom; je manjših dimenzij in porabi manj električne energije (v meritve vključujemo tudi porabo dodatnega brezžičnega vmesnika za Arduino). Po izboru strojne in programske opreme, smo pristopili k izdelavi fizičnih glasovalnih enot in programskega dela glasovalnega sistema.

4.1. FIZIČNA GLASOVALNA ENOTA

Prototipne fizične glasovalne enote sestavljajo sledeče komponente:

NodeMCU V1.0 Amica.

Tri 2-pinske tipke (»pushbutton«) za oddajo glasov: o Rdeča tipka »Se ne strinjam«, o Bela tipka »Vzdržan«, o Zelena tipka »Se strinjam«.

Tri LED diode za prikaz sprejema in zapisa oddanega odgovora v bazo podatkov:

o Rdeča LED za prikaz oddaje odgovora »Se ne strinjam«, o Bela LED za prikaz oddaje odgovora »Vzdržan«, o Zelena LED za prikaz oddaje odgovora »Se strinjam«.

2 upora za vezavo tipk in LED diod na zemljo (GND): o 220 Ohm (Ω) za vezavo tipk, o 1k Ohm (kΩ) za vezavo LED diod.

Povezovalne kable (»jumper wires«) za vezavo komponent.

USB kabel (s priključkoma A in mikro-B) za nalaganje kode na NodeMCU in priklop napajanja.

Power bank za napajanje glasovalne enote. Priklop tipk in LED diod je prikazan na skici (slika 22) in shemi (slika 23) spodaj. Sliki sta narisani z uporabo programa Fritzing (različica 0.9.3b; http://fritzing.org/home/, 13.5.2017), ki še ne vsebuje slike modela NodeMCU, zato smo ga morali prenesti s spletne strani https://github.com/squix78/esp8266-fritzing-parts/tree/master/nodemcu-v1.0/ (27.2.2017) in ročno uvoziti v program.

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 16

Slika 22: Skica priklopa tipk in LED diod na prototipni plošči

Opomba: rjava barva kabla označuje priklop na digitalne vhode, modra pa priklop na ozemljitev (GND).

Slika 23: Shema vezave tipk in LED diod z NodeMCU

Opis priklopa tipk: Tipka »Se ne strinjam« (rdeča) je z enim priključkom vezana na digitalni vhod D1, tipka »Vzdržan« (bela) na D2 in tipka »Se strinjam« (zelena) na D4. D3 je namenoma prost, saj je med testiranjem prihajalo do napak pri branju vrednosti D3 digitalnega vhoda. Napaka je bila prisotna pri vseh enotah, zato smo se odločili za uporabo D4 vhoda. Z drugim priključkom so vse tipke vezane na upor z vrednostjo 220 Ohm (Ω), ki je priključen na vhod GND.

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 17

Opis priklopa LED diod: Rdeča LED dioda »Se ne strinjam« je s pozitivnim priključkom vezana na digitalni izhod D5, bela »Vzdržan« na D6 in zelena »Se strinjam« na D7. Z negativnim priključkom so vse tri vezane na upor z vrednostjo 1k Ohm (kΩ), ki je priključen na vhod GND. Zaradi stabilnosti, lažjega transporta in demonstracije, smo vse skupaj vgradili v plastično ohišje in prek USB vhoda povezali z baterijo oziroma z »USB power bank-om«. Slika 24 prikazuje prototip 1, ki še ne vsebuje vgrajenega napajanja, slika 25 pa prototip 2, ki ima USB power bank že pritrjen na ohišje. S pritrditvijo napajanja na ohišje, smo zagotovili večjo in lažjo mobilnost glasovalne enote.

Slika 24: Prototip 1 in prikaz vezave

Slika 25: Prototip 2 in prikaz vezave

4.2. PROGRAMSKA KODA ZA NODEMCU

Koda za delovanje NodeMCU je dolga 86 vrstic in na začetku vključuje knjižnice Arduino.h, ESP8266WiFi.h, WebSocketsServer.h in Hash.h, ki skrbijo za pravilo izvajanje celotne kode. Ardunino.h (https://github.com/arduino/Arduino/blob/master/hardware/arduino/avr/cores/arduino/Arduino.h/, 26.4.2017) vsebuje ključne informacije za delovanje mikrokontrolerja in je vključena v urejevalnik Arduino IDE. ESP8266WiFi.h (https://github.com/esp8266/Arduino/blob/master/libraries/ESP8266WiFi/src/ESP8266WiFi.h/, 20.2.2017) vsebuje vse informacije za delovanje brezžičnega

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 18

vmesnika vgrajenega v NodeMCU, zato lahko posamezne ukaze za delovanje pokličemo z definirano spremenljivko »WiFi«. Primer kode (58. vrstica kode, prikazana na sliki 27): WiFi.begin(ssid, password); WebSocketsServer.h (https://github.com/Links2004/arduinoWebSockets/blob/master/src/WebSocketsServer.h/, 13.5.2017) vsebuje informacije za vzpostavitev WebSocket povezave, ki se uporablja za komunikacijo z administrativnim vmesnikom glasovanja. Primer kode za vzpostavitev WebSocket-a (5. vrstica kode, prikazana na sliki 26): WebSocketsServer webSocket = WebSocketsServer(81); Primer kode za sporočanje pritiska tipke (72. vrstica kode, prikazana na sliki 27): webSocket.broadcastTXT("3"); 01 #include <Arduino.h> 02 #include <ESP8266WiFi.h> 03 #include "WebSocketsServer.h" 04 #include <Hash.h> 05 WebSocketsServer webSocket = WebSocketsServer(81); 06 const char* ssid = "SSID"; // replace "SSID" with wireless name (SSID) 07 const char* password = "PWD"; // replace "PWD" eith wireless password 08 boolean empty = true; 09 unsigned long lastBtnPressTime = 0; 10 unsigned long debounceDelay = 1000; 11 12 void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload,

size_t lenght) 13 14 switch(type) 15 case WStype_DISCONNECTED: 16 break; 17 18 case WStype_CONNECTED: 19 IPAddress ip = webSocket.remoteIP(num); 20 break; 21 22 case WStype_TEXT: 23 String text = String((char *) &payload [0]); 24 if (text == "3") 25 digitalWrite(13, 1); //D7 26 delay(500); 27 digitalWrite(13, 0);

28 else if (text == "2") //D6 29 digitalWrite(12, 1); 30 delay(500); 31 digitalWrite(12, 0);

Slika 26: Programska koda za NodeMCU 1/2

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 19

32 else if (text == "1") //D5 33 digitalWrite(14, 1); 34 delay(500); 35 digitalWrite(14, 0); 36 else if (text == "0") 37 webSocket.sendTXT(num, "42"); 38 39 break; 40 41 case WStype_BIN: 42 hexdump(payload, lenght); 43 webSocket.sendBIN(num, payload, lenght); 44 break; 45 46 47 48 49 void setup() 50 pinMode(0, INPUT_PULLUP); // D3 51 pinMode(4, INPUT_PULLUP); // D2 52 pinMode(5, INPUT_PULLUP); // D1 53 pinMode(12, OUTPUT); // D6 54 pinMode(13, OUTPUT); // D7 55 pinMode(14, OUTPUT); // D5 56 57 Serial.begin(115200); 58 WiFi.begin(ssid, password); 59 while(WiFi.status() != WL_CONNECTED) 60 delay(100); 61 62 Serial.println(WiFi.localIP()); 63 webSocket.begin(); 64 webSocket.onEvent(webSocketEvent); 65 66 67 void loop() 68 webSocket.loop(); 69 70 if (digitalRead(2) == 0) // D4-proti (13) 71 if((millis()-lastBtnPressTime)>debounceDelay) 72 webSocket.broadcastTXT("3"); 73 74 lastBtnPressTime=millis(); 75 else if (digitalRead(4) == 0) // D2-nedolocen (12) 76 if((millis()-lastBtnPressTime)>debounceDelay) 77 webSocket.broadcastTXT("2"); 78 79 lastBtnPressTime=millis(); 80 else if (digitalRead(5) == 0) // D1-za (14) 81 if((millis()-lastBtnPressTime)>debounceDelay) 82 webSocket.broadcastTXT("1"); 83 84 lastBtnPressTime=millis(); 85 86

Slika 27: Programska koda za NodeMCU 2/2

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 20

Funkcija »webSocketEvent« prek WebSocket-a posluša sprejem informacije s strani administrativnega vmesnika glasovalne enote. V primeru prejema sporočila, ki vsebuje tekst:

»3«, se za 500 milisekund prižge zelena LED-ica;

»2«, se za 500 milisekund prižge bela LED-ica;

»1«, se za 500 milisekund prižge rdeča LED-ica;

»0«, se prek WebSocket-a administrativnemu vmesniku pošlje tekst, ki vsebuje odgovor »42«.

Zadnji pogoj se uporablja za preverjanje dosegljivosti fizične glasovalne enote. Stanje posamezne glasovalne enote se izpisuje v administrativnem spletnem vmesniku in se preverja periodično. Funkcija »setup« vsebuje definicije posameznih priključkov. Posebnost so priključki za priklop tipk, ki so namesto »input« definirani kot »input_pullup«. To pomeni, da mikrokontroler na omenjenih priključkih preverja, kdaj se stanje spremeni iz vrednosti 1 v 0. V funkcijo je zajeto tudi preverjanje, če je brezžična povezava vzpostavljena. Funkcija »loop« se konstantno ponavlja in preverja stanje tipk. Če je glasovalec pritisnil na katero od tipk, se vrednost izbrane tipke z uporabo WebSocket-a posreduje na administrativni spletni vmesnik, kjer se izvede zapis prejetega odgovora. Ko je odgovor zapisan v bazo podatkov, administrativni vmesnik prek WebSocket-a glasovalni enoti posreduje potrdilo o prejemu in zapisu izbranega odgovora. Za prikaz prejetega odgovora poskrbi funkcija »webSocketEvent«. Funkcija »loop« vsebuje varnostni element »debounce«, ki preprečuje preobremenitev komunikacije in sprejem ponavljajočih odgovorov v kratkem času. Po oddanem odgovoru mora preteči 1 sekunda, preden se lahko pošlje nov odgovor. V primeru, da je pritisk tipke zaznan pred potekom 1 sekunde, se časovnik, ki šteje pretečeni čas od zadnjega pritiska tipke, ponastavi na vrednost 0 in prične teči od začetka. S tem tudi preprečimo, da bi glasovalec neprestano držal aktivno posamezno tipko.

4.3. STREŽNIK – SERVER.JS

Strežnik je realiziran v datoteki server.js in je osnovna komponenta programskega dela glasovalnega sistema, ki poskrbi za:

Vključevanje zunanjih knjižnic, ki jih strežnik in spletni vmesnik potrebujeta za delovanje – Slika 28.

var express = require("express")(); var redis = require("redis");

Slika 28: Zunanje knjižnice, ki jih strežnik in spletni vmesnik potrebujeta za

delovanje

Vključevanje vseh povezanih datotek z uporabo knjižnice Express – Slika 29 (slika prikazuje izsek in ne celotne kode).

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 21

// === EXPRESS.GET initial files ===

express.get('/', function(req, res) res.sendFile(__dirname + '/webpage.html'); ); express.get('/app.js', function(req, res) res.sendFile(__dirname + '/app.js'); ); // === EXPRESS.GET pages ===

express.get('/pages/home.html', function(req, res) res.sendFile(__dirname + '/pages/home.html'); ); express.get('/pages/show-question.html', requireLogin, function(req, res) res.sendFile(__dirname + '/pages/show-question.html'); );

Slika 29: Izsek kode vključevanja povezanih datotek

Vzpostavitev spletnega strežnika, ki posluša na vratih 8080 in omogoči dostop in komunikacijo s spletnim vmesnikom – Slika 30.

http.listen(8080);

Slika 30: Vzpostavitev spletnega strežnika

Vzpostavitev komunikacije prek Socket.io s posameznimi klienti, ki dostopajo do spletnega vmesnika – Slika 31.

io.sockets.on("connection", function(socket)

// pridobivanje IP-naslova klienta in socket ID

cIP=socket.request.connection.remoteAddress.substring(7);

cSocketID = socket.id;

Slika 31: Izsek kode za vzpostavitev Socket.io komunikacije

Komunikacijo, branje in zapisovanje podatkov z/iz/v bazo podatkov Redis – Slika 32.

clientRedis.hget("webge", cIP, function(err, reply) if (reply === null) if (socket.request.connection.remoteAddress.substring(7) != serverIP) cNum++; clientRedis.hset("webge", cIP, '"IP":"'+cIP+'","SocketID":"'

+cSocketID+'","hms":"99:99:99","stanjeOdg":"0"'); console.log("New user has connected. Socket ID: "+socket.id+". IP:

"+socket.request.connection.remoteAddress.substring(7)+". Total users:

"+cNum+"."); else socket.emit("socketDuplicatedSocketID"); duplicatedSocketID=true; io.sockets.connected[cSocketID].disconnect(); console.log("Podvojen dostop naprave z IP: "+cIP+". Nov SocketID

podvojene naprave: "+cSocketID+". Povezava novega SocketID prekinjena!"); console.log("Reply: "+reply); duplicatedSocketID=false;

);

Slika 32: Prikaz komunikacije strežnika z bazo podatkov Redis

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 22

Komunikacijo s posameznimi klienti prek Socket.io – Slika 33. socket.on("socketF5", function(msg)

if (socketF5===true) // ob zagonu programa

branjeStVpr();

socketF5=false;

else if (msg.includes('add-question')) // ob refresh-u strani za

dodajanje vprašanj

beriVprasanje();

socket.emit("socketF5webpage");

else if (msg.includes('all-questions') || msg.includes('answers') ||

msg.includes('statistics')) // ob refresh-u (F5) strani s tabelami

podatkov

socket.emit("socketF5webpage");

else // ob refresh-u (F5) strani z vprašanjem in ostalih, stranek,

ki jih zgornji if-i ne pokrijejo

beriVprasanje();

);

Slika 33: Komunikacija strežnika prek Socket.io s povezanimi klienti

»socket.on« označuje kodo, ki se izvede ob prejemu posameznega ukaza prek Socket.io, »socket.emit« pa označuje ukaz, ki se prek Socket.io pošlje posameznemu klientu. Primer s slike 33: Ko strežnik preko Socket.io prejme ukaz »socketF5«, najprej prebere vsebino sporočila (spremenljivka »msg«), če je le-to tudi dodano k ukazu. Predpostavljajmo, da je vsebina sporočila enaka »all-questions«, kar pomeni, da se izvede tretji pogoj iz primera in ta klientu, ki je strežniku poslal ukaz »socketF5«, preko Socket.io vrne ukaz »socketF5webpage«. Spremenljivka »socket« vsebuje Socket ID klienta, ki je strežniku poslal ukaz, kar pomeni, da se sporočila pošiljajo samo določenemu klientu in ne vsem, ki so povezani na spletni vmesnik.

Obdelava zbranih podatkov – Sliki 34 in 35. // branje vprašanj in odgovorov ter sestavljanje v enoten string za izpis

na webpage (podstran 'statistika') function branjeVprOdgSkupaj() var j=1, k=0, tempVprID, tempVpr, tempVprasanje, tempOdgovor,

tempSeStrinjam, tempVzdrzan, tempSeNeStrinjam, odgovor=[]; // array vseh odgovorv, ki se posredujejo na webpage for(i=0; i<maxVprID; i++) clientRedis.zrangebyscore("vprasanja", '('+i, (i+1), function(err,

reply) if (reply != "") // pusti '!=', če spremeniš v '!==' ne deluje tempVprasanje = JSON.parse(reply); tempVprID = tempVprasanje.VprID; tempVpr = tempVprasanje.vprasanje; );

Slika 34: Primer kode za obdelovanje zbranih podatkov (1/2)

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 23

clientRedis.zrangebyscore("odgovori", '('+i, (i+1), function(err,

reply) if (reply != "") // pusti '!=', če spremeniš v '!==' ne deluje tempSeStrinjam=0, tempVzdrzan=0, tempSeNeStrinjam=0; tempOdgovor = JSON.parse('['+reply+']'); // ker je odgovorov za

dano vprašanje več kot eden, ga sestavimo v JSON array for(ii=0; ii<stOdgVpr[k]; ii++) // ponovimo branje JSON array-a

odgovorov, kolikor je odgovorov v array-u - 'stOdgVpr[k]' if (tempOdgovor[ii].Odg == "Se strinjam") tempSeStrinjam++; else if (tempOdgovor[ii].Odg == "Vzdržan") tempVzdrzan++; else if (tempOdgovor[ii].Odg == "Se ne strinjam") tempSeNeStrinjam++; tempOdgSkupaj=tempSeStrinjam+tempVzdrzan+tempSeNeStrinjam; tempOdgSeStrinjam=tempSeStrinjam+"

("+parseFloat(Math.round(tempSeStrinjam * 100) /

(tempSeStrinjam+tempVzdrzan+tempSeNeStrinjam)).toFixed(2)+"%)"; tempOdgVzdrzan=tempVzdrzan+" ("+parseFloat(Math.round(tempVzdrzan

* 100) / (tempSeStrinjam+tempVzdrzan+tempSeNeStrinjam)).toFixed(2)+"%)"; tempOdgSeNeStrinjam=tempSeNeStrinjam+"

("+parseFloat(Math.round(tempSeNeStrinjam * 100) /

(tempSeStrinjam+tempVzdrzan+tempSeNeStrinjam)).toFixed(2)+"%)";

odgovor[k]="VprID":tempVprID,"vprasanje":tempVpr,"seStrinjam":tempOdgSeStr

injam,"vzdrzan":tempOdgVzdrzan,"seNeStrinjam":tempOdgSeNeStrinjam,"skupaj":

tempOdgSkupaj; if ((k+1) == stOdgVpr.length) socket.emit("socketPosiljanjeStatistike", odgovor); k++; );

Slika 35: Primer kode za obdelovanje zbranih podatkov (2/2)

Sliki 34 in 35 prikazuje kodo, ki skrbi za izpis statistike rezultatov, ki se uporablja na podstrani »Statistika« na spletnem vmesniku. Koda je vključena v funkcijo »branjeVprOdgSkupaj«. Del njene posebnosti je, da se lahko izvede šele, ko imamo podatek o najvišjem ID vprašanja v bazi podatkov, za kar poskrbi funkcija »lastVprID« in o številu odgovorov na posamezno vprašanje, za kar poskrbi funkcija »stOdgPosameznoVpr«. Programska koda funkcij »lastVprID« in »stOdgPosameznoVpr« je prikazana na slikah 36 in 37:

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 24

function lastVprID() if (stVpr > 0) clientRedis.zrange("vprasanja", (stVpr-1), (stVpr-1), function(err,

reply) tempReply = JSON.parse(reply); maxVprID = tempReply.VprID; stOdgPosameznoVpr(); );

Slika 36: Funkcija "lastVprID"

// izpis števila prejetih odgovorov na posamezno vprašanje - beležimo

samo vprašanja, ki imajo odgovore function stOdgPosameznoVpr() var j=1, k=0; stOdgVpr = []; for (i=1; i<=maxVprID; i++) clientRedis.zcount("odgovori", i, i, function(err, reply) if (reply != "") stOdgVpr[k] = reply; k++; if (v02===1) branjeVprOdgSkupaj(); v02=0; );

Slika 37: Funkcija "stOdgPosameznoVpr"

Podatek o najvišjem ID vprašanja v bazi uporabimo v prvi zanki »for«, ki poskrbi za branje vseh vprašanj in na posamezno vprašanje vezanih odgovorov v bazi podatkov. Iz baze vprašanj preberemo ID vprašanja in vsebino vprašanja, iz baze odgovorov pa vse odgovore vezane na posamezno vprašanje. Za štetje posameznih odgovorov (Se ne strinjam, Vzdržan, Se strinjam) uporabimo drugo funkcijo »for«, ki za posamezno vprašanje izvede ustrezno število ponovitev branja odgovorov. Število ponovitev branja odgovorov je zapisano v spremenljivki »stOdgVpr« in predstavlja array števila odgovorov za vprašanja, na katera smo prejeli vsaj en odgovor. Vprašanj, ki ne vsebujejo odgovorov, ne beremo. Skladno s prebranim odgovorom povečujemo spremenljivke, vezane na posamezen tip odgovora. Ko so vsi odgovori, vezani na posamezno vprašanje, prebrani, podatke o vprašanju in odgovorih spremenimo v JSON obliko (https://www.w3schools.com/js/js_json_intro.asp/, 13.5.2017) in zapišemo v spremenljivko »odgovor« tipa array. Po zaključenem branju vseh vprašanj in povezanih odgovorov spremenljivko »odgovor« z uporabo Socket.io ukaza »socketPosiljanjeStatistike« pošljemo klientu, ki skrbi za prikaz administrativnega vmesnika.

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 25

4.3.1. ZAPISOVANJE PODATKOV V BAZO PODATKOV REDIS

Za zapisovanje in obdelovanje podatkov ter pravilno delovanje aplikacije, uporabljamo štiri baze podatkov: »vprasanja«, »odgovori«, »preverjanje« in »webge«. Bazi »vprasanja« in »odgovori« sta tipa »Sorted set« in vanju vpisujemo podatke o vprašanjih in povezanih odgovorih. Slika 38 prikazuje bazo »vprasanja«, kjer v polje vrednost (»value«), z uporabo JSON-a, vpišemo ID vprašanja (VprID) in vprašanje (vprasanje). V polje rezultat (»score«) vpišemo ID vprašanja, ki ga uporabimo pri povezovanju vprašanja in povezanih odgovorov.

Slika 38: Baza "vprasanja"

Baza »odgovori«, prikazana na sliki 39, je podobna bazi »vprasanja«, s tem, da v tej bazi shranjujemo vse prejete odgovore. Podatke o odgovorih shranjujemo v polje vrednost (»value«), kamor z uporabo JSON-a zapišemo ID vprašanja (VprID), prejeti odgovor (Odg), časovni žig v berljivem formatu (ts), časovni žig v milisekundah, pretečenih od 1.1.1970 (ts2) in ID Socket povezave naprave, s katere smo prejeli odgovor (SocketID). Enako kot v bazi »vprasanja«, tudi v tej bazi v polje rezultat (»Score«) vpišemo ID vprašanja in s tem odgovor povežemo s posameznim vprašanjem.

Slika 39: Baza "odgovori"

Ker struktura Sorted set omogoča iskanje po polju rezultat, katerega uporabimo za povezovanje vprašanja in prejetih odgovorov, moramo pri zapisovanju paziti, da v polje vpišemo pravilno vrednost (VprID). Bazi »preverjanje« in »webge« sta tipa Hash.

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 26

V bazo »webge«, slika 40, vpisujemo podatke o povezanih napravah, ki dostopajo do spletne glasovalne enote in jih uporabljamo za prikaz povezanih spletnih glasovalnih enot na administrativnem vmesniku ter za preverjanje podvojenega dostopa do spletne glasovalne enote z istega IP-naslova. V polje ključ (»key«) vpišemo IP-naslov povezane naprave, ki ga uporabimo za preverjanje podvojenosti povezave. V polje vrednost (»value«) z uporabo JSON-a vpišemo IP-naslov naprave (IP), ID Socket povezave (SocketID), čas oddaje odgovora (hms) in število oddanih odgovorov na izpisano vprašanje (stanjeOdg). Podatke polja vrednost uporabljamo za prikaz povezanih spletnih glasovalnih enot in stanje odgovora posamezne enote.

Slika 40: Baza "webge"

Bazo »preverjanje«, slika 41, uporabljamo za preverjanje podvojenih odgovorov v aktualnem glasovanju in se po zaključku glasovanja pobriše. V polje ključ (»key«) vpišemo vrednost sestavljeno iz ID vprašanja in ID Socket povezave (s tem zagotovimo enolično vrednost polja ključ). V polje vrednost (»value«) z uporabo JSON-a vpišemo podatke o prejetem odgovoru: ID vprašanja (VprID), prejet odgovor (Odg), časovni žig v berljivem formatu (ts), časovni žig v milisekundah, pretečenih od 1.1.1970 (ts2) in ID Socket povezave naprave, s katere smo prejeli odgovor (SocketID). Polje vrednost uporabljamo pri preverjanju ali prejeti odgovor v bazi »odgovori« že obstaja. Če odgovor obstaja, obstoječi odgovor izbrišemo in zapišemo na novo prejet odgovor. V primeru, da odgovor ne obstaja, zapišemo prejeti odgovor. S tem zagotovimo, da posamezni uporabnik na posamezno vprašanje v aktivnem glasovanju ne more oddati več odgovorov in s tem vplivati na izid glasovanja.

Slika 41: Baza "preverjanje"

4.4. SPLETNI VMESNIK – WEBPAGE.HTML

Spletni vmesnik je z uporabo platforme AngularJS narejen kot enostranska aplikacija (»single-page application«). Za uporabo AngularJS potrebujemo knjižnico angular.js, ki jo z uporabo Express-a prek strežnika vključimo v spletni vmesnik. Slika 42 prikazuje dodajanje knjižnice v strežnik, slika 43 pa

vključevanje v spletni vmesnik z uporabo značke <script

src="..."></script>

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 27

express.get('/src/angular.js', function(req, res)

res.sendFile(__dirname + '/src/angular.js');

);

Slika 42: Vključevanje knjižnice angular.js v strežnik

<script src="/src/angular.js"></script>

Slika 43: Vključevanje knjižnice angular.js v spletni vmesnik

Za večjo preglednost programske kode smo Angular krmilnike (controllers)

prestavili v ločeno datoteko app.js, ki smo jo z uporabo značke <script

src="..."></script>, kar prikazuje slika 44, vključili v spletni vmesnik.

<script src="app.js"></script>

Slika 44: Vključevanje app.js v spletni vmesnik

Datoteka webpage.html predstavlja osnovo, okoli katere je zgrajena AngularJS aplikacija. V html (https://www.w3schools.com/html/, 13.5.2017) delu zajema vključevanje vseh potrebnih knjižnic za delovanje spletnega vmesnika, datotek CSS (https://www.w3schools.com/css/, 13.5.2017) za oblikovanje vmesnika in navigacijsko vrstico vmesnika. V JavaScript delu zajema funkcije za

komunikacijo s fizičnimi glasovalnimi enotami,

komunikacijo s strežnikom z uporabo Socket.io,

osveževanje in povezavo s spletnim vmesnikom. 4.4.1. DOSTOP DO SPLETNEGA VMESNIKA

Za dostop do spletnega vmesnika moramo napravo najprej povezati na brezžično omrežje usmerjevalnika. V ta namen smo na usmerjevalniku kreirali dve brezžični omrežji: »glasovanje« in »glasovanje-guest«. V URL vrstico brskalnika vpišemo IP-naslov strežnika 192.168.42.50 in dodamo vrata 8080: »http://192.168.42.50:8080«. Odpre se nam domača stran glasovalnega sistema, prikazana na sliki 45:

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 28

Slika 45: Domača stran glasovalnega sistema (home.html)

4.4.2. NAČRT SPLETNEGA VMESNIKA

Slika 46: Načrt spletnega vmesnika

Slika 46 prikazuje načrt spletnega vmesnika. Razlaga namena posameznih strani in podstrani:

Domača stran (home.html): prikazuje osnovna navodila za uporabo in meni za izbiro posamezne podstrani (meni je vključen na vseh podstraneh).

Spletni vmesnik (webpage.html)

Domov (home.html)

Prijava v administrativni

vmesnik (login.html)

Odjava iz administrativnega

vmesnika

Vprašanje (show-question.html)

Spletna glasovalna enota

(submit-vote.html)

Dodaj vprašanje (add-question.html)

Izpis vseh vprašanj (all-questions.html)

Odgovori/rezultati (answers.html)

Statistika rezultatov (statistics.html)

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 29

Prijavna stran (login.html): vsebuje polji za vpis uporabniškega imena in gesla. S prijavo si omogočimo dostop do administrativnega vmesnika glasovalnega sistema.

Vprašanje (show-question.html): stran je del administrativnega vmesnika. Na njej pričnemo in ustavimo glasovanje, izbiramo med vprašanji in imamo nadzor nad povezanimi napravami (fizičnimi in spletnimi glasovalnimi enotami), ter oddanimi odgovori povezanih naprav.

Spletna glasovalna enota (submit-vote.html): stran je dostopna brez prijave in je namenjena za oddajo glasov prek povezanih mobilnih naprav glasovalcev. Vsebuje vprašanje, stanje glasovanja, 3 radio gumbe za izbiro odgovora, gumb za oddajo izbranega odgovora in polje za izpis povratne informacije.

Dodaj vprašanje (add-question.html): stran je namenjena za dodajanje novih vprašanj in vsebuje polje za vnos vprašanja in gumb za potrditev oddaje novega vprašanja. Stran je del administrativnega vmesnika in za dostop je potrebna prijava.

Izpis vseh vprašanj (all-questions.html): stran vsebuje tabelo, v kateri se izpišejo vsa vprašanja v bazi. Tabela vsebuje iskalnik, kjer lahko posamezno vprašanje poiščemo po ID-ju ali vsebini (tekstu) vprašanja in gumb za brisanje vprašanja iz baze. V primeru brisanja vprašanja se iz baze odgovorov pobrišejo tudi vsi povezani odgovori. Stran je del administrativnega vmesnika in za dostop je potrebna prijava.

Odgovori/rezultati (answers.html): stran vsebuje tabelo, v kateri se izpišejo vsi odgovori v bazi. Tabela vsebuje iskalnik po izpisani vsebini in omogoča iskanje po:

o ID vprašanja – prikaže vse odgovore, vezane na posamezno vprašanje;

o vsebini (tekstu) celotne tabele; o vsebini izbranega stolpca – iz padajočega menija izberemo stolpec,

po katerem želimo iskati in v polje vpišemo iskano besedilo; Tabela vsebuje tudi gumb za brisanje posameznega/izbranega vprašanja. Stran je del administrativnega vmesnika in za dostop je potrebna prijava.

Statistika rezultatov (statistic.html): stran vsebuje tabelo, ki prikazuje število prejetih odgovorov na posamezno vprašanje in statistično razporeditev odgovorov glede na posamezno vprašanje. Tabela vsebuje iskalnik, ki omogoča iskanje po ID vprašanja, vsebini vprašanja in vsebini celotne tabele. Stran je del administrativnega vmesnika in za dostop je potrebna prijava.

Domača stran glasovalnega sistema (home.html) in Spletna glasovalna enota (submit-vote.html) sta dosegljivi vsem, ki so povezani na brezžični vmesnik usmerjevalnika. Za dostop do ostalih podstrani (Vprašanje, Dodaj vprašanje, Izpis vseh vprašanj, Odgovori/rezultati in Statistika rezultatov) se je treba predhodno prijaviti, kar naredimo na podstrani Prijava v administrativni vmesnik (login.html). Odjavo naredimo s klikom na Odjava iz administrativnega vmesnika. Z dodajanjem prijave smo omejili dostop do administrativnega vmesnika, slika 47, kjer se upravlja z vprašanji in odgovori ter komunikacijo s fizičnimi in spletnimi glasovalnimi enotami.

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 30

Slika 47: stran Vprašanje (show-question.html) s tabelo povezanih glasovalnih

enot

4.4.3. KOMUNIKACIJA SPLETNEGA VMESNIKA S FIZIČNIMI GLASOVALNIMI

ENOTAMI

Sliki 48 in 49 vsebujeta programsko kodo za komunikacijo spletnega vmesnika s fizično glasovalno enoto. Ker ima vsaka glasovalna enota svoj IP-naslov, je treba prikazan izpis kode uporabiti za vsako glasovalno enoto posebej. V diplomskem delu uporabljamo štiri fizične glasovalne enote, zato je izpis ponovljen štirikrat in ustrezno prilagojen za vsako glasovalno enoto posebej. 01 function connectGE00() // povezava z glasovalno enoto 00, test

povezave in reconnect 02 var connection00Test = true, odgGE00; 03 var connection00 = new WebSocket('ws://192.168.42.70:81/',

['arduino']); // connection00 - ESP8266-0 04 connection00.onopen = function() 05 connection00.send(0); 06 connIDGE00 = new Date().getTime()+"-GE00"; 07 ; 08 testConnection00 = setInterval(function() 09 if (connection00Test && connection00.readyState != 0) 10 connection00.send(0); 11 connection00Test=false; 12 if (location.href.includes('show-question')) 13 document.getElementById("ge00Connection").value="Test povezave"; 14 15 else if (!connection00Test)

16 if (location.href.includes('show-question')) 17 document.getElementById("ge00Connection").value="Povezava

prekinjena! Vzpostavljanje povezave..."; 18

Slika 48: Komunikacija spletnega vmesnika s fizično glasovalno enoto 1/2

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 31

19 connIDGE00="clearConnIDGE00"; 20 clearInterval(testConnection00); 21 connectGE00(); 22 23 , 5000); 24 connection00.onerror = function(error) 25 console.log('WebSocket Error ', error); 26 ; 27 connection00.onclose = function() 28 clearInterval(testConnection00); 29 connectGE00(); 30 ; 31 connection00.onmessage = function(msg) 32 vrednostGE00 = parseInt(msg.data); 33 if (vrednostGE00 == 42) 34 connection00Test=true; 35 if (location.href.includes('show-question')) 36 setTimeout(function() 37 document.getElementById("ge00Connection").value="OK"; 38 , 500); 39 40 else if (vrednostGE00 != 0 && typeof(vrednostGE00) != "undefined" &&

location.href.includes('show-question')) 41 if (zapStVpr == 0) // preverjanje, da je izpisano vprašanje in da

se ne nahaja na 0. vprašanju 42 ge00odg = "Prazen odgovor. Počakaj na vprašanje..."; 43 else 44 // connection00.send(vrednostGE00); // povratna informacije ESP-ju,

katero LED naj prižge 45 if (vrednostGE00==1)

46 odgGE00="Se strinjam"; 47 else if (vrednostGE00==2) 48 odgGE00="Vzdržan"; 49 else if (vrednostGE00==3) 50 odgGE00="Se ne strinjam"; 51 52 timeStamp(); 53 ge00odg = "Ob "+hms+" "+ge00odgCnt+". prejem odgovora na izpisano

vprašanje."; 54 socket.emit("socketPisanjeOdg", "Odg":odgGE00,"SocketID":con-

nIDGE00); 55 ge00odgCnt++; 56 57 document.getElementById("btnRewriteGE00odg").click(); 58 59 ; 60 socket.on("socketOdgGE00Zapisan", function() 61 connection00.send(vrednostGE00); // povratna informacije ESP-ju, katero

LED naj prižge 62 ); 63

Slika 49: Komunikacija spletnega vmesnika s fizično glasovalno enoto 2/2

Komunikacijo med spletnim vmesnikom in glasovalno enoto vzpostavlja funkcija

»connectGE00« in poteka prek WebSocketa. Le-tega definiramo s spremenljivko

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 32

»connection00« (vrstica 1) ter določimo IP-naslov glasovalne enote in vrata, na katerih glasovalna enota posluša za WebSocket komunikacijo.

Za vzpostavitev komunikacije uporabimo funkcijo »connection00.onopen« (vrstica 4) in glasovalni enoti pošljemo sporočilo, ki vsebuje tekst »0« ter določimo ID aktualne povezave. S tem nadomestimo ID Socket.io-ja, ki se uporablja za spletne glasovalne enote. Če je povezava z glasovalno enoto vzpostavljena, prejmemo povratni odgovor, ki

vsebuje tekst »42«, kar obravnavamo v funkciji »connection00.onmessage« (vrstica 31).

Za določanje ID povezave uporabimo funkcijo »Date().getTime()« (vrstica 6), ki nam vrne pretečen čas v milisekundah od 1.1.1970 do trenutka klica funkcije. Rezultatu dodamo še oznako »-GE00« (vrstica 6). Na tak način dobimo unikatni ID povezave za posamezno glasovalno enoto. V primeru, da glasovalna enota iz kakršnegakoli razloga izgubi povezavo s spletnim vmesnikom, se ob ponovni vzpostavitvi povezave generira nov ID povezave.

S funkcijo »testConnection00« (vrstica 8) periodično preverjamo dosegljivost glasovalne enote, kar izvedemo s pošiljanjem sporočila, ki vsebuje tekst »0«. Za čas testiranja povezave se v administrativnem vmesniku izpiše obvestilo »Test povezave« (vrstica 13). Če glasovalna enota ne odgovori na poslano sporočilo, se v administrativnem vmesniku izpiše obvestilo »Povezava prekinjena! Vzpostavljanje povezave…« (vrstica 17).

Funkcija »connection00.onerror« (vrstica 24) vrne izpis morebitne napake pri WebSocket komunikaciji

Funkcija »connection00.onclose« (vrstica 27) ob prekinitvi WebSocket

povezave prekine funkcijo »testConnection00« in ponovno pokliče glavno

funkcijo »connectGE00« za vzpostavitev celotne komunikacije z glasovalno enoto. Za odziv na poslana sporočila glasovalne enote skrbi funkcija

»connection00.onmessage« (vrstica 31). Odziv je odvisen od vsebine sporočila glasovalne enote. Če sporočilo vsebuje tekst »42« (vrstica 33), je glasovalna enota odgovorila na test povezave in v administrativni vmesnik izpišemo obvestilo »OK« (vrstica 37). Ker je odgovor na poslano sporočilo skoraj instanten, izpis potrditve dosegljivosti zakasnimo za 500 milisekund. V primeru, da se izvajanje ne ustavi pri prvem pogoju, v nadaljevanju preverjamo, če sporočilo vsebuje tekst »1«, »2« ali »3« (vrstice 45, 47 in 49), kar pomeni, da je uporabnik pritisnil tipko za izbran odgovor. Izbrani odgovor zapišemo v

spremenljivko »odgGE00« (vrstice 46, 48 in 50) in posredujemo na administrativni vmesnik kot obvestilo o prejetem odgovoru. Odgovor z uporabo Socket.io posredujemo tudi strežniku (vrstica 54), ki poskrbi za zapis odgovora v bazo. Po prejemu potrditve zapisa odgovora v bazo (vrstica 60), glasovalni enoti s funkcijo

»connection00.send()« (vrstica 61) posredujemo podatek, katera LED-ica naj utripne, da uporabnik dobi povratno informacijo o zapisu oddaje odgovora.

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 33

V posameznih delih kode je dodana omejitev izvajanja kode

(location.href.includes('show-question')), če se uporabnik ne nahaja na strani za prikaz vprašanja. 4.4.4. PREVERJANJE DOSEGLJIVOSTI SPLETNIH GLASOVALNIH ENOT

Administrativni vmesnik vsebuje tabelo, ki prikazuje vse trenutno povezane spletne glasovalne enote - lastne naprave glasovalcev, ki imajo v brskalniku odprt spletni vmesnik glasovalnega sistema. Tabela voditelju glasovanja omogoča vpogled v število povezanih glasovalnih enot, da ve, koliko odgovorov lahko pričakuje. V tabeli se poleg dosegljivosti glasovalne enote izpisuje tudi podatek, če je posamezna glasovalna enota že oddala oziroma kolikokrat je oddala odgovor na izpisano vprašanje. Slika tabele je prikazana na sliki 47 v poglavju 4.4.4. Načrt spletnega vmesnika. Ob dostopu naprave do spletnega vmesnika glasovalnega sistema se prek SocketIO vzpostavi povezava – Sliki 50 in 51. Ob vzpostavitvi povezave se preveri, če je IP naprave že v tabeli povezanih naprav. Če IP-ja ni na seznamu, se napravo vpiše v tabelo in poveča števec povezanih naprav. io.sockets.on("connection", function(socket) // pridobivanje IP-naslova klienta in socket ID cIP=socket.request.connection.remoteAddress.substring(7); // substring

odreže prvih 7 znakov (":ffff::") cSocketID = socket.id; clientRedis.hget("webge", cIP, function(err, reply) // preverjanje

podvojenosti dostopa z istega IP

Slika 50: Vzpostavitev povezave z glasovalno enoto (1/2)

if (reply === null) if (socket.request.connection.remoteAddress.substring(7) != serverIP)

cNum++; clientRedis.hset("webge", cIP,

'"IP":"'+cIP+'","SocketID":"'+cSocketID+'","hms":"99:99:99","stanjeOdg":"0

"'); console.log("New user has connected. Socket ID: "+socket.id+". IP:

"+socket.request.connection.remoteAddress.substring(7)+". Total users:

"+cNum+"."); if (glasovanjeStartStop) socket.emit("socketStartStopGlasovanjeIzpis", 1); else if (!glasovanjeStartStop) socket.emit("socketStartStopGlasovanjeIzpis", 0); else socket.emit("socketDuplicatedSocketID"); duplicatedSocketID=true; io.sockets.connected[cSocketID].disconnect(); console.log("Podvojen dostop naprave z IP: "+cIP+". Nov SocketID

podvojene naprave: "+cSocketID+". Povezava novega SocketID prekinjena!"); duplicatedSocketID=false; );

Slika 51: Vzpostavitev povezave z glasovalno enoto (2/2)

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 34

Če je IP že na seznamu sklepamo, da je naprava že povezana v spletni vmesnik prek drugega brskalnika ali v drugem zavihku istega brskalnika. V tem primeru uporabniku izpišemo obvestilo o podvojeni povezavi (Slika 52) in novo SocketIO povezavo prekinemo.

Slika 52: Obvestilo o podvojeni povezavi

V tabeli povezanih glasovalnih enot ne želimo imeti strežnika, s katerim dostopamo do administrativnega vmesnika. V ta namen smo dodali varovalko (pogoj), ki preprečuje dodajanje IP-naslova strežnika v tabelo. IP-naslov strežnika zapišemo v spremenljivko »serverIP«. Ko naprava zapusti spletni vmesnik glasovalnega sistema, se prek SocketIO sproži ukaz »disconnect«, ki iz tabele povezanih naprav pobriše IP-naslov naprave – Slika 53. socket.on("disconnect", function() if (!duplicatedSocketID &&

socket.request.connection.remoteAddress.substring(7) != serverIP) clientRedis.hdel("webge",

socket.request.connection.remoteAddress.substring(7)); cNum--; console.log("User has disconnected. Socket ID: "+socket.id+". IP:

"+socket.request.connection.remoteAddress.substring(7)+". Total users:

"+cNum+"."); );

Slika 53: Funkcija "disconnect", ko naprava zapusti spletni vmesnik

Slika 54 prikazuje programsko kodo, ki z uporabo funkcije »setInterval« vsake 3 sekunde osveži tabelo povezanih naprav.

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 35

function WebGERefresh()

setInterval(function()

if (location.href.includes('show-question'))

socket.emit("socketWebGEF5");

console.log("interval 3s");

, 3000);

Slika 54: Funkcija, ki na 3 sekunde osveži stanje v tabeli povezanih naprav

4.5. ANGULARJS KRMILNIKI – APP.JS

AngularJS za delovanje uporablja krmilnike (controllers) (https://www.w3schools.com/angular/angular_controllers.asp/, 13.5.2017). Krmilniki so JavaScript objekti, ki krmilijo podatke in delovanje AngularJS aplikacij. Vsaka podstran AngularJS aplikacije ima lahko svoj krmilnik, si ga deli z drugo

podstranjo ali pa uporabi več krmilnikov. Krmilnike se definira z značko »ng-

controller«. Primer določanja krmilnika: <div ng-controller="myCtrl">. Slika 55 prikazuje določanje krmilnikana strani Vprašanje.

<div id="divVprasanje", ng-controller="QuestionController">

Slika 55: Primer določanja krmilnika na strani Vprašanje (show-question.html)

V spletnem vmesniku vsaka stran uporablja svoj krmilnik za prikaz strani, delijo pa si skupen krmilnik za prikaz pojavnih (popup) obvestil. Glede na vsebino strani imajo krmilniki različne naloge. Nekateri skrbijo samo za

prikaz strani ob prihodu na stran - primer: StatisticsController (slika 56), ki skrbi za prikaz tabele na strani Statistika rezultatov… app.controller('StatisticsController', function($scope, $filter, $route,

$uibModal)

$scope.odgovori = statOdg;

$scope.rewriteStatistika = function()

return $scope.odgovori = statOdg,

$route.reload();

;

$scope.predicates = ['ID vprašanja', 'Vprašanje'];

$scope.selectedPredicate = $scope.predicates[0];

);

Slika 56: Primer krmilnika "StatisticsController" iz app.js

…drugi pa se prožijo na podlagi uporabnikove interakcije – primer:

AnswersController (slika 57), ki skrbi za prikaz tabele odgovorov, ima pa še

dodatno funkcijo removeRow, ki po uporabnikovem kliku na ikono za brisanje posameznega odgovora prikaže pojavno okno, ki preprečuje brisanje odgovorov po pomoti. Če uporabnik potrdi brisanje odgovora, krmilnik poskrbi za brisanje izbranega odgovora iz baze odgovorov.

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 36

app.controller('AnswersController', function($scope, $filter, $route,

$uibModal)

$scope.podatki = rezultati;

$scope.rewrite = function()

return $scope.podatki = rezultati,

$route.reload(); // za osvežitev podatkov v expression-ih. Brez tega ne

deluje iskanje po tabeli takoj po izpisu.

;

$scope.removeRow = function removeRow(podatek) // Brisanje posamezne

vrstice v izpisani tabeli

removeRowPodatek = podatek;

var modalInstance = $uibModal.open(

templateUrl: '/pages/popup/delete-warning.html',

controller: 'CtrlRmRow',

controllerAs: '$ctrl',

windowClass: 'app-modal-window' // Uporablja se v povezavi s CSS za

določanje izgleda

);

;

$scope.predicates = ['ID vprašanja', 'Odgovor', 'Čas oddaje odgovora',

'Časovni žig', 'ID Socket povezave'];

$scope.selectedPredicate = $scope.predicates[0];

);

Slika 57: Primer krmilnika "AnswersController" iz app.js

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 37

5. ZAGON SPLETNEGA GLASOVANJA

Zagon spletnega glasovanja izvedemo prek terminala, s katerim se premaknemo v mapo »glasovanje«. Pred zagonom spletnega glasovanja moramo zagnati strežnik Redis (slika 58 prikazuje ukaz za zagon strežnika Redis), ki skrbi za dosegljivost baze podatkov Redis. Za zagon strežnika Redis se pomaknemo v mapo »/glasovanje/redis«, ki vsebuje shranjeno bazo podatkov (dump.rdb). redis-server

Slika 58: Ukaz za zagon Redis strežnika

Slika 59 prikazuje terminal v katerem smo v spodnjem delu zagnali strežnik Redis, v zgronjem delu pa strežnik spletnega glasovanja.

Slika 59: Terminal za zagon strežnikov Redis-a in spletnega glasovanja

Slika 60 prikazuje ukaz za zagon strežnika spletnega glasovanja. node server.js

Slika 60: Ukaz za zagon strežnika spletnega glasovanja

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 38

Ko sta strežnik Redis in strežnik spletnega glasovanja zagnana, zaženemo spletni brskalnik in odpremo spletno stran na naslovu http://192.168.42.50:8080. Odpre se osnovna stran spletnega glasovanja. Pred zagonom glasovanja se moramo prijaviti, da lahko dostopamo do administrativnega vmesnika. Prijavo opravimo na strani »Prijava v administrativni vmesnik« (Slika 61). Prednastavljeni podatki za prijavo so: uporabniško ime: »admin«, geslo: »riba«.

Slika 61: Prijavna stran za dostop do administrativnega vmesnika

Po prijavi odpremo stran Vprašanje (slika 62), na kateri se nahajajo podatki: o številu vprašanj v bazi, trenutno vprašanje, stanje glasovanja, gumbi za upravljanje glasovanja in kontrolni tabeli povezanih naprav spletnega glasovanja, prek katerih uporabniki oddajajo glasove. Z gumbom »Začni/ustavi glasovanje« zaženemo in ustavimo glasovanje. Sprememba stanja glasovanja se beleži pri oznaki »Stanje glasovanja« in se prenese na vse povezane spletne in fizične glasovalne enote. V primeru oddaje glasu ob ustavljenjem glasovanju, uporabnik dobi obvestilo. Z gumboma »Prejšnje vprašanje« in »Naslednje vprašanje« prehajamo med vprašanji v bazi podatkov. Pod gumbi se nahajata dve tabeli. Prva je statična in prikazuje stanje povezave fizičnih glasovalnih enot ter stanje odgovora. Osveževanje poteka na 5 sekund za GE01 in dodatnih 100 milisekund zakasnitve za vsako naslednjo glasovalno enoto. Druga tabela je dinamična in prikazuje spletne glasovalne enote. Vsebuje podatke o IP-naslovu glasovalne enote, SocketID povezavi in stanju odgovora. Osveževanje celotne tabele poteka na 3 sekunde. Stolpec stanje odgovora v obeh tabelah vsebuje podatek, kolikokrat je uporabnik na izpisano vpašanje oddal odgovor in kdaj je oddal zadnji odgovor. Pri končnem rezultatu se upošteva le zadnji oddani odgovor posameznega uporabnika.

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 39

Slika 62: Stran Vprašanje z gumbi za upravljanje glasovanja in kontrolnimi

elementi

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 40

6. ZAKLJUČEK

V okviru diplomskega dela smo razvili programsko in strojno opremo, ki omogoča izvedbo glasovanja in temelji na odprtokodnih programskih in strojnih elementih. Ker je tudi sama rešitev odprtokodna, lahko vsak pripravi ustrezne strojne elemente in vzpostavi sistem glasovanja za lastne potrebe. Rešitev je namenjena lokalni uporabi in za delovanje ne potrebuje povezave v splet. S tem povečamo uporabnost rešitve za primere, ko želi organizator glasovanja sistem uporabiti na lokacijah, kjer nima enostavne povezave v splet (npr. razne prireditve in dogodki). Rešitev tudi ni omejena z operacijskim sistemom. Strežniški del lahko zaženemo na kateremkoli operacijskem sistemu, ki podpira delovanje baze podatkov Redis in Node.js, kar zajema večino modernih operacijskih sistemov. Enako velja za uporabniški del (klient), saj je za povezavo s spletno glasovalno enoto potrebna naprava, ki omogoča brezžično povezavo in uporablja posodobljen internetni brskalnik.

Razvita rešitev omogoča zbiranje glasov (mnenj) uporabnikov/udeležencev, pri čemer lahko uporabimo namenske fizične glasovale enote in spletne glasovalne enote. Do spletnih glasovalnih enot uporabniki dostopajo prek svojih mobilnih naprav, ki se povežejo z, v ta namen pripravljenim, brezžičnim omrežjem. Spletna glasovalna enota je dosegljiva vsem napravam, ki so povezane v brezžično omrežje glasovalnega sistema in imajo nameščen ustrezen internetni brskalnik. Upravljanje in nadzor nad potekom glasovanja je omogočen prek namenskega spletnega administrativnega vmesnika. Za dostop moramo poznati uporabniško ime in geslo. Adminstrativni vmesnik omogoča dodajanje in brisanje vprašanj, brisanje posameznih odgovorov in statistični pregled prejetih odgovorov na posamezno vprašanje. Trenutna različica rešitve dopušča možnosti za nadaljnji razvoj in eventualno možnost uporabe prek spleta, kar bi dodatno povečalo krog uporabe glasovalnega sistema. Možnost nadaljnjega razvoja vidimo tudi v nadgradnji avtentikacije uporabnika s strani fizičnih in spletnih glasovalnih enot, da bo glasovalni sistem uporaben tudi v primerih, ko moramo uporabnika enolično prepoznati in preveriti, če lahko na zastavljeno vprašanje odda svoj glas (npr. volitve).

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 41

LITERATURA IN VIRI AngularJS, https://angularjs.org/ (13.5.2017)

Arduino IDE, https://www.arduino.cc/en/main/software/ (27.2.2017)

Ardunino.h https://github.com/arduino/Arduino/blob/master/hardware/arduino/avr/cores/arduino/Arduino.h/ (26.4.2017)

Programski jezik C, https://www.tutorialspoint.com/cprogramming/ (13.5.2017)

DD-WRT, https://www.dd-wrt.com/site/ (13.5.2017)

ESP8266, http://www.esp8266.com/ (20.4.2017) in https://en.wikipedia.org/wiki/ESP8266/ (21.4.2017)

ESP8266WiFi.h, https://github.com/esp8266/Arduino/blob/master/libraries/ESP8266WiFi/src/ESP8266WiFi.h/ (20.2.2017)

Express, https://expressjs.com/ (13.5.2017) in https://www.npmjs.com/package/express/ (13.5.2017)

Fritzing, http://fritzing.org/home/ (13.5.2017)

Fritzing - NodeMCU model, https://github.com/squix78/esp8266-fritzing-parts/tree/master/nodemcu-v1.0/ (27.2.2017)

Infoworld - Why Redis beats Memcached for caching, http://www.infoworld.com/article/3063161/application-development/why-redis-beats-memcached-for-caching.html/ (10.7.2017)

Node.js, http://nodejs.org/dist/v4.2.6/node-v4.2.6.tar.gz/ (27.2.2017)

NodeMCU, http://nodemcu.com/ (21.4.2017) in https://en.wikipedia.org/wiki/NodeMCU/ (21.4.2017)

Redis, http://download.redis.io/redis-stable.tar.gz/ (27.2.2017)

Socket.io, https://socket.io/ (27.4.2017), https://github.com/socketio/socket.io/ (27.4.2017) in https://en.wikipedia.org/wiki/Socket.IO/ (27.4.2017)

Škraba A., Koložvari A., Kofjač D., Stojanović R., Stanovov V., Semenkin E., »Streaming pulse data to the cloud with bluetooth LE or NODEMCU ESP8266«, 2016 5th Mediterranean Conference on Embedded Computing (MECO), Bar, 2016, pp. 428-431. doi: 10.1109/MECO.2016.7525798

Škraba A., Koložvari A., Kofjač D., Stojanović R., Stanovov V., Semenkin E., »Prototype of group heart rate monitoring with NODEMCU ESP8266«, 2017 6th Mediterranean Conference on Embedded Computing (MECO), Bar, 2017, pp. 534-537. doi: 10.1109/MECO.2017.7977151

Ubuntu 14.04 LTS 64-bit, https://www.ubuntu.com/ (13.1.2017)

WebSocket, https://www.websocket.org/ (13.5.2017) in https://en.wikipedia.org/wiki/WebSocket/ (13.5.2017)

WebSocketsServer.h, https://github.com/Links2004/arduinoWebSockets/blob/master/src/WebSocketsServer.h/ (13.5.2017)

w3schools - AngularJS Tutorial, https://www.w3schools.com/angular/ (13.5.2017)

w3schools - AngularJS Controllers, https://www.w3schools.com/angular/angular_controllers.asp/ (13.5.2017)

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 42

w3schools - JavaScript Tutorial, https://www.w3schools.com/js/ (13.5.2017)

w3schools - HTML5 Tutorial, https://www.w3schools.com/html/ (13.5.2017)

w3schools - CSS Tutorial, https://www.w3schools.com/css/ (13.5.2017)

w3schools - Node.js Tutorial, https://www.w3schools.com/nodejs/ (13.5.2017)

w3schools - JSON - Introduction, https://www.w3schools.com/js/js_json_intro.asp/ (13.5.2017)

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 43

PRILOGE Programska koda zadnje različice razvite rešitve. Koda je razdeljena po posameznih sklopih.

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 44

KAZALO SLIK Slika 1: Prijava v administrativni vmesnik ................................................ 4 Slika 2: Shema sistema ....................................................................... 5 Slika 3: NodeMCU V1.0 Amica ............................................................... 6 Slika 4: Arduino IDE ........................................................................... 7 Slika 5: Brezžični omrežji glasovalnega sistema ........................................ 10 Slika 6: DHCP rezervacija IP-naslovov na usmerjevalniku ............................. 11 Slika 7: Posodobitev operacijskega sistema.............................................. 12 Slika 8: Nameščanje osnovnih komponent sistema ..................................... 12 Slika 9: Nameščanje paketa npm .......................................................... 12 Slika 10: Ukazi za prenos in namestitev Node.js ........................................ 13 Slika 11: Ukaz za preverjanje nameščene različice Node.js .......................... 13 Slika 12: Ukaz za namestitev Socket.IO .................................................. 13 Slika 13: Ukaz za preverjanje nameščene različice Socket.IO ........................ 13 Slika 14: Ukazi za prenos, razširitev in izgradnjo Redis-a iz izvorne kode .......... 13 Slika 15: Ukaz za test zgrajenega objekta za nameščanje Redis-a ................... 13 Slika 16: Ukaz za namestitev Redis-a ..................................................... 13 Slika 17: Ukaz za zagon Redis strežnika .................................................. 13 Slika 18: Ukaz za preverjanje delovanja Redis strežnika .............................. 14 Slika 19: Preverjanje delovanja Redis strežnika in prejet odgovor .................. 14 Slika 20: Ukaz za nameščanje Redis kompomente za Node.js ........................ 14 Slika 21: Nameščanje knjižnice Express .................................................. 14 Slika 22: Skica priklopa tipk in LED diod na prototipni plošči ......................... 16 Slika 23: Shema vezave tipk in LED diod z NodeMCU ................................... 16 Slika 24: Prototip 1 in prikaz vezave ...................................................... 17 Slika 25: Prototip 2 in prikaz vezave ...................................................... 17 Slika 26: Programska koda za NodeMCU 1/2 ............................................. 18 Slika 27: Programska koda za NodeMCU 2/2 ............................................. 19 Slika 28: Zunanje knjižnice, ki jih strežnik in spletni vmesnik potrebujeta za delovanje ..................................................................................... 20 Slika 29: Izsek kode vključevanja povezanih datotek .................................. 21 Slika 30: Vzpostavitev spletnega strežnika............................................... 21 Slika 31: Izsek kode za vzpostavitev Socket.io komunikacije ......................... 21 Slika 32: Prikaz komunikacije strežnika z bazo podatkov Redis ...................... 21 Slika 33: Komunikacija strežnika prek Socket.io s povezanimi klienti ............... 22 Slika 34: Primer kode za obdelovanje zbranih podatkov (1/2) ........................ 22 Slika 35: Primer kode za obdelovanje zbranih podatkov (2/2) ........................ 23 Slika 36: Funkcija "lastVprID" .............................................................. 24 Slika 37: Funkcija "stOdgPosameznoVpr" ................................................. 24 Slika 38: Baza "vprasanja" .................................................................. 25 Slika 39: Baza "odgovori" .................................................................... 25 Slika 40: Baza "webge" ...................................................................... 26 Slika 41: Baza "preverjanje" ................................................................ 26 Slika 42: Vključevanje knjižnice angular.js v strežnik ................................. 27 Slika 43: Vključevanje knjižnice angular.js v spletni vmesnik ........................ 27 Slika 44: Vključevanje app.js v spletni vmesnik ........................................ 27 Slika 45: Domača stran glasovalnega sistema (home.html) ............................ 28 Slika 46: Načrt spletnega vmesnika ....................................................... 28 Slika 47: stran Vprašanje (show-question.html) s tabelo povezanih glasovalnih enot ................................................................................................. 30 Slika 48: Komunikacija spletnega vmesnika s fizično glasovalno enoto 1/2 ......... 30

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 45

Slika 49: Komunikacija spletnega vmesnika s fizično glasovalno enoto 2/2 ......... 31 Slika 51: Vzpostavitev povezave z glasovalno enoto (1/2)............................. 33 Slika 52: Vzpostavitev povezave z glasovalno enoto (2/2)............................. 33 Slika 53: Obvestilo o podvojeni povezavi................................................. 34 Slika 54: Funkcija "disconnect", ko naprava zapusti spletni vmesnik ................ 34 Slika 55: Funkcija, ki na 3 sekunde osveži stanje v tabeli povezanih naprav ....... 35 Slika 56: Primer določanja krmilnika na strani Vprašanje (show-question.html)... 35 Slika 57: Primer krmilnika "StatisticsController" iz app.js ............................. 35 Slika 58: Primer krmilnika "AnswersController" iz app.js .............................. 36 Slika 59: Ukaz za zagon Redis strežnika .................................................. 37 Slika 60: Terminal za zagon strežnikov Redis-a in spletnega glasovanja ............ 37 Slika 61: Ukaz za zagon strežnika spletnega glasovanja ............................... 37 Slika 62: Prijavna stran za dostop do administrativnega vmesnika ................... 38 Slika 63: Stran Vprašanje z gumbi za upravljanje glasovanja in kontrolnimi elementi ...................................................................................... 39

Univerza v Mariboru – Fakulteta za organizacijske vede Diplomsko delo univerzitetnega študija

Gašper Pintar: Razvoj interaktivnega glasovalnega sistema s spletnim in strojnim vmesnikom stran 46

POJMOVNIK

Power bank: prenosni polnilec različnih kapacitet, ki se večinoma uporablja za polnjenje mobilnih telefonov, ko v bližini ni električne vtičnice.

Single page application (SPA): spletna aplikacija narejena z uporabo AngularJS, ki se naloži kot ena stran.

Odprta koda (open-source): programska koda/rešitev, ki je dana v prosto uporabo vsem, ki jo želijo uporabiti. Za uporabo ni potrebno plačati nadomestila/kupiti licence.

JavaScript: programski jezik.

AngularJS: framework za izdelavo single page application.

NodeMCU: odprtokodna IoT platforma.

ESP8266: čip z Wi-Fi vmesnikom in mikrokontrolerjem.

Node.js: odprtokodna platforma za poganjanje JavaScript aplikacij na strani strežnika.

Fritzing: odprtokodni program za risanje in dizajniranje elektronske strojne opreme s povezanimi komponentami.

Arduino UNO: odprtokodna elektronska plošča z mikrokontrolerjem.

WebSocket: dvosmerni komunikacijski protokol, ki se uporablja pri komunikaciji med računalniki.

DD-WRT: programska oprema za usmerjevalnike in brezžične dostopovne točke.

Arduino IDE: odprtokodni vmesnik, za pisanje, urejanje in nalaganje kode na mikrokontrolerje.

Socket.io: JavaScript spletna aplikacija, ki omogoča dvosmerno komunikacijo med strežnikom in klientom v realnem času.

Express.js (ali Express): spletna aplikacija za Node.js, ki omogoča vzpostavitev spletnega strežnika za prikaz spletne strani.

Redis: odprtokodna baza podatkov.

KRATICE IN AKRONIMI JSON: JavaScript Object Notation

LTS: Long-term support

HTML: Hypertext Markup Language

GND: Ground

USB: Universal Serial Bus

DHCP: Dynamic Host Configuration Protocol

MAC: Media access control

IP: Internet protokol

TLS: Transport Layer Security

TCP: Transmission Control Protocol

LED: Light-emitting diode

PROGRAMSKA KODA ESP8266-Connection.ino // Opis: Koda, ki se izvaja na ESP8266 in skrbi za vzpostavitev WiFi povezave z routerjem, zajem glasov in

komunikacijo s spletno stranjo.

// Verzija: 2017.01.06a

// ====================================================================================================

#include <Arduino.h>

#include <ESP8266WiFi.h>

#include "WebSocketsServer.h"

#include <Hash.h>

WebSocketsServer webSocket = WebSocketsServer(81);

const char* ssid = "glasovanje"; // replace "SSID" with wireless name (SSID)

const char* password = "DontPanic!42"; // replace "PWD" eith wireless password

boolean empty = true;

unsigned long lastBtnPressTime = 0;

unsigned long debounceDelay = 1000;

void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t lenght)

switch(type)

case WStype_DISCONNECTED:

break;

case WStype_CONNECTED:

IPAddress ip = webSocket.remoteIP(num);

break;

case WStype_TEXT:

String text = String((char *) &payload [0]);

if (text == "3")

digitalWrite(13, 1); //D7

delay(500);

digitalWrite(13, 0);

else if (text == "2") //D6

digitalWrite(12, 1);

delay(500);

digitalWrite(12, 0);

else if (text == "1") //D5

digitalWrite(14, 1);

delay(500);

digitalWrite(14, 0);

else if (text == "0")

webSocket.sendTXT(num, "42");

);

break;

case WStype_BIN:

hexdump(payload, lenght);

webSocket.sendBIN(num, payload, lenght);

break;

void setup()

pinMode(0, INPUT_PULLUP); // D3

pinMode(4, INPUT_PULLUP); // D2

pinMode(5, INPUT_PULLUP); // D1

pinMode(12, OUTPUT); // D6

pinMode(13, OUTPUT); // D7

pinMode(14, OUTPUT); // D5

Serial.begin(115200);

WiFi.begin(ssid, password);

while(WiFi.status() != WL_CONNECTED)

delay(100);

Serial.println(WiFi.localIP());

webSocket.begin();

webSocket.onEvent(webSocketEvent);

void loop()

webSocket.loop();

if (digitalRead(2) == 0) // D4-proti (13)

if((millis()-lastBtnPressTime)>debounceDelay)

webSocket.broadcastTXT("3");

lastBtnPressTime=millis();

else if (digitalRead(4) == 0) // D2-nedolocen (12)

if((millis()-lastBtnPressTime)>debounceDelay)

webSocket.broadcastTXT("2");

lastBtnPressTime=millis();

else if (digitalRead(5) == 0) // D1-za (14)

if((millis()-lastBtnPressTime)>debounceDelay)

webSocket.broadcastTXT("1");

lastBtnPressTime=millis();

webpage.html <!-- Opis: Spletna stran za prikaz zbranih glasov. -->

<!-- Verzija: 2017.04.15b -->

<!-- ==================================================================================================== -->

<!DOCTYPE html>

<meta charset=utf-8>

<html ng-app="myApp">

<head>

<script src="/src/angular.js"></script>

<script src="/src/angular-route.js"></script>

<script src="/src/jquery.min.js"></script>

<script src="/src/bootstrap.min.js"></script>

<script src="/src/bootstrap-hover-dropdown.min.js"></script>

<script src="/src/ui-bootstrap-tpls.js"></script>

<script src="/src/smart-table.js"></script>

<script src="/socket.io/socket.io.js"></script>

<link rel="stylesheet" type="text/css" href="/css/bootstrap-theme.min.css">

<link rel="stylesheet" type="text/css" href="/css/bootstrap.min.css">

<link rel="stylesheet" type="text/css" href="/css/style.css">

<style>

html, body font-size: 1em;

body padding-top: 70px;

</style>

</head>

<body>

<header>

<nav class="navbar navbar-default navbar-fixed-top">

<div class="container">

<div class="navbar-header">

<p class="navbar-brand">Glasovanje</p>

</div>

<ul class="nav navbar-nav navbar-right">

<li class="dropdown">

<a href="#/home" class="dropdown-toggle" data-toggle="dropdown" data-hover="dropdown" data-

delay="1000" data-close-others="false">

Domov

<b class="caret"></b>

</a>

<ul class="dropdown-menu">

<li><a tabindex="-1" href="#/login">Prijava v administrativni vmesnik</a></li>

<li><a tabindex="-2" href="#/" ng-click="logout()" ng-controller="LogoutController">Odjava iz

administrativnega vmesnika</a></li>

</ul>

</li>

<li class="dropdown">

<a href="#/show-question" onClick="osveziVprasanje()" class="dropdown-toggle" data-

toggle="dropdown" data-hover="dropdown" data-delay="1000" data-close-others="false">

Vprašanje

<b class="caret"></b>

</a>

<ul class="dropdown-menu">

<li><a tabindex="-1" href="#/submit-vote" onClick="osveziVprasanje()">Spletna glasovalna

enota</a></li>

</ul>

</li>

<li class="dropdown">

<a href="#/add-question" class="dropdown-toggle" data-toggle="dropdown" data-hover="dropdown" data-

delay="1000" data-close-others="false">

Dodaj vprašanje

<b class="caret"></b>

</a>

<ul class="dropdown-menu">

<li><a tabindex="-1" href="#/all-questions" onClick="izpisVprasanj()">Izpis vseh

vprašanj</a></li>

</ul>

</li>

<li class="dropdown">

<a href="#/answers" onClick="izpisRezultatov()" class="dropdown-toggle" data-toggle="dropdown"

data-hover="dropdown" data-delay="1000" data-close-others="false">

Odgovori/rezultati

<b class="caret"></b>

</a>

<ul class="dropdown-menu">

<li><a tabindex="-1" href="#/statistics" onClick="izpisStatistike()">Statistika

rezultatov</a></li>

</ul>

</li>

</ul>

</div>

</nav>

</header>

<div class='banner'></div>

<div class="container">

<div class='content' ng-view><div> <!-- div za prikaz Single page aplication -->

</div>

<script src="app.js"></script>

<script type="text/javascript" src="/socket.io/socket.io.js"></script>

<script type="text/javascript">

var odg = 'x';

var socket = io.connect("192.168.42.50:8080");

var VprID,

stVpr,

novoVpr,

potrZapOdg, // potrZapOdg-potrditev zapisa odgovora

rezultati, // rezultati-spremenljiva za izpis odgovorov v tabelo

vsaVpr, // vsaVpr-spremenljivka za zapis vprašanj v tabelo

zapStVpr, // zapStVpr-zaporedna številka vprašanja

statOdg, // spremenljivka za zapis odgovorov

url, // zapis trenutnega url-ja

hms,

WebGETabela, // spremenljiva za vrednost tabele WebGE

load=true,

startStop = false; // spremenljivka za start/stop glasovanja (false=stop, true=start)

var prazenOdg = false; // Varovalka pred praznim oddajanjem odgovora

var vprasanje = 'Za prikaz vprašanja klikni na gumb "Naslednje vprašanje".',

stanjeGlasovanja = 'Glasovanje je ustavljeno. Počakaj na začetek glasovanja.';

// spremenljvke za glasovalne enote

var vrednostGE00, vrednostGE01, vrednostGE02, vrednostGE03,

testConnection00, testConnection01, testConnection02, testConnection03,

connIDGE00, connIDGE01, connIDGE02, connIDGE03,

ge00odg = "...", ge01odg = "...", ge02odg = "...", ge03odg = "...", // spremenljivke za zapis

odgovora

ge00odgCnt=1, ge01odgCnt=1, ge02odgCnt=1, ge03odgCnt=1; // števec podanih odgovorov na trenutno

vprašanje prek GE

// POVEZAVA Z ESP8266

connectGE00();

connectGE01();

connectGE02();

connectGE03();

WebGERefresh(); // funckija za osveževanje tabele WebGE na 3 sekunde

function connectGE00() // povezava z glasovalno enoto 00, test povezave in reconnect

var connection00Test = true, odgGE00;

var connection00 = new WebSocket('ws://192.168.42.70:81/', ['arduino']); // connection00 - ESP8266-0

connection00.onopen = function()

connection00.send(0);

connIDGE00 = new Date().getTime()+"-GE00";

;

testConnection00 = setInterval(function()

if (connection00Test && connection00.readyState != 0)

connection00.send(0);

connection00Test=false;

if (location.href.includes('show-question'))

document.getElementById("ge00Connection").value="Test povezave";

else if (!connection00Test)

if (location.href.includes('show-question'))

document.getElementById("ge00Connection").value="Povezava prekinjena! Vzpostavljanje

povezave...";

connIDGE00="clearConnIDGE00";

clearInterval(testConnection00);

connectGE00();

, 5000);

connection00.onerror = function(error)

console.log('WebSocket Error ', error);

;

connection00.onclose = function()

clearInterval(testConnection00);

connectGE00();

;

connection00.onmessage = function(msg)

vrednostGE00 = parseInt(msg.data);

if (vrednostGE00 == 42)

connection00Test=true;

if (location.href.includes('show-question'))

setTimeout(function()

document.getElementById("ge00Connection").value="OK";

, 500);

else if (vrednostGE00 != 0 && typeof(vrednostGE00) != "undefined" && location.href.includes('show-

question'))

if(startStop)

if (zapStVpr == 0) // preverjanje, da je izpisano vprašanje in da se ne nahaja na 0. vprašanju

ge00odg = "Prazen odgovor. Počakaj na vprašanje...";

else

if (vrednostGE00==1)

odgGE00="Se strinjam";

else if (vrednostGE00==2)

odgGE00="Vzdržan";

else if (vrednostGE00==3)

odgGE00="Se ne strinjam";

timeStamp();

ge00odg = "Ob "+hms+" "+ge00odgCnt+". prejem odgovora na izpisano vprašanje.";

socket.emit("socketPisanjeOdg", "Odg":odgGE00,"SocketID":connIDGE00);

ge00odgCnt++;

document.getElementById("btnRewriteGE00odg").click();

else

ge00odg="Glasovanje je ustavljeno! Za oddajo glasu počakaj na zagon glasovanja.";

document.getElementById("btnRewriteGE00odg").click();

;

socket.on("socketOdgGE00Zapisan", function()

connection00.send(vrednostGE00); // povratna informacije ESP-ju, katero LED naj prižge

);

function connectGE01() // povezava z glasovalno enoto 01, test povezave in reconnect

var connection01Test = true, odgGE01;

var connection01 = new WebSocket('ws://192.168.42.71:81/', ['arduino']); // connection01 - ESP8266-1

connection01.onopen = function()

connection01.send(0);

connIDGE01 = new Date().getTime()+"-GE01";

;

testConnection01 = setInterval(function()

if (connection01Test && connection01.readyState != 0)

connection01.send(0);

connection01Test=false;

if (location.href.includes('show-question'))

document.getElementById("ge01Connection").value="Test povezave";

else if (!connection01Test)

if (location.href.includes('show-question'))

document.getElementById("ge01Connection").value="Povezava prekinjena! Vzpostavljanje

povezave...";

connIDGE01="clearConnIDGE01";

clearInterval(testConnection01);

connectGE01();

, 5100);

connection01.onerror = function(error)

console.log('WebSocket Error ', error);

;

connection01.onclose = function()

clearInterval(testConnection01);

connectGE01();

;

connection01.onmessage = function(msg)

vrednostGE01 = parseInt(msg.data);

if (vrednostGE01 == 42)

connection01Test=true;

if (location.href.includes('show-question'))

setTimeout(function()

document.getElementById("ge01Connection").value="OK";

, 500);

else if (vrednostGE01 != 0 && typeof(vrednostGE01) != "undefined" && location.href.includes('show-

question'))

if(startStop)

if (zapStVpr == 0) // preverjanje, da je izpisano vprašanje in da se ne nahaja na 0. vprašanju

ge01odg = "Prazen odgovor. Počakaj na vprašanje...";

else

if (vrednostGE01==1)

odgGE01="Se strinjam";

else if (vrednostGE01==2)

odgGE01="Vzdržan";

else if (vrednostGE01==3)

odgGE01="Se ne strinjam";

timeStamp();

ge01odg = "Ob "+hms+" "+ge01odgCnt+". prejem odgovora na izpisano vprašanje.";

socket.emit("socketPisanjeOdg", "Odg":odgGE01,"SocketID":connIDGE01);

ge01odgCnt++;

document.getElementById("btnRewriteGE01odg").click();

else

ge01odg="Glasovanje je ustavljeno! Za oddajo glasu počakaj na zagon glasovanja.";

document.getElementById("btnRewriteGE01odg").click();

;

socket.on("socketOdgGE01Zapisan", function()

connection01.send(vrednostGE01); // povratna informacije ESP-ju, katero LED naj prižge

);

function connectGE02() // povezava z glasovalno enoto 02, test povezave in reconnect

var connection02Test = true, odgGE02;

var connection02 = new WebSocket('ws://192.168.42.72:81/', ['arduino']); // connection02 - ESP8266-2

connection02.onopen = function()

connection02.send(0);

connIDGE02 = new Date().getTime()+"-GE02";

;

testConnection02 = setInterval(function()

if (connection02Test && connection02.readyState != 0)

connection02.send(0);

connection02Test=false;

if (location.href.includes('show-question'))

document.getElementById("ge02Connection").value="Test povezave";

else if (!connection02Test)

if (location.href.includes('show-question'))

document.getElementById("ge02Connection").value="Povezava prekinjena! Vzpostavljanje

povezave...";

connIDGE02="clearConnIDGE02";

clearInterval(testConnection02);

connectGE02();

, 5200);

connection02.onerror = function(error)

console.log('WebSocket Error ', error);

;

connection02.onclose = function()

clearInterval(testConnection02);

connectGE02();

;

connection02.onmessage = function(msg)

vrednostGE02 = parseInt(msg.data);

if (vrednostGE02 == 42)

connection02Test=true;

if (location.href.includes('show-question'))

setTimeout(function()

document.getElementById("ge02Connection").value="OK";

, 500);

else if (vrednostGE02 != 0 && typeof(vrednostGE02) != "undefined" && location.href.includes('show-

question'))

if(startStop)

if (zapStVpr == 0) // preverjanje, da je izpisano vprašanje in da se ne nahaja na 0. vprašanju

ge02odg = "Prazen odgovor. Počakaj na vprašanje...";

else

if (vrednostGE02==1)

odgGE02="Se strinjam";

else if (vrednostGE02==2)

odgGE02="Vzdržan";

else if (vrednostGE02==3)

odgGE02="Se ne strinjam";

timeStamp();

ge02odg = "Ob "+hms+" "+ge02odgCnt+". prejem odgovora na izpisano vprašanje.";

socket.emit("socketPisanjeOdg", "Odg":odgGE02,"SocketID":connIDGE02);

ge02odgCnt++;

document.getElementById("btnRewriteGE02odg").click();

else

ge02odg="Glasovanje je ustavljeno! Za oddajo glasu počakaj na zagon glasovanja.";

document.getElementById("btnRewriteGE02odg").click();

;

socket.on("socketOdgGE02Zapisan", function()

connection02.send(vrednostGE02); // povratna informacije ESP-ju, katero LED naj prižge

);

function connectGE03() // povezava z glasovalno enoto 03, test povezave in reconnect

var connection03Test = true, odgGE03;

var connection03 = new WebSocket('ws://192.168.42.73:81/', ['arduino']); // connection03 - ESP8266-3

connection03.onopen = function()

connection03.send(0);

connIDGE03 = new Date().getTime()+"-GE03";

;

testConnection03 = setInterval(function()

if (connection03Test && connection03.readyState != 0)

connection03.send(0);

connection03Test=false;

if (location.href.includes('show-question'))

document.getElementById("ge03Connection").value="Test povezave";

else if (!connection03Test)

if (location.href.includes('show-question'))

document.getElementById("ge03Connection").value="Povezava prekinjena! Vzpostavljanje

povezave...";

connIDGE03="clearConnIDGE03";

clearInterval(testConnection03);

connectGE03();

, 5300);

connection03.onerror = function(error)

console.log('WebSocket Error ', error);

;

connection03.onclose = function()

clearInterval(testConnection03);

connectGE03();

;

connection03.onmessage = function(msg)

vrednostGE03 = parseInt(msg.data);

if (vrednostGE03 == 42)

connection03Test=true;

if (location.href.includes('show-question'))

setTimeout(function()

document.getElementById("ge03Connection").value="OK";

, 500);

else if (vrednostGE03 != 0 && typeof(vrednostGE03) != "undefined" && location.href.includes('show-

question'))

if(startStop)

if (zapStVpr == 0) // preverjanje, da je izpisano vprašanje in da se ne nahaja na 0. vprašanju

ge03odg = "Prazen odgovor. Počakaj na vprašanje...";

else

if (vrednostGE03==1)

odgGE03="Se strinjam";

else if (vrednostGE03==2)

odgGE03="Vzdržan";

else if (vrednostGE03==3)

odgGE03="Se ne strinjam";

timeStamp();

ge03odg = "Ob "+hms+" "+ge03odgCnt+". prejem odgovora na izpisano vprašanje.";

socket.emit("socketPisanjeOdg", "Odg":odgGE03,"SocketID":connIDGE03);

ge03odgCnt++;

document.getElementById("btnRewriteGE03odg").click();

else

ge03odg="Glasovanje je ustavljeno! Za oddajo glasu počakaj na zagon glasovanja.";

document.getElementById("btnRewriteGE03odg").click();

;

socket.on("socketOdgGE03Zapisan", function()

connection03.send(vrednostGE03); // povratna informacije ESP-ju, katero LED naj prižge

);

function timeStamp()

var today=new Date(),

h=today.getHours(),

m=today.getMinutes(),

s=today.getSeconds();

if (String(h).length<2) h="0"+h;

if (String(m).length<2) m="0"+m;

if (String(s).length<2) s="0"+s;

hms = h+":"+m+":"+s;

if (load) // izvajanje kode ob dostopu na webpage ali ob refresh-u webpage-a

url=location.href;

socket.emit("socketF5", url);

load=false;

socket.on("socketF5webpage", function() // osveževanje posameznih podstrani, glede na to, na kateri

podstrani je bil narejen F5

if (url.includes('add-question'))

location.href = "#/";

setTimeout(function()

location.href = "#/add-question";

, 1);

else if (url.includes('all-questions'))

izpisVprasanj();

else if (url.includes('answers'))

izpisRezultatov();

else if (url.includes('statistics'))

izpisStatistike();

);

// branje novega vprašanja iz webpage-a in pošiljanje v bazo

function dodajVpr()

novoVpr = document.getElementById("iptDodajVpr").value;

if (novoVpr != "")

socket.emit("socketDodajVpr", novoVpr);

document.getElementById("iptDodajVpr").value = "";

else

document.getElementById("btnPraznoVpr").click();

;

// pritisk na gumb "Beri vprašanje" proži ukaz za branje vprašanja in ID vprašanja iz baze

function beriVpr()

if (zapStVpr < stVpr)

socket.emit("socketBeriVpr", 1);

ge00odgCnt=1;

ge01odgCnt=1;

ge02odgCnt=1;

ge03odgCnt=1;

else // popup za opozorilo zadnjega vprašanja

document.getElementById("btnZadnjeVpr").click();

;

// branje predhodnjega vprašanja trenutnemu

function prejsnjeVpr()

if (zapStVpr > 1)

socket.emit("socketBeriVpr", 2);

ge00odgCnt=1;

ge01odgCnt=1;

ge02odgCnt=1;

ge03odgCnt=1;

else

document.getElementById("btnZadnjeVpr").click();

;

// prejem in izpis vprašanja, števila vprašanj v bazi in prejem ID vprašanja

socket.on("socketVprPrebran", function(data)

zapStVpr = data.zapStVpr;

stVpr = data.stVpr;

if (data.vpr != "delniIzpis" && data.vpr != "osveziPodatke" && (location.href.includes('show-question')

|| location.href.includes('submit-vote'))) // funkcija je uporabljena 2x, pri branju vprašanja in pri

dodajanju novega vprašanja, zato omejitev izpisa

vprasanje = data.vpr;

potrZapOdg = '', ge00odg = '...', ge01odg = '...', ge02odg = '...', ge03odg = '...';

setTimeout( function() // če ni timeout-a, v browserju javi napako 'Cannot read property of null'-

preverjanje se izvede pred nalaganjem strani

if (location.href.includes('submit-vote'))

document.getElementById("strinja").checked = false;

document.getElementById("vzdrzan").checked = false;

document.getElementById("nestrinja").checked = false;

document.getElementById("btnRewriteVprasanje").click();

, 10);

else if (data.vpr == "delniIzpis") // uporablja se pri F5 add-question.html

document.getElementById("btnPrejemStVpr").click();

);

// funkcija branja izbranega odg in pošiljanja povratne informacije o vprID, izbranem odg, timestamp...

serverju za vpis v Redis

function oddajOdg()

if(startStop)

if(document.getElementById("strinja").checked)

odg = 'Se strinjam';

prazenOdg = false;

else if (document.getElementById("vzdrzan").checked)

odg = 'Vzdržan';

prazenOdg = false;

else if (document.getElementById("nestrinja").checked)

odg = 'Se ne strinjam';

prazenOdg = false;

else

prazenOdg = true;

if (zapStVpr == 0) // popup za oddajo odgovora na 0. vprašanje/ni vprašanja

document.getElementById("btnNiVpr").click();

else if (prazenOdg == true) // popup za prazen odgovor

document.getElementById("btnPrazenOdg").click();

else // pošiljanje odgovora v bazo

potrZapOdg = 'Odgovor zabeležen. Vprašanje: "'+vprasanje+'", odgovor: "'+odg+'".'; // potrditev

zapisa odgovora v bazo

socket.emit("socketPisanjeOdg", "Odg":odg,"SocketID":"webpage");

document.getElementById("btnPotrditevPrejemaOdg").click();

else

document.getElementById("btnGlasovanjeStop").click();

;

// izpis rezultatov glasovanja

function izpisRezultatov()

socket.emit("socketIzpisiRezultate");

;

// izpis rezultatov glasovanja

socket.on("socketPosiljanjeRezultatov", function(msg)

rezultati = msg;

setTimeout(function()

if (location.href.includes('answers'))

document.getElementById("btnRewrite").click();

, 10);

);

// izpis vseh vprašanj v tabelo

function izpisVprasanj()

socket.emit("socketIzpisVprasanj");

;

// prejem vseh vprašanj za prikaz

socket.on("socketPosiljanjeVprasanj", function(msg)

vsaVpr = msg;

setTimeout(function()

if (location.href.includes('show-question'))

document.getElementById("btnRewriteVprasanja").click();

, 10)

);

function izpisStatistike()

socket.emit("socketIzpisStatistike");

;

socket.on("socketPosiljanjeStatistike", function(msg)

statOdg = msg;

setTimeout(function()

if (location.href.includes('statistics'))

document.getElementById("btnRewriteStatistika").click();

, 10);

);

function osveziVprasanje() // osveževanja spremenljivke za izpis vprašanja na Spletni glasovalni enoti

socket.emit("socketOsveziVprasanje");

;

socket.on("socketDuplicatedSocketID", function()

document.getElementById("btnPodvojenSocketID").click();

);

function WebGERefresh() // funkcija za osveževanje tabele povezanih WebGE na 'show-question' strani.

Osvežitev se izvede samo, ko smo na strani 'show-question'.

setInterval(function()

if (location.href.includes('show-question'))

socket.emit("socketWebGEF5");

console.log("interval 3s");

, 3000);

socket.on("socketWebGETabela", function(msg)

WebGETabela = JSON.parse('['+msg+']');

document.getElementById("btnRewriteWebGETabela").click();

);

function startStopGlasovanje() // funkcija za zagon in ustavitev glasovanja/sprejemanja glasov

ge00odg = '...', ge01odg = '...', ge02odg = '...', ge03odg = '...';

if(startStop) // če je true, pošlji 0=false

socket.emit("socketStartStopGlasovanje", 0); // start>stop

else // če je false, pošlji 1=true

socket.emit("socketStartStopGlasovanje", 1); // stop>start

document.getElementById("btnStartStopGL").click();

socket.on("socketStartStopGlasovanjeIzpis", function(msg)

if(msg===0)

startStop = false;

stanjeGlasovanja="Glasovanje je ustavljeno. Počakaj na začetek glasovanja.";

else if(msg===1)

startStop = true;

stanjeGlasovanja="Glasovanje je v teku. Prosim, oddaj svoj glas.";

if(location.href.includes('submit-vote'))

document.getElementById("btnStartStopInfo").click();

);

// =============================================

</script>

</body>

</html>

server.js // Verzija: 2017.04.30b

// ====================================================================================================

var express = require("express")();

var http = require("http").Server(express);

var io = require("socket.io").listen(http);

var redis = require("redis");

var clientRedis = redis.createClient();

var VprID, // ID vprašanja v bazi "vprasanja"

maxVprID=0, // VprID zadnjega/najnovejšega vpr v DB

vprasanja, // shranjevanje vprašanj iz DB

rezultati, // shranjevanje rezultatov iz DB

v00=0, // varovalka, ki se uporablja v funkciji 'branjeVprasanj'

v01=0, // varovalka, ki se uporablja v funkciji 'branjeRezultatov'

v02=0, // varovalka, ki se uporablja v funkciji 'stOdgPosameznoVpr'

stOdgVpr = [], // število odgovorov na posamezno vprašanje - array

stVpr = 0, // število vprašanj v bazi

zapStVpr = 0, // zaporedna številka vprašanja pri branju iz baze

SocketID, // SocketID povezave posameznega brskalnika

cIP, // IP-naslov clienta

cIP1,

cSocketID, // client Socket ID

cNum=0, // število povezanih klientov

timestamp2, // timestamp v berljivi obliki hh:mm:ss

hms, // timestamp hh:mm:ss

serverIP="192.168.42.50"; // spremenljivka za server IP. Uporablja se pri preverjanju in zapisovanju

podatkov v Redis tabelo WebGE

var osveziPodatke = true, // spremenjivka, ki se uporabi za preverjanje ob vnovičnem zagonu programa

socketF5 = true, // spremenljivka, ki se uporablja pri zagonu programa in osveževanju (F5) webpage-a

duplicatedSocketID=false, // spremenljivka, ki se uporablja pri preverjanju dupliciranih WebGE enot

glasovanjeStartStop=false; // spremenljivka, ki se uporablja za spremljanje stanja glasovanja

// === EXPRESS.GET initial files ===

express.get('/', function(req, res)

res.sendFile(__dirname + '/webpage.html');

);

express.get('/app.js', function(req, res)

res.sendFile(__dirname + '/app.js');

);

// === EXPRESS.GET pages ===

express.get('/pages/home.html', function(req, res)

res.sendFile(__dirname + '/pages/home.html');

);

express.get('/pages/show-question.html', function(req, res)

// express.get('/pages/show-question.html', requireLogin, function(req, res)

res.sendFile(__dirname + '/pages/show-question.html');

);

express.get('/pages/add-question.html', function(req, res)

// express.get('/pages/add-question.html', requireLogin, function(req, res)

res.sendFile(__dirname + '/pages/add-question.html');

);

express.get('/pages/answers.html', function(req, res)

// express.get('/pages/answers.html', requireLogin, function(req, res)

res.sendFile(__dirname + '/pages/answers.html');

);

express.get('/pages/all-questions.html', function(req, res)

// express.get('/pages/all-questions.html', requireLogin, function(req, res)

res.sendFile(__dirname + '/pages/all-questions.html');

);

express.get('/pages/statistics.html', function(req, res)

// express.get('/pages/statistics.html', requireLogin, function(req, res)

res.sendFile(__dirname + '/pages/statistics.html');

);

express.get('/pages/submit-vote.html', function(req, res)

res.sendFile(__dirname + '/pages/submit-vote.html');

);

express.get('/pages/login.html', function(req, res)

res.sendFile(__dirname + '/pages/login.html');

);

// === EXPRESS.GET src ===

express.get('/src/angular.js', function(req, res)

res.sendFile(__dirname + '/src/angular.js');

);

express.get('/src/angular-route.js', function(req, res)

res.sendFile(__dirname + '/src/angular-route.js');

);

express.get('/src/ui-bootstrap-tpls.js', function(req, res)

res.sendFile(__dirname + '/src/ui-bootstrap-tpls.js');

);

express.get('/src/smart-table.js', function(req, res)

res.sendFile(__dirname + '/src/smart-table.js');

);

express.get('/src/jquery.min.js', function(req, res)

res.sendFile(__dirname + '/src/jquery.min.js');

);

express.get('/src/bootstrap.min.js', function(req, res)

res.sendFile(__dirname + '/src/bootstrap.min.js');

);

express.get('/src/bootstrap-hover-dropdown.min.js', function(req, res)

res.sendFile(__dirname + '/src/bootstrap-hover-dropdown.min.js');

);

// === EXPRESS.GET css ===

express.get('/css/style.css', function(req, res)

res.sendFile(__dirname + '/css/style.css');

);

express.get('/css/bootstrap-theme.min.css', function(req, res)

res.sendFile(__dirname + '/css/bootstrap-theme.min.css');

);

express.get('/css/bootstrap.min.css', function(req, res)

res.sendFile(__dirname + '/css/bootstrap.min.css');

);

// === EXPRESS.GET fonts ===

express.get('/css/fonts/Raleway-Light.woff2', function(req, res)

res.sendFile(__dirname + '/css/fonts/Raleway-Light.woff2');

);

express.get('/css/fonts/Raleway-Regular.woff2', function(req, res)

res.sendFile(__dirname + '/css/fonts/Raleway-Regular.woff2');

);

express.get('/css/fonts/Raleway-Medium.woff2', function(req, res)

res.sendFile(__dirname + '/css/fonts/Raleway-Medium.woff2');

);

express.get('/css/fonts/Raleway-SemiBold.woff2', function(req, res)

res.sendFile(__dirname + '/css/fonts/Raleway-SemiBold.woff2');

);

express.get('/css/fonts/Raleway-Bold.woff2', function(req, res)

res.sendFile(__dirname + '/css/fonts/Raleway-Bold.woff2');

);

express.get('/css/fonts/Raleway-Light-ext.woff2', function(req, res)

res.sendFile(__dirname + '/css/fonts/Raleway-Light-ext.woff2');

);

express.get('/css/fonts/Raleway-Regular-ext.woff2', function(req, res)

res.sendFile(__dirname + '/css/fonts/Raleway-Regular-ext.woff2');

);

express.get('/css/fonts/Raleway-Medium-ext.woff2', function(req, res)

res.sendFile(__dirname + '/css/fonts/Raleway-Medium-ext.woff2');

);

express.get('/css/fonts/Raleway-SemiBold-ext.woff2', function(req, res)

res.sendFile(__dirname + '/css/fonts/Raleway-SemiBold-ext.woff2');

);

express.get('/css/fonts/Raleway-Bold-ext.woff2', function(req, res)

res.sendFile(__dirname + '/css/fonts/Raleway-Bold-ext.woff2');

);

// === EXPRESS.GET popup ===

express.get('/pages/popup/last-question.html', function(req, res)

res.sendFile(__dirname + '/pages/popup/last-question.html');

);

express.get('/pages/popup/duplicated-answer.html', function(req, res)

res.sendFile(__dirname + '/pages/popup/duplicated-answer.html');

);

express.get('/pages/popup/empty-answer.html', function(req, res)

res.sendFile(__dirname + '/pages/popup/empty-answer.html');

);

express.get('/pages/popup/no-question.html', function(req, res)

res.sendFile(__dirname + '/pages/popup/no-question.html');

);

express.get('/pages/popup/delete-warning.html', function(req, res)

res.sendFile(__dirname + '/pages/popup/delete-warning.html');

);

express.get('/pages/popup/empty-question.html', function(req, res)

res.sendFile(__dirname + '/pages/popup/empty-question.html');

);

express.get('/pages/popup/duplicated-socketid.html', function(req, res)

res.sendFile(__dirname + '/pages/popup/duplicated-socketid.html');

);

express.get('/pages/popup/vote-stop.html', function(req, res)

res.sendFile(__dirname + '/pages/popup/vote-stop.html');

);

// === EXPRESS.GET pictures ===

express.get('/pictures/ozadje.jpg', function(req, res)

res.sendFile(__dirname + '/pictures/ozadje.jpg');

);

http.listen(8080);

console.log("Zagon sistema");

clientRedis.del("preverjanje"); // brisanje Redis tabele 'preverjanje' ob zagonu server.js

clientRedis.del("webge"); // brisanje Redis tabele 'webge' ob zagonu server.js

io.sockets.on("connection", function(socket)

// pridobivanje IP-naslova klienta in socket ID

cIP=socket.request.connection.remoteAddress.substring(7); // substring odreže prvih 7 znakov (":ffff::")

cSocketID = socket.id;

clientRedis.hget("webge", cIP, function(err, reply) // preverjanje podvojenosti dostopa z istega IP

if (reply === null)

if (socket.request.connection.remoteAddress.substring(7) != serverIP)

cNum++;

clientRedis.hset("webge", cIP,

'"IP":"'+cIP+'","SocketID":"'+cSocketID+'","hms":"99:99:99","stanjeOdg":"0"');

console.log("New user has connected. Socket ID: "+socket.id+". IP:

"+socket.request.connection.remoteAddress.substring(7)+". Total users: "+cNum+".");

if (glasovanjeStartStop)

socket.emit("socketStartStopGlasovanjeIzpis", 1);

else if (!glasovanjeStartStop)

socket.emit("socketStartStopGlasovanjeIzpis", 0);

else

socket.emit("socketDuplicatedSocketID");

duplicatedSocketID=true;

io.sockets.connected[cSocketID].disconnect();

console.log("Podvojen dostop naprave z IP: "+cIP+". Nov SocketID podvojene naprave: "+cSocketID+".

Povezava novega SocketID prekinjena!");

duplicatedSocketID=false;

);

socket.on("disconnect", function()

if (!duplicatedSocketID && socket.request.connection.remoteAddress.substring(7) != serverIP)

clientRedis.hdel("webge", socket.request.connection.remoteAddress.substring(7));

cNum--;

console.log("User has disconnected. Socket ID: "+socket.id+". IP:

"+socket.request.connection.remoteAddress.substring(7)+". Total users: "+cNum+".");

);

// klic ob zagonu programa in ob refresh-u (F5) webpage-a

socket.on("socketF5", function(msg)

if (socketF5===true) // ob zagonu programa

branjeStVpr();

socketF5=false;

else if (msg.includes('add-question')) // ob refresh-u strani za dodajanje vprašanj

beriVprasanje();

socket.emit("socketF5webpage");

else if (msg.includes('all-questions') || msg.includes('answers') || msg.includes('statistics')) //

ob refresh-u (F5) strani s tabelami podatkov

socket.emit("socketF5webpage");

else // ob refresh-u (F5) strani z vprašanjem in ostalih, stranek, ki jih zgornji if-i ne pokrijejo

beriVprasanje();

);

// branje vprašanja iz Redis + pošiljanje ID vprašanja (zaporedna št vpr)

socket.on("socketBeriVpr", function(msg)

clientRedis.hkeys("webge", function(err, reply)

reply.forEach(WebGEQRst); // za vsako vrednost v array-u kličemo funkcijo 'WebGEQRst'

);

if (msg===1)

zapStVpr++;

else if (msg===2)

zapStVpr--;

branjeStVpr();

clientRedis.zrange("vprasanja", zapStVpr-1, zapStVpr-1, function(err, reply)

tempReply = JSON.parse(reply);

VprID = tempReply.VprID;

io.emit("socketVprPrebran", "vpr":tempReply.vprasanje, "zapStVpr":zapStVpr, "stVpr":stVpr); //io.emit

pošlje novo vprašanje vsem povezanim klientom

);

);

// zapisovanje novega vprašanja v Redis

socket.on("socketDodajVpr", function(msg)

++maxVprID;

clientRedis.zadd("vprasanja", maxVprID, '"VprID":"'+maxVprID+'","vprasanje":"'+msg+'"');

socket.emit("socketVprPrebran", "vpr":"delniIzpis", "zapStVpr":zapStVpr, "stVpr":stVpr+1);

console.log("Prejem "+(stVpr+1)+". vprašanja: "+msg);

stVpr++;

);

// zapisovanje izbranega odgovora v Redis

socket.on("socketPisanjeOdg", function(msg)

timeStamp();

if (msg.SocketID == "webpage")

SocketID = socket.id;

cIP1=socket.request.connection.remoteAddress.substring(7); // substring odreže prvih 7 znakov

(":ffff::")

clientRedis.hget("webge", cIP1, function(err, reply)

tempReply=JSON.parse(reply);

clientRedis.hset("webge", cIP1,

'"IP":"'+tempReply.IP+'","SocketID":"'+tempReply.SocketID+'","hms":"'+hms+'","stanjeOdg":"'+(++tempReply.sta

njeOdg)+'"');

);

else

SocketID = msg.SocketID;

clientRedis.hget("preverjanje", VprID+"-"+SocketID, function(err, reply) // preverjanje podvojenosti

odgovora. Če je odgovor na prikazano vprašanje z sporočenim SocketID že zapisan, se že podan odgovor izbriše

iz DB in zapiše novega-upoštevamo samo zadnji odgovor.

if (reply !== null)

clientRedis.zrem("odgovori", reply);

);

var timestamp = new Date().getTime(); // timestamp v milisekundah

clientRedis.zadd("odgovori", VprID,

'"VprID"'+':"'+VprID+'","Odg"'+':"'+msg.Odg+'","ts"'+':"'+timestamp2+'","ts2":"'+timestamp+'","SocketID":"'+

SocketID+'"');

clientRedis.hset("preverjanje", VprID+"-"+SocketID,

'"VprID"'+':"'+VprID+'","Odg"'+':"'+msg.Odg+'","ts"'+':"'+timestamp2+'","ts2":"'+timestamp+'","SocketID":"'+

SocketID+'"'); // Redis DB hash za zapisovanje spremenljivk za preverjanje podvojenosti odgovora glede na

vprašanje in SocketID.

if (msg.SocketID.includes('-GE00')) // pošiljanje povratne informacije o zapisu odgovora glasovalne

enote, da se na GE prižge ustrezno LED

socket.emit("socketOdgGE00Zapisan");

else if (msg.SocketID.includes('-GE01'))

socket.emit("socketOdgGE01Zapisan");

else if (msg.SocketID.includes('-GE02'))

socket.emit("socketOdgGE02Zapisan");

else if (msg.SocketID.includes('-GE03'))

socket.emit("socketOdgGE03Zapisan");

);

socket.on("socketIzpisiRezultate", function()

v01=1;

branjeRezultatov();

);

// brisanje posameznega odgovora iz tabele odgovorov

socket.on("socketBrisanjeVrsticeOdg", function(msg)

var delElement = "$$hashKey"; // prejet JSON string vsebuje dodatni element '$$hashKey', ki je dodan s

strani tabele in ga je potrebno odstraniti, da bo vsebina enaka vsebini vrstice, ki jo želimo brisati

delete msg[delElement]; // brisanje '$$hashKey' elementa iz JSON stringa

clientRedis.zrem("odgovori", JSON.stringify(msg), function(err, reply)

console.log("Število brisanih odgovorov iz tabele odgovorov: "+reply);

);

);

// izpis vseh prašanj v tabelo

socket.on("socketIzpisVprasanj", function()

v00=1;

branjeVprasanj();

);

// brisanje posameznega vprašanja in povezanih odgovorov iz tabele in DB

socket.on("socketBrisanjeVrsticeVpr", function(msg)

var delElement = "$$hashKey";

delete msg[delElement];

var delVprID = msg.VprID;

clientRedis.zrem("vprasanja", JSON.stringify(msg), function(err, reply)

console.log("Število brisanih vprašanj iz tabele vprašanj: "+reply);

);

clientRedis.zrangebyscore("odgovori", '('+(delVprID-1), delVprID, function(err, reply)

var delSeznamOdg = reply;

clientRedis.zrem("odgovori", delSeznamOdg, function(err, reply)

console.log("Število brisanih odgovorov, vezanih na brisano vprašanje: "+reply);

);

);

osveziPodatke = true;

v02=1;

branjeStVpr();

);

// branje vprašanj in povezanih odgovorov ter pošiljanje VprID, vprašanja, št odg 'se strinja', št odg

'vzdržan' in št odg 'se ne strinja' na webpage

socket.on("socketIzpisStatistike", function()

v02=1;

branjeStVpr();

);

socket.on("socketOsveziVprasanje", function()

beriVprasanje();

);

socket.on("socketWebGEF5", function() // osveževanje WebGE tabele

clientRedis.hvals("webge", function(err, reply)

socket.emit("socketWebGETabela", reply);

);

);

socket.on("socketStartStopGlasovanje", function(msg) // osveževanje izpisa start/stop glasovanja

if(msg===0)

glasovanjeStartStop=false;

io.emit("socketStartStopGlasovanjeIzpis", 0);

else if(msg===1)

glasovanjeStartStop=true;

io.emit("socketStartStopGlasovanjeIzpis", 1);

);

// FUNKCIJE =================================================================

// branje števila vprašanj

function branjeStVpr()

clientRedis.zcount("vprasanja", "-inf", "+inf", function(err, reply)

stVpr = reply;

if (osveziPodatke === true) // pošiljanje števila vprašanj v bazi ob zagonu programa in ob brisanju

vprašanja iz DB

socket.emit("socketVprPrebran", "vpr":"osveziPodatke", "zapStVpr":(zapStVpr), "stVpr":stVpr);

osveziPodatke = false;

lastVprID(); // ko preberemo število vprašanj v DB, kličemo funkcijo 'lastVprID()', ki prebere VprID

zadnjega vprašanja - 'maxVprID'

);

// branje VprID zadnjega vprašanja v DB

function lastVprID()

if (stVpr > 0)

clientRedis.zrange("vprasanja", (stVpr-1), (stVpr-1), function(err, reply)

tempReply = JSON.parse(reply);

maxVprID = tempReply.VprID;

stOdgPosameznoVpr(); // ko preberemo VprID zadnjega vprašanja v DB, kličemo funkcijo

'stOdgPosameznoVpr', ki sestavi array, ki vsebuje število odgovorov za posamezno vprašanje

);

// izpis števila prejetih odgovorov na posamezno vprašanje - beležimo samo vprašanja, ki imajo odgovore

function stOdgPosameznoVpr()

var j=1,

k=0;

stOdgVpr = [];

for (i=1; i<=maxVprID; i++)

clientRedis.zcount("odgovori", i, i, function(err, reply)

if (reply != "")

stOdgVpr[k] = reply;

k++;

if (v02===1)

branjeVprOdgSkupaj();

v02=0;

);

// branje vprašanj in odgovorov ter sestavljanje v enoten string za izpis na webpage (podstran

'statistika')

function branjeVprOdgSkupaj()

var j=1, k=0, tempVprID, tempVpr, tempVprasanje, tempOdgovor, tempSeStrinjam,

tempVzdrzan, tempSeNeStrinjam,

odgovor=[]; // array vseh odgovorv, ki se posredujejo na webpage za izpis

for(i=0; i<maxVprID; i++)

clientRedis.zrangebyscore("vprasanja", '('+i, (i+1), function(err, reply)

if (reply != "") // pusti '!=', če spremeniš v '!==' ne deluje

tempVprasanje = JSON.parse(reply);

tempVprID = tempVprasanje.VprID;

tempVpr = tempVprasanje.vprasanje;

);

clientRedis.zrangebyscore("odgovori", '('+i, (i+1), function(err, reply)

if (reply != "") // pusti '!=', če spremeniš v '!==' ne deluje

tempSeStrinjam=0, tempVzdrzan=0, tempSeNeStrinjam=0;

tempOdgovor = JSON.parse('['+reply+']'); // ker je odgovorov za dano vprašanje več kot eden, ga

sestavimo v JSON array

for(ii=0; ii<stOdgVpr[k]; ii++) // ponovimo branje JSON array-a odgovorov, kolikor je odgovorov v

array-u - 'stOdgVpr[k]'

if (tempOdgovor[ii].Odg == "Se strinjam")

tempSeStrinjam++;

else if (tempOdgovor[ii].Odg == "Vzdržan")

tempVzdrzan++;

else if (tempOdgovor[ii].Odg == "Se ne strinjam")

tempSeNeStrinjam++;

tempOdgSkupaj=tempSeStrinjam+tempVzdrzan+tempSeNeStrinjam;

tempOdgSeStrinjam=tempSeStrinjam+" ("+parseFloat(Math.round(tempSeStrinjam * 100) /

(tempSeStrinjam+tempVzdrzan+tempSeNeStrinjam)).toFixed(2)+"%)";

tempOdgVzdrzan=tempVzdrzan+" ("+parseFloat(Math.round(tempVzdrzan * 100) /

(tempSeStrinjam+tempVzdrzan+tempSeNeStrinjam)).toFixed(2)+"%)";

tempOdgSeNeStrinjam=tempSeNeStrinjam+" ("+parseFloat(Math.round(tempSeNeStrinjam * 100) /

(tempSeStrinjam+tempVzdrzan+tempSeNeStrinjam)).toFixed(2)+"%)";

odgovor[k]="VprID":tempVprID,"vprasanje":tempVpr,"seStrinjam":tempOdgSeStrinjam,"vzdrzan":tempOdgVzdrzan,"se

NeStrinjam":tempOdgSeNeStrinjam,"skupaj":tempOdgSkupaj;

if ((k+1) == stOdgVpr.length)

socket.emit("socketPosiljanjeStatistike", odgovor);

k++;

);

// branje rezultatov iz Redis in pošiljanje na webpage za izpis v tabeli

function branjeRezultatov()

clientRedis.zrange("odgovori", 0, -1, function(err, reply) // pridobivanje seznama odgovorov

rezultati = JSON.parse('['+reply+']');

if (v01===1)

socket.emit("socketPosiljanjeRezultatov", rezultati);

v01=0;

);

// branje vprašanj iz DB

function branjeVprasanj()

clientRedis.zrange("vprasanja", 0, -1, function(err, reply) // pridobivanje seznama vprašanj

vprasanja = JSON.parse('['+reply+']');

if (v00===1) // izvedba 'socketIzpisVprasanj' in pošiljanje seznama vprašanj na webpage

socket.emit("socketPosiljanjeVprasanj", vprasanja);

v00=0;

);

function beriVprasanje()

branjeStVpr();

if (zapStVpr===0)

socket.emit("socketVprPrebran", "vpr":'Za prikaz vprašanja klikni na gumb "Naslednje vprašanje".',

"zapStVpr":zapStVpr, "stVpr":stVpr);

else

clientRedis.zrange("vprasanja", zapStVpr-1, zapStVpr-1, function(err, reply)

tempReply = JSON.parse(reply);

VprID = tempReply.VprID;

socket.emit("socketVprPrebran", "vpr":tempReply.vprasanje, "zapStVpr":zapStVpr, "stVpr":stVpr);

);

function timeStamp() // funkcija timestamp-a, ki se uporabi pri zapisu prejetih odg v tabelo. Timestamp v

human readable format-u.

var today=new Date(),

h=today.getHours(),

m=today.getMinutes(),

s=today.getSeconds(),

dd=today.getDate(),

mm=today.getMonth()+1,

yyyy=today.getFullYear();

if (String(h).length<2) h="0"+h;

if (String(m).length<2) m="0"+m;

if (String(s).length<2) s="0"+s;

if (String(dd).length<2) dd="0"+dd;

if (String(mm).length<2) mm="0"+mm;

timestamp2 = dd+"."+mm+"."+yyyy+", "+h+":"+m+":"+s;

hms=h+":"+m+":"+s;

function WebGEQRst(key) // funkcija za reset vrednosti 'stanjeOdg' ob branju drugega vprašanja

clientRedis.hget("webge", key, function(err, reply)

tempReply=JSON.parse(reply);

clientRedis.hset("webge", key,

'"IP":"'+tempReply.IP+'","SocketID":"'+tempReply.SocketID+'","hms":"99:99:99","stanjeOdg":"0"');

);

// FUNKCIJE =================================================================

);

app.js // Verzija: 2017.04.15b

// ====================================================================================================

var app = angular.module('myApp', ['ngRoute', 'ui.bootstrap', 'smart-table']);

var removeRowPodatek, removeRowVprasanje;

app.config(function($routeProvider)

$routeProvider

.when('/',

templateUrl:'pages/home.html',

controller:'HomeController'

)

.when('/show-question',

resolve:

"check": function($location, $rootScope)

if(!$rootScope.loggedIn)

$location.path('/login');

alert('Za dostop do te strani se je treba prijaviti!');

,

templateUrl:'pages/show-question.html',

controller:'QuestionController'

)

.when('/add-question',

resolve:

"check": function($location, $rootScope)

if(!$rootScope.loggedIn)

$location.path('/login');

alert('Za dostop do te strani se je treba prijaviti!');

,

templateUrl:'pages/add-question.html',

controller:'Add-questionController'

)

.when('/answers',

resolve:

"check": function($location, $rootScope)

if(!$rootScope.loggedIn)

$location.path('/login');

alert('Za dostop do te strani se je treba prijaviti!');

,

templateUrl:'pages/answers.html',

controller:'AnswersController'

)

.when('/all-questions',

resolve:

"check": function($location, $rootScope)

if(!$rootScope.loggedIn)

$location.path('/login');

alert('Za dostop do te strani se je treba prijaviti!');

,

templateUrl:'pages/all-questions.html',

controller:'all-questionsController'

)

.when('/statistics',

resolve:

"check": function($location, $rootScope)

if(!$rootScope.loggedIn)

$location.path('/login');

alert('Za dostop do te strani se je treba prijaviti!');

,

templateUrl:'pages/statistics.html',

controller:'StatisticsController'

)

.when('/submit-vote',

templateUrl:'pages/submit-vote.html',

controller:'WebGEController'

)

.when('/login',

templateUrl:'pages/login.html',

controller:'LoginController'

)

.otherwise(redirectTo:'/');

);

app.controller('HomeController', function($scope)

$scope.message = 'Home';

);

app.controller('QuestionController', function($scope)

$scope.vprasanje = vprasanje,

$scope.zapStVpr = zapStVpr,

$scope.stVpr = stVpr,

$scope.potrZapOdg = potrZapOdg,

$scope.ge00odg = ge00odg,

$scope.ge01odg = ge01odg,

$scope.ge02odg = ge02odg,

$scope.ge03odg = ge03odg,

$scope.WebGETabela = WebGETabela,

$scope.stanjeGlasovanja = stanjeGlasovanja,

$scope.stanjeGlasovanjaTF = false;

$scope.rewriteVprasanje = function()

return $scope.vprasanje = vprasanje,

$scope.zapStVpr = zapStVpr,

$scope.stVpr = stVpr,

$scope.potrZapOdg = potrZapOdg,

$scope.ge00odg = ge00odg,

$scope.ge01odg = ge01odg,

$scope.ge02odg = ge02odg,

$scope.ge03odg = ge03odg;

;

$scope.potrditevPrejemaOdg = function()

return $scope.potrZapOdg = potrZapOdg;

;

$scope.rewriteGE00odg = function()

return $scope.ge00odg = ge00odg;

;

$scope.rewriteGE01odg = function()

return $scope.ge01odg = ge01odg;

;

$scope.rewriteGE02odg = function()

return $scope.ge02odg = ge02odg;

;

$scope.rewriteGE03odg = function()

return $scope.ge03odg = ge03odg;

;

$scope.rewriteWebGETabela = function()

return $scope.WebGETabela = WebGETabela;

;

$scope.startStopGl = function()

console.log("bla");

if(!$scope.stanjeGlasovanjaTF)

$scope.stanjeGlasovanjaTF = true;

return $scope.stanjeGlasovanja = 'Glasovanje je v teku. Prosim, oddaj svoj glas.',

$scope.ge00odg = ge00odg,

$scope.ge01odg = ge01odg,

$scope.ge02odg = ge02odg,

$scope.ge03odg = ge03odg;

else

$scope.stanjeGlasovanjaTF = false;

return $scope.stanjeGlasovanja = 'Glasovanje je ustavljeno. Počakaj na začetek glasovanja.';

;

);

// === POPUP-i ===

app.controller('ModalPopup', function ($uibModal)

var $ctrl = this;

$ctrl.ZadnjeVpr = function () // Popup za zadnje vprašanje. Uporaba v question.html

var modalInstance = $uibModal.open(

templateUrl: '/pages/popup/last-question.html',

controller: 'ModalInstanceCtrl',

controllerAs: '$ctrl',

windowClass: 'app-modal-window' // Uporablja se v povezavi s CSS za določanje izgleda

);

;

$ctrl.PodvojenOdg = function () // Popup za podvojen odgovor. Uporaba v question.html

var modalInstance = $uibModal.open(

templateUrl: '/pages/popup/duplicated-answer.html',

controller: 'ModalInstanceCtrl',

controllerAs: '$ctrl',

windowClass: 'app-modal-window' // Uporablja se v povezavi s CSS za določanje izgleda

);

;

$ctrl.PrazenOdg = function () // Popup za prazen odgovor. Uporaba v question.html

var modalInstance = $uibModal.open(

templateUrl: '/pages/popup/empty-answer.html',

controller: 'ModalInstanceCtrl',

controllerAs: '$ctrl',

windowClass: 'app-modal-window' // Uporablja se v povezavi s CSS za določanje izgleda

);

;

$ctrl.NiVpr = function () // Popup za odgovor na 0. vprašanje/ni vprašanja. Uporaba v question.html

var modalInstance = $uibModal.open(

templateUrl: '/pages/popup/no-question.html',

controller: 'ModalInstanceCtrl',

controllerAs: '$ctrl',

windowClass: 'app-modal-window' // Uporablja se v povezavi s CSS za določanje izgleda

);

;

$ctrl.PraznoVpr = function () // Popup za opozorili pri dodajanju novega prašanja, ko je vnosno polje

prazno. Uporaba v add-question.html

var modalInstance = $uibModal.open(

templateUrl: '/pages/popup/empty-question.html',

controller: 'ModalInstanceCtrl',

controllerAs: '$ctrl',

windowClass: 'app-modal-window' // Uporablja se v povezavi s CSS za določanje izgleda

);

;

$ctrl.PodvojenSocketID = function () // Popup za opozorilo pri podvojenem dostopu do webpage-a z istega

IP. Uporaba na vseh straneh.

var modalInstance = $uibModal.open(

templateUrl: '/pages/popup/duplicated-socketid.html',

controller: 'ModalInstanceCtrl',

controllerAs: '$ctrl',

windowClass: 'app-modal-window' // Uporablja se v povezavi s CSS za določanje izgleda

);

;

$ctrl.GlasovanjeStop = function () // Popup, ko je glasovanje ustavljeno in uporabnik želi oddati

odgovor.

var modalInstance = $uibModal.open(

templateUrl: '/pages/popup/vote-stop.html',

controller: 'ModalInstanceCtrl',

controllerAs: '$ctrl',

windowClass: 'app-modal-window' // Uporablja se v povezavi s CSS za določanje izgleda

);

;

);

app.controller('ModalInstanceCtrl', function ($uibModalInstance)

var $ctrl = this;

$ctrl.ok = function()

$uibModalInstance.close();

;

$ctrl.cancel = function()

;

);

// === /POPUP-i ===

app.controller('Add-questionController', function($scope)

$scope.stVpr1 = stVpr;

$scope.prejemStVpr = function()

return $scope.stVpr1 = stVpr,

$scope.potrditevPrejemaNovegaVpr = 'Novo vprašanje: "'+novoVpr+'" dodano na seznam.';

;

);

app.controller('AnswersController', function($scope, $filter, $route, $uibModal)

$scope.podatki = rezultati;

$scope.rewrite = function()

return $scope.podatki = rezultati,

$route.reload(); // za osvežitev podatkov v expression-ih. Brez tega ne deluje iskanje po tabeli takoj po

izpisu.

;

$scope.removeRow = function removeRow(podatek) // Brisanje posamezne vrstice v izpisani tabeli

removeRowPodatek = podatek;

var modalInstance = $uibModal.open(

templateUrl: '/pages/popup/delete-warning.html',

controller: 'CtrlRmRow',

controllerAs: '$ctrl',

windowClass: 'app-modal-window' // Uporablja se v povezavi s CSS za določanje izgleda

);

;

$scope.predicates = ['ID vprašanja', 'Odgovor', 'Čas oddaje odgovora', 'Časovni žig', 'ID Socket

povezave'];

$scope.selectedPredicate = $scope.predicates[0];

);

app.controller('CtrlRmRow', function ($uibModalInstance, $scope) // ta kontroler se uporablja skupaj z

'$scope.removeRow', za prikaz popup-a ob brisanju odgovora

var $ctrl = this;

$scope.podatki = rezultati;

$ctrl.ok = function()

var index = $scope.podatki.indexOf(removeRowPodatek);

if (index !== -1)

$scope.podatki.splice(index, 1);

socket.emit("socketBrisanjeVrsticeOdg", removeRowPodatek); // pošiljanje vsebine vrstice, ki jo želimo

izbrisati

$uibModalInstance.close();

;

$ctrl.cancel = function()

$uibModalInstance.close();

;

);

app.controller('all-questionsController', function($scope, $filter, $route, $uibModal)

$scope.vprasanja = vsaVpr;

$scope.rewriteVprasanja = function()

return $scope.vprasanja = vsaVpr,

$route.reload();

;

$scope.removeRowVprasanja = function removeRow(vprasanje)

removeRowVprasanje = vprasanje;

var modalInstance = $uibModal.open(

templateUrl: '/pages/popup/delete-warning.html',

controller: 'CtrlRmRowVprasanje',

controllerAs: '$ctrl',

windowClass: 'app-modal-window'

);

;

$scope.predicates = ['ID vprašanja', 'Vprašanje'];

$scope.selectedPredicate = $scope.predicates[0];

);

app.controller('CtrlRmRowVprasanje', function ($uibModalInstance, $scope)

var $ctrl = this;

$scope.vprasanja = vsaVpr;

$ctrl.ok = function()

var index = $scope.vprasanja.indexOf(removeRowVprasanje);

if (index !== -1)

$scope.vprasanja.splice(index, 1);

socket.emit("socketBeriVpr", 2);

socket.emit("socketBrisanjeVrsticeVpr", removeRowVprasanje);

socket.emit("socketIzpisiRezultate");

$uibModalInstance.close();

;

$ctrl.cancel = function()

$uibModalInstance.close();

;

);

app.controller('StatisticsController', function($scope, $filter, $route, $uibModal)

$scope.odgovori = statOdg;

$scope.rewriteStatistika = function()

return $scope.odgovori = statOdg,

$route.reload();

;

$scope.predicates = ['ID vprašanja', 'Vprašanje'];

$scope.selectedPredicate = $scope.predicates[0];

);

app.controller('WebGEController', function($scope)

$scope.vprasanje = vprasanje,

$scope.zapStVpr = zapStVpr,

$scope.stVpr = stVpr,

$scope.potrZapOdg = potrZapOdg,

$scope.stanjeGlasovanja = stanjeGlasovanja,

$scope.stanjeGlasovanjaTF = false;

$scope.rewriteVprasanje = function()

return $scope.vprasanje = vprasanje,

$scope.zapStVpr = zapStVpr,

$scope.stVpr = stVpr,

$scope.potrZapOdg = potrZapOdg;

;

$scope.potrditevPrejemaOdg = function()

return $scope.potrZapOdg = potrZapOdg;

;

$scope.startStopGlWebGE = function()

if(!$scope.stanjeGlasovanjaTF && !startStop)

$scope.stanjeGlasovanjaTF = true;

return $scope.stanjeGlasovanja = stanjeGlasovanja;

else if ($scope.stanjeGlasovanjaTF && startStop)

$scope.stanjeGlasovanjaTF = false;

return $scope.stanjeGlasovanja = stanjeGlasovanja;

;

);

app.controller('LoginController', function($scope, $http, $location, $rootScope) // controller za Login

stran.

$scope.sub = function()

if($scope.username == 'admin' && $scope.password == 'riba')

$rootScope.loggedIn = true;

$location.path('/show-question');

else

alert('Napačno uporabniško ime in/ali geslo!');

;

);

app.controller('LogoutController', function($scope, $http, $location, $rootScope)

$scope.logout = function()

$rootScope.loggedIn = false;

$location.path('/');

;

);

style.css /* Verzija: 2017.04.14c */

/* latin-ext */

@font-face

font-family: 'Raleway';

font-style: normal;

font-weight: 300;

src: local('Raleway Light'), local('Raleway-Light'), url("fonts/Raleway-Light-ext.woff2") format('woff2');

unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;

/* latin */

@font-face

font-family: 'Raleway';

font-style: normal;

font-weight: 300;

src: local('Raleway Light'), local('Raleway-Light'), url("fonts/Raleway-Light.woff2") format('woff2');

unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC,

U+2212, U+2215;

/* latin-ext */

@font-face

font-family: 'Raleway';

font-style: normal;

font-weight: 400;

src: local('Raleway'), local('Raleway-Regular'), url("fonts/Raleway-Regular-ext.woff2") format('woff2');

unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;

/* latin */

@font-face

font-family: 'Raleway';

font-style: normal;

font-weight: 400;

src: local('Raleway'), local('Raleway-Regular'), url("fonts/Raleway-Regular.woff2") format('woff2');

unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC,

U+2212, U+2215;

/* latin-ext */

@font-face

font-family: 'Raleway';

font-style: normal;

font-weight: 500;

src: local('Raleway Medium'), local('Raleway-Medium'), url("fonts/Raleway-Medium-ext.woff2")

format('woff2');

unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;

/* latin */

@font-face

font-family: 'Raleway';

font-style: normal;

font-weight: 500;

src: local('Raleway Medium'), local('Raleway-Medium'), url("fonts/Raleway-Medium.woff2") format('woff2');

unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC,

U+2212, U+2215;

/* latin-ext */

@font-face

font-family: 'Raleway';

font-style: normal;

font-weight: 600;

src: local('Raleway SemiBold'), local('Raleway-SemiBold'), url("fonts/Raleway-SemiBold-ext.woff2")

format('woff2');

unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;

/* latin */

@font-face

font-family: 'Raleway';

font-style: normal;

font-weight: 600;

src: local('Raleway SemiBold'), local('Raleway-SemiBold'), url("fonts/Raleway-SemiBold.woff2")

format('woff2');

unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC,

U+2212, U+2215;

/* latin-ext */

@font-face

font-family: 'Raleway';

font-style: normal;

font-weight: 700;

src: local('Raleway Bold'), local('Raleway-Bold'), url("fonts/Raleway-Bold-ext.woff2") format('woff2');

unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;

/* latin */

@font-face

font-family: 'Raleway';

font-style: normal;

font-weight: 700;

src: local('Raleway Bold'), local('Raleway-Bold'), url("fonts/Raleway-Bold.woff2") format('woff2');

unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC,

U+2212, U+2215;

bodypadding-top:0 !important;background:#4a4a4a;

.navbar-defaultbackground:#50e3c2;box-shadow:none;border:none;

.navbar-default .navbar-brandfont-family:"Raleway";color: #fff;font-size: 30px;text-transform: uppercase;

font-weight: 500;margin:0;

.navbar-default .navbar-brand:hovercolor:#fff;

.navbar-default .navbar-nav>li>afont-family:"Raleway";color: #fff;font-weight:300;text-transform:uppercase;

.navbar-default .navbar-nav>li>a:hovercolor:#ebebeb;

.navbar .containerheight: 80px;line-height: 80px; padding: 15px;

h1color:#fff;font-family:"Raleway";margin-bottom:390px;text-shadow: 0px 0px 2px rgba(150, 150, 150, 1);text-

align:center;text-transform:uppercase;

h2text-align:center;font-family:"Raleway";color:#fff;font-size:30px;margin-bottom:30px;font-weight:300;

h3font-family:"Raleway";color:#fff;font-size:30px;margin-bottom:30px;font-weight:300;

button, button.btn-primarymargin-top:15px;transition:0.1s all linear;font-family:"Raleway";background-

color:transparent;border:1px solid #fff;padding:10px 20px;color:#fff;text-transform:uppercase;

button:hover, button.btn-primary:hoverborder-color:#50e3c2;color:#50e3c2;background-color:transparent;

button.btn-primary:focusoutline:0;background-color:transparent;box-shadow: none;border: 1px solid #fff;

#btnStartStopfloat:right;

outputcolor:#fff;

body .container input, body .container selectcolor:#fff;background-color: transparent;border: none;border:

1px solid #fff;padding: 0 20px;outline:none;

body .container selectheight:40px;

labelcolor:#fff;font-family:"Raleway";font-weight:300;

input[type='text']min-width:400px;

input[type='radio']margin-right:15px;

input:focusborder-color:#50e3c2;

::-webkit-input-placeholder

color: #fff;

font-weight:300;

font-family:"Raleway";

:-moz-placeholder /* Firefox 18- */

color: #fff;

font-weight:300;

font-family:"Raleway";

::-moz-placeholder /* Firefox 19+ */

color: #fff;

font-weight:300;

font-family:"Raleway";

:-ms-input-placeholder

color: #fff;

font-weight:300;

font-family:"Raleway";

p,li,ulfont-family:"Raleway";color:#fff;

.custom-list liline-height:2.3;list-style-type:none;position:relative;

.custom-list li::beforecontent:"• ";font-family:"Raleway";font-size:30px;position:absolute;left:-20px;top:-

17px;color:#50e3c2;;

.custom-list ulline-height:2.3;list-style-type:none;position:relative;

.custom-list ul::beforecontent:"• ";font-family:"Raleway";font-size:30px;position:absolute;left:-20px;top:-

17px;color:#50e3c2;;

/*DOMAČA STRAN*/

.homepageborder-radius:20px 0;margin-top:-390px;padding-bottom:100px;

.bannerbackground-image:url(../pictures/ozadje.jpg);height:500px;background-size:cover;background-

position:50%;

.poudarekcolor:#50e3c2;

/*VPRAŠANJE*/

.dvetretjinifloat:left;width:66%;

.enatretjinafloat:left;width:33%;

.celotafloat:left;width:100%;

.radioLinecolor:#fff;font-family:"Raleway";margin-bottom:15px;

.vrsticaxodytext-transform:uppercase;font-weight:700;

.trenutnoVprasanjeborder-bottom:1px solid #50e3c2;padding-bottom:10px;margin-right:100px;font-

weight:300;font-size:20px;

.modal-contentbackground-color:#4a4a4a;

/*DODAJANJE VPRAŠANJ*/

.stvprasanj-holdermargin-bottom:15px;

.stvprasanjfont-size: 30px;margin-left: 10px;color: #50e3c2;

.povratnamargin-top:30px;border-bottom:1px solid #50e3c2;padding-bottom:10px;

#iptDodajVprpadding:15px 10px;

/*IZPIS*/

#btnIzpisRezultatovdisplay:block;margin:auto;

#btnIzpisVprasanjdisplay:block;margin:auto;

#btnIzpisStatistikedisplay:block;margin:auto;

table.table-striped.table trbackground-color:transparent;

tablewidth:100%;font-family:"Raleway";margin-bottom:100px;

table thcolor:#fff;font-weight:300;text-align:center;border-bottom:1px solid #fff;padding:15px 10px

table tdcolor:#fff;font-weight:300;text-align:center;padding:15px 10px

table .btn-dangermargin-top:0;

@media screen and (max-width: 990px)

@media screen and (max-width: 767px)

.navbar .containerheight:auto;

.homepagemargin-top:-190px;

h1margin-bottom:190px;

/*TABELE*/

#predicate float:left;width:300px;background:#4a4a4a;

#predicate2 float:left;margin-right:10px;

#thVprID width:35px;font-weight:bold;

#thIzbrisi width:70px;font-weight:bold;

#thRezultat min-width:115px;font-weight:bold;

#thSkupaj min-width:70px;font-weight:bold;

#thPrva font-weight:bold;

#thIP width:150px;font-weight:bold;

#thSocketID width:300px;font-weight:bold;

#thGEnota width:160px;font-weight:bold;

#thStanjeOdg width:200px;font-weight:bold;

/*ISKANJE PO TABELAH*/

#thSrchVprID float:left;min-width:120px;width:15%;margin-right:0.5%;

#thSrchIskanje float:left;min-width:200px;width:42%;margin-right:0.5%;

#thSrchIskanje2 float:left;min-width:200px;width:42%;

#thSrchIskanjeAQ float:left;min-width:200px;width:84.5%;

/*=== === === === === === POPUP-i === === === === === ===*/

.app-modal-window .modal-dialog /*uporabljen v: last-question.html*/

width: 350px;

/*=== === === === === === /POPUP-i === === === === === ===*/

pages/add-question.html <!-- Verzija: 2016.12.13b -->

<div class='homepage'>

<h1>Dodajanje novega vprašanja</h1>

<div id="divDodajVprasanje", ng-controller="Add-questionController">

<p class='stvprasanj-holder'>

<span class='stvprasanj-title'>Število vprašanj v bazi:<span class='stvprasanj'>stVpr1</span>

</p>

<input placeholder='Vpišite vprašanje...' id="iptDodajVpr" type="text" /><br>

<button id="btnDodajVpr" onClick="dodajVpr()">Dodaj vprašanje</button>

<button id="btnPrejemStVpr" type="button" ng-click="prejemStVpr()" style="display:none"></button>

<br>

<p class='povratna'>potrditevPrejemaNovegaVpr</p>

</div>

</div>

<button ng-controller="ModalPopup as $ctrl" id="btnPraznoVpr" type="button" class="btn btn-default" ng-

click="$ctrl.PraznoVpr()" style="display:none">Prazno vprašanje</button>

pages/all-questions.html <!-- Verzija: 2017.01.20c -->

<div class='homepage'>

<h1>Seznam vseh vprašanj</h1>

<button id="btnIzpisVprasanj" onClick="izpisVprasanj()" style="display:none">Izpiši vprašanja</button>

<div id="divVsaVprasanja", ng-controller="all-questionsController">

<button id="btnRewriteVprasanja" type='button' ng-click="rewriteVprasanja()" style="display:none">Naloži

podatke</button> <!-- gumb je skrit, ker se ga uporablja v 'socketPosiljanjeVprasanj' za izpis nove vrstice v

Angular tabelo -->

<table st-table="vprasanja" class="table table-striped">

<thead>

<tr>

<th id="thVprID" st-sort="VprID">ID</th>

<th id="thPrva" st-sort="vprasanje">Vprašanje</th>

<th id="thIzbrisi">Izbriši</th>

</tr>

<tr>

<th colspan="3">

<input id="thSrchVprID" st-search="VprID" placeholder="Išči po ID" class="input-sm form-control"

type="search" />

<input id="thSrchIskanjeAQ" st-search placeholder="Iskanje po celotni tabeli" class="input-sm

form-control" type="search" />

</th>

</tr>

</thead>

<tbody>

<tr ng-repeat="vprasanje in vprasanja">

<td>vprasanje.VprID</td>

<td>vprasanje.vprasanje</td>

<td><button type="button" style="font-weight:bold;" ng-click="removeRowVprasanja(vprasanje)"

class="btn btn-sm btn-danger">X</button></td>

</tr>

</tbody>

</table>

</div>

</div>

pages/answers.html <!-- Verzija: 2017.01.20b -->

<div class='homepage'>

<h1>Odgovori / rezultati</h1>

<button id="btnIzpisRezultatov" onClick="izpisRezultatov()" style="display:none">Izpiši rezultate

glasovanja</button>

<div id="divIzpisRezultatov" ng-controller="AnswersController">

<button id="btnRewrite" type='button' ng-click="rewrite()" style="display:none">Naloži podatke</button>

<!-- gumb je skrit, ker se ga uporablja v 'socketPosiljanjeRezultatov' za izpis nove vrstice v Angular tabelo

-->

<!-- <h2>Izpis rezultatov:</h2> -->

<form>

<label id="predicate2" for="predicate">Izberi ime stolpca, po katerem želiš iskati:</label>

<select class="form-control" id="predicate" ng-model="selectedPredicate" ng-options="predicate for

predicate in predicates"></select>

</form>

<br>

<table st-table="podatki" class="table table-striped">

<thead>

<tr>

<th id="thVprID" st-sort="VprID">ID</th>

<th id="thPrva" st-sort="Odg">Odgovor</th>

<th id="thPrva" st-sort="ts">Čas oddaje odgovora</th>

<th id="thPrva" st-sort="ts2">Časovni žig</th>

<th id="thPrva" st-sort="SocketID">ID Socket povezave</th>

<th id="thIzbrisi">Izbriši</th>

</tr>

<tr>

<th colspan="6">

<input id="thSrchVprID" st-search="VprID" placeholder="Išči po ID" class="input-sm form-control"

type="search" />

<!-- </th>

<th colspan="2"> -->

<input id="thSrchIskanje" st-search placeholder="Iskanje po celotni tabeli" class="input-sm form-

control" type="search" />

<!-- </th>

<th colspan="2"> -->

<input id="thSrchIskanje2" st-search="selectedPredicate" placeholder="Išči po izbranem

stolpcu" class="input-sm form-control" type="search" />

</th>

</tr>

</thead>

<tbody>

<tr ng-repeat="podatek in podatki">

<td>podatek.VprID</td>

<td>podatek.Odg</td>

<td>podatek.ts</td>

<td>podatek.ts2</td>

<td>podatek.SocketID</td>

<td><button type="button" style="font-weight:bold;" ng-click="removeRow(podatek)" class="btn btn-sm

btn-danger">X</button></td>

</tr>

</tbody>

</table>

</div>

</div>

pages/home.html <!-- Verzija: 2017.01.28b -->

<div class='homepage'>

<h1>Glasovalni sistem</h1>

<h2>Pozdravljeni na spletni strani glasovalnega sistema</h2>

<ul class='custom-list'><span style="font-weight:bold;">Administrativni vmesnik</span>

<li>Za prikaz vprašanj in oddajanje glasov, v navigacijski vrstici izberite <a href="#/show-question"

onClick="osveziVprasanje()" class='poudarek'>Vprašanje</a>.</li>

<li>Za dodajanje novega vprašanja, v navigacijski vrstici izberite <a href="#/add-question"

class='poudarek'>Dodaj vprašanje</a>.</li>

<li>Za prikaz rezultatov, v navigacijski vrstici izberite <a href="#/answers"

onClick="izpisRezultatov()" class='poudarek'>Odgovori/rezultati</a>.</li>

</ul>

<br>

<ul class="custom-list"><span style="font-weight:bold;">Uporabniški vmesnik</span>

<li>Za oddajanje glasu, v navigacijski vrstici izberite <b>Vprašanje</b> in nato <a href="#/submit-

vote" onClick="osveziVprasanje()" class='poudarek'>Spletna glasovalna enota</a>.</li>

</ul>

</div>

<button ng-controller="ModalPopup as $ctrl" id="btnPodvojenSocketID" type="button" class="btn btn-default"

ng-click="$ctrl.PodvojenSocketID()" style="display:none">PodvojenSocketID</button>

pages/login.html <div class="homepage">

<h1>Prijavna stran</h1>

<div id="divLogin", ng-controller="LoginController">

<form>

<p>Uporabniško ime: <input type="text", ng-model="username" required=true></p><br>

<p>Geslo: <input type="password", ng-model="password" required=true></p><br>

<button type="submit" class="btn btn-primary" ng-click="sub()">Prijava</button>

</form>

</div>

</div>

pages/show-question.html <!-- Verzija: 2017.04.15b-->

<div class='homepage'>

<h1>Vprašanje</h1>

<div id="divVprasanje", ng-controller="QuestionController">

<div class='celota'>

<p class='vrsticaxody'><span class='poudarek'>Vprašanje: </span>zapStVpr+' od '+stVpr<br>

<p class='trenutnoVprasanje'><b>> </b>vprasanje</p>

<p class="stanjeGlasovanja"><span class='poudarek'>Stanje glasovanja: </span>stanjeGlasovanja</p>

<button id="btnPrejsnjeVpr" onClick="prejsnjeVpr()"><< Prejšnje vprašanje</button>

<button id="btnBeriVpr" onClick="beriVpr()">Naslednje vprašanje >></button>

<button id="btnStartStop" onClick="startStopGlasovanje()">Začni/ustavi glasovanje</button>

<button id="btnRewriteVprasanje" type="button" ng-click="rewriteVprasanje()"

style="display:none"></button>

<button id="btnPotrditevPrejemaOdg" type="button" ng-click="potrditevPrejemaOdg()"

style="display:none"></button>

<button id="btnRewriteGE00odg" type="button" ng-click="rewriteGE00odg()"

style="display:none"></button>

<button id="btnRewriteGE01odg" type="button" ng-click="rewriteGE01odg()"

style="display:none"></button>

<button id="btnRewriteGE02odg" type="button" ng-click="rewriteGE02odg()"

style="display:none"></button>

<button id="btnRewriteGE03odg" type="button" ng-click="rewriteGE03odg()"

style="display:none"></button>

<button id="btnRewriteWebGETabela" type='button' ng-click="rewriteWebGETabela()"

style="display:none">Osveži podatke WebGE tabele</button>

<button id="btnStartStopGL" type="button" ng-click="startStopGl()" style="display:none"></button>

</div>

<div class='enatretjina'>

</div>

<table style="width:100%">

<tr>

<th id="thGEnota">Glasovalna enota</th>

<th id="thStanjeOdg">Stanje povezave</th>

<th id="thPrva">Stanje odgovora</th>

</tr>

<tr>

<th>GE01</th>

<th><output id="ge00Connection" type="text">X</output></th>

<th><output id="ge00" type="text">ge00odg</output></th>

</tr>

<tr>

<th>GE02</th>

<th><output id="ge01Connection" type="text">X</output></th>

<th><output id="ge01" type="text">ge01odg</output></th>

</tr>

<tr>

<th>GE03</th>

<th><output id="ge02Connection" type="text">X</output></th>

<th><output id="ge02" type="text">ge02odg</output></th>

</tr>

<tr>

<th>GE04</th>

<th><output id="ge03Connection" type="text">X</output></th>

<th><output id="ge03" type="text">ge03odg</output></th>

</tr>

</table>

<table style="width:100%" st-table="WebGETabela" class="table table-striped">

<thead>

<th id="thIP" st-sort="IP">IP</th>

<th id="thSocketID" st-sort="SocketID">SocketID</th>

<th id="thPrva" st-sort="stanjeOdg">Stanje odgovora</th>

</thead>

<tbody>

<tr ng-repeat="WebGEData in WebGETabela">

<td>WebGEData.IP</td>

<td>WebGEData.SocketID</td>

<td>WebGEData.stanjeOdg==="0"?"Čakam odgovor...":"Ob "+WebGEData.hms+" "+WebGEData.stanjeOdg+".

prejem odgovora na izpisano vprašanje."</td>

</tr>

</tbody>

</table>

</div>

</div>

<button ng-controller="ModalPopup as $ctrl" id="btnZadnjeVpr" type="button" class="btn btn-default" ng-

click="$ctrl.ZadnjeVpr()" style="display:none">Zadnje vprašanje</button>

<button ng-controller="ModalPopup as $ctrl" id="btnPrazenOdg" type="button" class="btn btn-default" ng-

click="$ctrl.PrazenOdg()" style="display:none">Prazen odgovor</button>

<button ng-controller="ModalPopup as $ctrl" id="btnNiVpr" type="button" class="btn btn-default" ng-

click="$ctrl.NiVpr()" style="display:none">Ni vprašanja</button>

<button ng-controller="ModalPopup as $ctrl" id="btnPodvojenSocketID" type="button" class="btn btn-default"

ng-click="$ctrl.PodvojenSocketID()" style="display:none">PodvojenSocketID</button>

pages/statistics.html <!-- Verzija: 2017.01.20a -->

<div class="homepage">

<h1>Statistika rezultatov</h1>

<button id="btnIzpisStatistike" onClick="izpisStatistike()" style="display:none">Izpiši statistiko</button>

<div id="divStatistikaRezultatov", ng-controller="StatisticsController">

<button id="btnRewriteStatistika" type='button' ng-click="rewriteStatistika()"

style="display:none">Naloži podatke</button> <!-- gumb je skrit, ker se ga uporablja v

'socketPosiljanjeVprasanj' za izpis nove vrstice v Angular tabelo -->

<form>

<label id="predicate2" for="predicate">Izberi ime stolpca, po katerem želiš iskati:</label>

<select class="form-control" id="predicate" ng-model="selectedPredicate" ng-options="predicate for

predicate in predicates"></select>

</form>

<table st-table="odgovori" class="table table-striped">

<thead>

<tr>

<th id="thVprID" st-sort="VprID">ID</th>

<th id="thPrva" st-sort="vprasanje">Vprašanje</th>

<th id="thRezultat" st-sort="seStrinjam">Se strinjam</th>

<th id="thRezultat" st-sort="vzdrzan">Vzdržan</th>

<th id="thRezultat" st-sort="seNeStrinjam">Se ne strinjam</th>

<th id="thSkupaj" st-sort="skupaj">Skupaj</th>

</tr>

<tr>

<th colspan="6">

<input id="thSrchVprID" st-search="VprID" placeholder="Išči po ID" class="input-sm form-control"

type="search" />

<input id="thSrchIskanje" st-search placeholder="Iskanje po celotni tabeli" class="input-sm form-

control" type="search" />

<input id="thSrchIskanje2" st-search="selectedPredicate" placeholder="Išči po izbranem

stolpcu" class="input-sm form-control" type="search" />

</th>

</tr>

</thead>

<tbody>

<tr ng-repeat="odgovor in odgovori">

<td>odgovor.VprID</td>

<td>odgovor.vprasanje</td>

<td>odgovor.seStrinjam</td>

<td>odgovor.vzdrzan</td>

<td>odgovor.seNeStrinjam</td>

<td>odgovor.skupaj</td>

</tr>

</tbody>

</table>

</div>

</div>

pages/submit-vote.html <!-- Opis: Spletna stran za prikaz vprašanja in oddajo glasu prek spletne glasovalne enote. -->

<!-- Verzija: 2017.04.04c -->

<!-- ==================================================================================================== -->

<div class="homepage">

<h1>Spletna glasovalna enota</h1>

<div id="divWebGE" ng-controller="WebGEController">

<div class='dvetretjini'>

<p class='vrsticaxody'><span class='poudarek'>Vprašanje: </span>zapStVpr+' od '+stVpr<br>

<p class='trenutnoVprasanje'><b>> </b>vprasanje</p>

<p class="stanjeGlasovanja"><span class='poudarek'>Stanje glasovanja:

</span>stanjeGlasovanja</p><br>

<button id="btnRewriteVprasanje" type="button" ng-click="rewriteVprasanje()"

style="display:none"></button>

<button id="btnPotrditevPrejemaOdg" type="button" ng-click="potrditevPrejemaOdg()"

style="display:none"></button>

<button id="btnStartStopInfo" type="button" ng-click="startStopGlWebGE()"

style="display:none"></button>

</div>

<div class='enatretjina'>

<p class='poudarek'>Odgovori:</p>

<form action="">

<div class='radioLine'><input type="radio" name="Q" id="strinja"> Se strinjam</div>

<div class='radioLine'><input type="radio" name="Q" id="vzdrzan"> Vzdržan</div>

<div class='radioLine'><input type="radio" name="Q" id="nestrinja"> Se ne strinjam</div>

</form>

</div>

<button id="btnOddajOdg" onClick="oddajOdg()">Oddaj odgovor</button><br>

<p class='povratna'>potrZapOdg</p>

</div>

</div>

<button ng-controller="ModalPopup as $ctrl" id="btnPrazenOdg" type="button" class="btn btn-default" ng-

click="$ctrl.PrazenOdg()" style="display:none">Prazen odgovor</button>

<button ng-controller="ModalPopup as $ctrl" id="btnNiVpr" type="button" class="btn btn-default" ng-

click="$ctrl.NiVpr()" style="display:none">Ni vprašanja</button>

<button ng-controller="ModalPopup as $ctrl" id="btnPodvojenSocketID" type="button" class="btn btn-default"

ng-click="$ctrl.PodvojenSocketID()" style="display:none">PodvojenSocketID</button>

<button ng-controller="ModalPopup as $ctrl" id="btnGlasovanjeStop" type="button" class="btn btn-default" ng-

click="$ctrl.GlasovanjeStop()" style="display:none">Ustavljeno glasovanje</button>

pages/popup/delete-warning.html <div class="modal-header">

<h3 class="modal-title" id="modal-title">Opozorilo!</h3>

</div>

<div class="modal-body" id="modal-body">

<p>

Nadaljuj z brisanjem?

</p>

</div>

<div class="modal-footer">

<button class="btn btn-primary" type="button" ng-click="$ctrl.ok()">Da</button>

<button class="btn btn-warning" type="button" ng-click="$ctrl.cancel()">Ne</button>

</div>

pages/popup/duplicated-answer.html <div class="modal-header">

<h3 class="modal-title" id="modal-title">Opozorilo!</h3>

</div>

<div class="modal-body" id="modal-body">

<p>

Podvojen odgovor!

</p>

</div>

<div class="modal-footer">

<button class="btn btn-primary" type="button" ng-click="$ctrl.ok()">OK</button>

</div>

pages/popup/duplicated-socketid.html <div class="modal-header">

<h3 class="modal-title" id="modal-title">POZOR!</h3>

</div>

<div class="modal-body" id="modal-body">

<p>

Zaznali smo nedovoljen podvojen dostop z istega IP-naslova.<br><br>

V primeru, da je spletna stran glasovanja odprta v drugem zavihku ali brskalniku, uporabite le-tega in

trenutnega zaprite.

</p>

</div>

<div class="modal-footer">

<button class="btn btn-primary" type="button" ng-click="$ctrl.ok()">OK</button>

</div>

pages/popup/empty-answer.html <div class="modal-header">

<h3 class="modal-title" id="modal-title">Opozorilo!</h3>

</div>

<div class="modal-body" id="modal-body">

<p>

Prazen odgovor!<br>

Izberi in ponovno oddaj odgovor.

</p>

</div>

<div class="modal-footer">

<button class="btn btn-primary" type="button" ng-click="$ctrl.ok()">OK</button>

</div>

pages/popup/empty-question.html <div class="modal-header">

<h3 class="modal-title" id="modal-title">Opozorilo!</h3>

</div>

<div class="modal-body" id="modal-body">

<p>

Vnosno polje za dodajanje vprašanja je prazno!

</p>

</div>

<div class="modal-footer">

<button class="btn btn-primary" type="button" ng-click="$ctrl.ok()">OK</button>

</div>

pages/popup/last-question.html <div class="modal-header">

<h3 class="modal-title" id="modal-title">Opozorilo!</h3>

</div>

<div class="modal-body" id="modal-body">

<p>

Dosegel si zadnje vprašanje.<br>

Za nadaljevanje dodaj novo vprašanje.

</p>

</div>

<div class="modal-footer">

<button class="btn btn-primary" type="button" ng-click="$ctrl.ok()">OK</button>

</div>

pages/popup/login.html <div class="modal-header">

<h3 class="modal-title" id="modal-title">Prijava</h3>

</div>

<div class="modal-body" id="modal-body">

<form>

Uporabniško ime: <input type="text" ng-model="username">

Geslo: <input type="password" ng-model="password">

</form>

</div>

<div class="modal-footer">

<button class="btn btn-primary" type="button" ng-click="$ctrl.login()">OK</button>

</div>

pages/popup/no-question.html <div class="modal-header">

<h3 class="modal-title" id="modal-title">Opozorilo!</h3>

</div>

<div class="modal-body" id="modal-body">

<p>

Za oddajo odgovora najprej izberi vprašanje!

</p>

</div>

<div class="modal-footer">

<button class="btn btn-primary" type="button" ng-click="$ctrl.ok()">OK</button>

</div>

pages/popup/vote-stop.html <div class="modal-header">

<h3 class="modal-title" id="modal-title">Opozorilo!</h3>

</div>

<div class="modal-body" id="modal-body">

<p>

Glasovanje je ustavljeno!<br>Za oddajo glasu počakaj na zagon glasovanja.

</p>

</div>

<div class="modal-footer">

<button class="btn btn-primary" type="button" ng-click="$ctrl.ok()">OK</button>

</div>