Skip to main content

Sovellusohjelmat

Sovellusohjelma ja tehtävä ovat tässä dokumentissa oikeastaan synonyymejä. Käyttäjän kannalta sovellusohjelma sisältää lua koodin lisäksi myös mm. käyttöliittymän grafiikkasivut, joita ei käsitellä tässä dokumentissa. 

Tehtävien eli task:ien luominen

Mikäli käytetään vakio versiota käynnistys skriptistä, on tehtävien luominen järjestelmään hyvin helppoa, sillä käynnistys skripti etsii kaikki ”.lua” -päätteiset tiedostot /opt/slc/prg/run/ hakemistosta, ja käynnistää jokaisen niistä omana tehtävänään. Mikäli jossakin yhteydessä on tärkeätä hallita näiden ohjelmien käynnistysjärjestystä, se voidaan tehdä nimeämällä ohjelmatiedostot sopivasti. Järjestelmä nimittäin ensiksi hakee kaikkien hakemistossa sijaitsevien tiedostojen nimet, järjestää ne aakkosjärjestykseen – tai oikeastaan numerojärjestykseen ASCII merkistötaulukon mukaisesti pienimmästä suurimpaa. Näin ollen antamalla tiedostonimille alkuliitteet vaikkapa 01 .. 99 saadaan ohjelmien käynnistysjärjestys halutuksi. Esimerkin tapauksessa tiedosto joka alkaa 01 ladataan ja suoritetaan ensimmäisenä, ja tiedosto joka alkaa 99 ladataan viimeisenä.

Actiweb laitteen /opt/slc/prg/run/ hakemisto Kuvassa on näkyvissä Actiweb laitteen /opt/slc/prg/run/ hakemistossa olevat automaattisesti käynnistyvät autorun ohjelmat.

Kuten sanottua, uusia task:eja eli tehtäviä on mahdollista luodaan lisäämällä uusi ”.lua”-päätteinen tiedosto /opt/slc/prg/run/-hakemistoon. Kun tällainen automaattisesti käynnistettäv ohjelma ladataan, ja järjestelmä luo siitä taskin, annetaan sille aluksi seuraavat oletusasetukset: 

• Suoritusväli 1000 ms
• Ajoitustapa cyclic (eli suorituskertojen välillä odotetaan 1000 ms)
• Nimi on tiedoston nimi

Näitä oletusarvoja on mahdollista muuttaa Slc kirjaston kutsuilla Slc.setTaskName (), Slc.setSchedule (), Slc.setTiming (). Tätä hakemistoa kutsutaan lyhyesti autorun hakemistoksi.

Latausprosessi toimii siten, että ensiksi kääntäjä lataa ja kääntää autorun-hakemistossa olevan lähdekooditiedoston. Mikäli kyseinen tiedosto viittaa muihintiedostoihin esimerkiksi ”require” käskyllä, ne ladataan ja käännetään samalla hetkellä kun kääntäjä törmää näihin viittauksiin. Alkuperäisen tiedoston kääntäminen jatkuu kun viitattu tiedosto on käsitelty. 

Kun tiedosto on käännetty tavukoodiksi laitteen muistiin, se suoritetaan kokonaisuudessaan yhden kerran. Tämän jälkeen käynnistyy varsineinen ajoitettu suoritussykli, jossa actiweb ohjelmisto kutsuu asetetulla suoritusvälillä ohjelman niin sanottua pääfunktiota. Autorun hakemistosta käynnistetyillä ohjelmilla se on aina nimeltään ”main”. Tämä nimitys mukailee monia ohjelmointikieliä kuten C, C++, Java tai C#. 

Tämä suoritustapa tarkoittaa sitä, että pääfunktion ulkopuolinen ohjelmakoodi suoritetaan yhden kerran käännösvaiheen jälkeen, eli siellä voidaan hoitaa esimerkiksi erilaisia alustukseen liittyviä toimenpiteitä, kuten määrittää asetuksia tai globaaleita muuttujia. 

Alla on esimerkki hyvin yksinkertaisesta Lua kielisestä sovellusohjelmasta, joka antaa hälytyksen mikäli mittaus ylittää raja-arvon. Ohjelma olettaa että pistetietokannassa on olemassa ”TE20” ja ”TE20_HI_AL” nimiset tietokantapisteet.

Esimerkki 1:    
/opt/slc/prg/run/example1.lua

Slc.setSchedule (”periodic”)
Slc.setTiming (1000)
Slc.setTaskName (”simpleAlarmTask”)

function main()
	local m = Data.getReal (”TE20.pv”)
	local hilimit = Data.getReal (”TE20.hi”)
	local hyst = 0.5
	if m > hilimit then
		Data.set (”TE20_HI_AL.pv”, 1)
	elseif m < (hilimit - hyst) then
		Data.set (”TE20_HI_AL.pv”, 0)
	end
end

Yllä oleva ohjelma käynnistyy siis automaattisesti omaksi prosessikseen kun tiedosto on luotu, ja näkyy siten myös käyttöliittymässä system→setting -sivun alaosan tehtävälistassa. Tuossa listassa näytetään Slc.setTaskName () kutsulla asetettu nimi.

On hyvä ymmärtää kuinka lua tiedostostot ladataan ja suoritetaan. Koska lua on käyttäjän kannalta tulkattu kieli, näyttää tilanne siltä että sovellusohjelmat (ja muutkin komennot) suoritetaan suoraan tekstitiedostosta tai komentiriviltä, joka on täysin poikkeavata esimerkiksi C-kieleen verrattuna. Todellisuudessa lua-koodi käännetään samalla hetkellä kun käyttäjä pyytää koneelta että lähdekooditiedosto ajetaan. Tiedosto käydään lävitse, ja mikäli siinä ei ole syntaksi-virheitä, siitä tehdään RAM muistiin niin sanottu tavukoodi ”möhkäle” - eli chunk. Tavallisesti tätä tavukoodikäännöstä ei kirjoiteta levylle. Tämän jälkeen perinteisen lua (tai python) ympäristön tapauksessa  tulkki ryhtyy sitten suorittamaan tätä tavukoodia, ja osa virheistä paljastuu vasta tässä vaiheessa. Slc engine käyttää kuitenkin lua ohjelmien suorittamiseen niin sanottua JIT kääntäjää, jolloin tavukoodi käännetäänkin suoraan natiiviksi konekieliseksi ohjelmakoodiksi, jonka prosessori sitten suorittaa. Teoriassa ohjelman kääntäminen kestää tässä tapauksessa hieman kauemmin, mutta toisaalta, ohjelmakoodin suorittaminen on hyvin nopeata.


Funktiot ja globaalit muuttujat

Alla olevassa esimerkissä näytetään muutama hieman edistyneempi rakenne.

Esimerkki 2:
/opt/slc/prg/run/example2.lua

Slc.setSchedule (”periodic”)
Slc.setTiming (1000)
Slc.setTaskName (”mySecondTask”)

counter = 0

function inc(i)
	return i+1
end

function main()
	counter = inc(counter)
	Slc.echo (”Round ”.. counter )
end

Ensimmäinen tärkeä seikka on counter muuttuja. Sen edessä ei ole local sanaa, jolloin se määrittyy globaaliksi muuttujaksi, ja on esittelyn jälkeen käytössä kaikkialla ohjelmassa. Globaaleilla muuttujilla on lisäksi tärkeä piirre, että ne säilyttävät arvonsa suorituskertojen välillä, eli niin kauan kuin Lua suoritusympäristöä ei alusteta uudelleen. Tämä tapahtuu pääasiassa silloin, kun koko Actiweb ohjelma uudelleenkäynnistetään – eli esimerkiksi painetaan restart -painiketta käyttöliittymässä, tai koko laitteisto uudelleenkäynnistyy. Joitakin laskureita ja viiveitä voi siis aivan hyvin tehdä jopa ylläolevalla tavalla.

Toinen huomionarvoinen piirre on funktio inc() – tämä liittyy itse lua-kieleen. Ohjelmien ylläpidon ja uudelleenkäytettävyyden kannalta olisi hyvä tapa, että samaa ohjelmakoodia kirjoiteta kuin yhden kerran. Tätä varten useimmissa ohjelmointikielissä on jokin tapa luoda aliohjelmia, jotka tekevät tietyn. Lua kielessä ne kuvataan function-avainsanan avulla, ja ohjelmointikielestä ja tilanteesta riippuen niitä voidaan kutsua fuktioiksi, rutiineiksi tai proseduureiksi. Kun ohjelman toistuvista osista tehtään funktio, eräs suurimmista hyödyistä on, että siinä olevaa virhettä ei tarvitse korjata kuin yhteen paikkaan. Toisaalta, kun rutiini nimetään järkevästi, se myös yksinkertaistaa sitä ohjelman kohtaa, josta rutiinia kutsutaan. Se taas on ohjelman monimutkaisuuden hallinnan kannalta korvaamatonta, ja  myös eräs proseduraalisen ohjelmointitavan keskeisistä ajatuksista. 

Oliot

Ohjelman ymmärrettävyyden kannalta on usein hyödyksi, kun sekä data, että toiminnallisuus (eli rutiinit) saadaan liimattua yhteen, jolloin tuloksena on olio.

Vaikka tämän oppaan tarkoituksena ei ole opettaa ohjelmointia yleisesti, eikä lua kieltä erityisesti, vain esittää mitä lua kielisten ohjelmien kirjoittaminen Slc engine ympäristöön vaatii, käymme seuraavassa lävitse olioiden (eng. objects) luomisen perusteet.

Lua kielessä oliot perustuvat prototyyppeihin ja constructor funktioihin (kuten myös esimerkiksi ECMA script -kielessä). C++ ja Java kielissä taas oliot luodaan n.s. luokkien pohjalta, mikä on hieman byrokraattisempi lähestymistapa, ja sopii paremmin vahvasti tyypitettyihin kieliin. 

Esimerkki 2:    
/opt/slc/prg/run/example3.lua

Slc.setSchedule (”periodic”)
Slc.setTiming (1000)
Slc.setTaskName (”mySecondTask”)

-- Lets create object prototype cSimplePump
cSimplePump = {}
cSimplePump.new = function (s, idDI, idDO, idAL) 
	local c = {}
	c.idDI = (idDI or ””)
	c.idDO = (idDO or ””)
	c.idAL = (idAL or ””)
	
	local c.state = 0		-- off by default

	c.setState = function (s, nState)
		if type(nState) ~= ”number” then
			return false
		elseif nState<0 then
			return false
		elseif nState>1 then
			return false
		else	
			s.state = nState
			return true
		end
	end

	c.runLogic = function (s) 
		-- Control conflict alarm
		local DI = Data.getReal (s.idDI)
		local DO = Data.getReal (s.idDO)
		if DI ~= DO then
			Data.set ( s.idAL, 1)
		else
			Data.set ( s.idAL, 0)
		end

		-- Control logic
		if s.state == 1 then
			Data.set ( s.idDO, 1)
		else
			Data.set ( s.idDO, 0)
		end
	end
					
	return c
end

--  Create pumps
PU01 = cSimplePump:new (”PU01_DI.pv”, ”PU01_DO.pv”, ”PU01_AL.pv”)
PU02 = cSimplePump:new (”PU02_DI.pv”, ”PU02_DO.pv”, ”PU02_AL.pv”)
PU03 = cSimplePump:new (”PU03_DI.pv”, ”PU03_DO.pv”, ”PU03_AL.pv”)

function main()
	-- Handle all pumps
	if Data.getReal (”PU_ENABLED.pv”) > 0 then 
		PU01:setState (1)
		PU02:setState (1)
		PU03:setState (1)
	else
		PU01:setState (0)
		PU02:setState (0)
		PU03:setState (0)
	end

	PU01:runLogic ()
	PU02:runLogic ()
	PU03:runLogic ()
end

Ylläoleva esimerkki on pyritty pitämään niin yksinkertaisena kuin mahdollista, ja sen on tarkoitus havainnollistaa sitä, että 

  • Miten objektityyppi luodaan - tässä tapauksessa cSimplePump konstruktori.
  • Miten konstruktorin avulla luodaan uusia olioita. Tässä tapauksessa cSimplePump tyyppisiä olioita.
  • Miten noita olioita on mahdollista käyttää.

ylläolevassa esimerkissä näytetään siis ensiksi, kuinka oliotyypille varataan ensiksi tyhjä taulukko, ja sitten määritellään n.s. konstruktori, jonka avulla luokan oliot rakennetaan. Konstruktorin nimellä ei sinänsä ole mitään merkitystä, vaan tärketätä on toiminnallisuus; Konstruktori on funktio, joka palauttaa aina kutsuttaessa tietyllä tavalla rakennetun olion. Konstruktori voi ollaa parametreina olion alustamiseen vaikuttavia tietoja, kuten tässä tapauksessa pistetunnuksia, jotka täytetään luotavaan olioon konstruktorissa.

Konstruktorin sisällä uutta oliota ryhdytään kasaamaan luomalla yhtä taulukko ’c’. Ensiksi siihen määritellään data-alkiot joita jokaisella tämän tyyppisellä oliolla on, ja sen jälkeen olion metodit, eli funktiot jotka muokkaavat olion omaa tilaa (eli dataa), ja joita kutsumalla oliota voidaan käyttää.

Olioiden kanssa puuhaillessa on hyvä muistaa niiden tärkeimmät edut; rajapinnat ja tiedon piilottaminen;  Nämä seikat ovat tavallaan saman asian kaksi puolta. Plc ohjelmoinnissa olioita ovat muistuttaneet IEC61131 standardin kuvaamat Funktioblokit, mutta toisaalta, niistä puuttuu monia tärkeitä piirteitä, kuten juuri metodit. Vaikka lua ei kielenä sitä varsinaisesti rajoitakaan, ei olioiden sisäisiin muuttujiin (tässä tapauksessa eism. idDO tai status) saisi koskaan viitata suoraan, vaan niille tiedoille joita pitäisi päästä muuttamaan ulkoa päin, tulisi tehdä set() ja get() -metodit (eli funktiot). Näin voidaan varmistaa että olion tila (eli sen sisäiset muuttujat) pysyvät tietyissä rajoissa. Toisaalta, metodit tarjoavat selkeän rajapinnan olion käyttämiseen. Yllä olevassa esimerkissä tehtyä cSimplePump oliota voisi laajentaa esimerkiksi toteuttamaan vuorottelu automaattisesti, ja se olisi edelleen ohjelmoijalle yhtä helppo käyttää; kutsutaan olion run() metodia tasaisin väliajoin. Kaikki vuorottelun aiheuttama monimutkaisuun jää piiloon olion sisään.

Lua on dynaaminen ohjelmointikieli

Heikosti tyypitetyt dynaamiset ohjelmointikielet – kuten Lua, Python ja Javascript – sisältävät piirteitä, jotka voivat vaatia vahvasti tyypitettyihin kieliin tottuneelta ohjelmoijalta hieman totuttelua ja ajattelutapojen muutosta.

Kenties suurin muutos on se, että koska muuttuja myData voi sisältää rivillä 5 merkkijonon, rivillä 20 taulukon, ja rivillä 50 numeraalisen arvon, kääntäjä ei ennen ohjelman suorittamista antaa virheilmoitusta mikäli muuttuja sisältää väärän tyyppistä tietoa, tai jos sitä ei ole olemassa ollenkaan. Tämä aiheuttaa ongelmia kun muuttujien arvoja vertaillaan toisiin arvoihin, vakioihin, tai niille koetetaan tehdä matemaattisia operaatioita. Muuttujan sisältämän datan tyypin voi tarkistaa käyttämällä type() kutsua, ja se voi palauttaa arvot ”string”, ”number”, ”boolean”, ”table”, ”function”, ”thread”, ”userdata” tai ”nil”. Nämä ovat Lua kielen tuntemat datatyypit.

Ohjelmakoodissa voi olla rivi

myData = myData + 1

Ja mikäli myData sisältää tuossa vaiheessa arvon false, aiheutuu siitä virhe ”runtime error”.

Dynaamisissa kielissä on tyypillinen piirre, että muuttujat ja objektit ovat olemassa vain niin kauan kuin niitä tarvitaan, eikä niitä tarvitse esitellä etukäteen millään tavalla. Muuttuja luodaan, kun sille annetaan arvo. Ja toisaalta, ja se lakkaa olemasta samalla hetkellä, kun sen arvoksi asetetaan nil. Kääntäen, muuttuja on olemassa, jos sen arvo on jotakin muuta kuin nil.


Esimerkikki: 

if myData ~= nil then
    print (”Variable exists”)
else
    print (”variable does not exists”)
end

Esimerkki:

    

if type (myData) == ”string” then
        -- do something
end

Lua kielessä muuttujien joustavuutta kannattaa käyttää hyödyksi, ja haittoja voi minimoida käyttämällä loogisia operaatioita. Niiden avulla ohjelmien rakennetta saa usein yksinkertaistettua, eikä aina ole tarvetta käyttää if-käskyjä:

Esimerkikki: 
    
    -- Jos myData on nil (ei olemassa) käytetään arvoa 0 sen tilalla
myData = (myData or 0) + 1
    
Esimerkikki: 
    
myData = (type(myData) ~= ’number) and 0 or myData
    
Yllä olevassa esimerkissä tarkistetaan onko myData:n tyyppi numero, ja jos ei, annetaan sille arvo 0, muutoin se pitää nykyisen arvonsa. Voisi sanoa, että yllä olevassa esimerkissä arvoa 0 käytetään muuttujan oletusarvona, jota käytetään mikäli muuttujaa ei ole olemassa, tai se on eri tyyppiä kuin pitäsi.

Lua sisältää myös tonumber ja tostring nimiset komennot jolla tulkki tai kääntäjä koettavat muuttaa muuttujan arvon joko numeroksi tai merkkijonoksi. Tämä muunnos tosin täytyy yleensä yhdistää ehdolliseen sijoitukseen, kutsut voivat palauttaa arvon nil mikäli muunnos ei onnistu -  esimerkiksi jos koetetaan muuttaa taulukkoa numeroksi.

    -- Not like this
    myData = tonumber (myData)

    -- but do it like this
    myData = (tonumber (myData) or 0)

Monista kielistä löytyvää ehdollista sijoistusta (eng. ternary operator) ei Lua kielestä löydy. Sen sijaan, vastaavan toiminnallisuuden saavuttaa Lua kielessä usein käyttämällä and ja or  operaatioita luovasti yllä olevaa esimerkkiä mukaillen. 

Esimerkki (ei toimi Lua kielessä): 

C-kielessä käytetty ehdollinen sijoitus (ternary operator) näyttää tältä:

    int i = (r > 10) ? 1 : 0;        // i gets value 1 if r is grater than 10

Ja vaikka täysin vastaavaa rakennetta ei lua kielessä suoraan olekkaan, sitä vastaava toiminnallisuus ja hyvin saman kaltainen luettavuus on saavutettavissa and ja or operaatioilla. Tässä yhteydessä tulee kuitenkin kiinnittää erityistä huomiota  suoritusjärjestykseen, jotta operaatio toimii aina odotetulla tavalla.

    local i = (r > 10) and 1 or 0        -- i gets value 1 if r is greater than 10

Samalla logiikalla voidaan tätä rakennetta käyttää myös seuraavilla tavoilla

    local i = (r == nil) and 0 or i
    
    local i = (type(r) == ”string”) and r or ”none”

    local i = (type(r) == ”number”) and r or -999

Tässä rakenteessa tärkeimmät mm. suoritusjärjestyksestä johtuvat piirteet ovat, että and operaatio palauttaa arvon, joka annetaan sen oikealla puolella (eli järjestyksessä jälkimmäisen), jos molemmat puolet ovat totussarvoltaan true. Sellaisia arvoja ovat kaikki muut paitsi false ja nil. Or -operaatio palauttaa taas järjestyksessä ensimäisen arvon, joka on totuusarvoltaan true. 

Täten, yllä oleva rakenne palauttaa and opertaation jälkimmäisen arvon, mikäli molemmat puolet ovat tosia, ja toisaalta, or operaation oikean puolen jos and operaatio palauttaa arvon epätosi.

Lisää tietoa tästä mm. lua-käyttäjien wiki sivulla: 
http://lua-users.org/wiki/TernaryOperator