Arduino konyhapult világítás

Egy Arduino Nano, egy MOSFET meghajtó, led szalagok, alu profilok, kábelcsatornák, 20m vezeték és egy hét szabadidő.

Bevezető

Egy ideje érdekel már a mikrokontrollerek világa, ezen belül is az Arduino platform. Digitális RGB LED szalagok kapcsán kezdtem el ismerkedni vele és építettem már egy-két dolgot. Szüleimet szerettem volna meglepni egy igazán praktikus és több szempontból hasznos megoldással, aminek az elkészítését szeretném most bemutatni.
Dióhéjban: egy félautomata mozgás- és fényérzékelős világítást készítettem nekik, éjszakai irányfénnyel és 24/7-es készenléttel.

Szó lesz az összetevőkről, összeszerelésükről és a vezérlő felprogramozásáról. A barkácsolásra nem fogok nagyon kitérni, mert ez a része talán nem igényel túl sok magyarázatot, hisz úgy gondolom elég sok ember képes azonosulni a fúrással és faragással. Inkább a technikai részletekkel és a program bemutatásával kívánok foglalkozni. Nem a teljesen kezdőknek szánom e sorokat, így akikben már a bevezető láttán kérdések merülnek fel, nekik összeszedtem alább egy csokornyi kapcsolódó cikket az alapok megismeréséhez.

Alkatrészek kiválasztása

Először is azt kell eldöntenünk milyen igényeknek szeretnénk megfelelni. Pl. egy RGB LED szalag kiváló dekor- és hangulatvilágításnak, de csapnivaló fényt ad bármilyen munkához. (Tudom, próbáltam.)
Az én esetemben konyhai használatra elsősorban kitűnő semleges fehér fény volt a szempont, ami kellemes a szemnek és kellő fényt biztosít a magabiztos munkavégzéshez. A színhűség nem lényeges, így megelégedtem az Aliexpress kínálatával. Bár kaphatók magas CRI értékű szalagok is (színvisszaadási mutató), ahol fontos a fényminőség és így a megvilágított terület színhelyes bemutatása.

Erre a feladatra manapság az 5630/5730 SMD LEDek a legelterjedtebbek, amiket a felső szekrényekre vettem:
Nyitófeszültség: 3 V
Áramerősség: 150 mA
Fényerő: 54-60 lm
Sugárzási szög: 160 °

Ezt egyszínű fehérben gyártják többféle színhőmérséklettel. Lábjegyzetbe hozzáfűzném, hogy ez egy nagyteljesítményű dióda és hűtés nélkül percek alatt tönkremehet. Így a szalagot kötelezően alumínium profilba kell telepíteni passzív hűtés gyanánt.

Választható még az 5050 SMD LED is, melyet az alsó szekrényre vettem:
Nyitófeszültség: 3 V
Áramerősség: 60 mA
Fényerő: 9000 mCd
Sugárzási szög: 120 °

A ledek specifikációit a www.artled.hu-ról vettem.
Hasonlóan a fentihez ez is többféle színhőmérséklettel rendelhető és nem feltétlen igényel hűtést, így akár önmagában is felragasztható. Viszont erősen ajánlott az alu sín, így garantálható mind a hosszú élettartam, mind a biztos felszerelés - a bútorlap alján hamar elenged a ragasztócsík.

Létezik még egy ún. 2835-ös LED dióda is - amiről én is csak a cikk írásakor olvastam először -, ez állítólag az 5630 fényerejével bír és az 5050 teljesítmény felvételével. Ha ez igaz, akkor ez tűnik jelenleg a legfejlettebbnek és a leghatékonyabbnak mind közül.

Szükségünk lesz egy tápra is, melyből az ATX tápokra esett a választásom. Egyrészt mert ismervén kitűnő megbízhatóságukat és beépített védelmeiket, relatíve könnyű szívvel bízom meg még egy márkás használt példányban is, szemben egy random kínai kompakt LED-es táppal, vagy a kétes minőségű fém házas csavar terminálos változatokkal, vagy valamilyen utángyártott noname "laptop töltő" formátummal. Másrészt mert egy kiszuperált ATX tápot néhány sörért az ember után hajítanak, míg a nagyobb teljesítményű céleszközök még Kínából rendelve is tisztességes árakon kelletik magukat, helyi szaküzletben (bár garantált minőségben) meg kifejezetten drágák. Harmadrészt meg, mert ha már Arduino, akkor célszerű megtanulni hogyan is módosíthatjuk ezeket igényeinknek megfelelően, ugyanis a vezérlésük pofon egyszerű és nem utolsó sorban több különböző feszültségszintet szolgáltat mindenféle felhasználáshoz. Azért ez elég nyomós érv lehet, mert nem kell külön tápról gondoskodnunk a mikrokontroller számára.*
Sőt! Az ATX tápok készenléti 5V-ja az esetek többségében több, mint elég és egyben kiváló megoldás 24/7-es felhasználásra.

Mikrokontrollerből egy kínai Arduino Nano-ra esett a választásom; kicsi mérete, olcsó ára és könnyű programozhatósága miatt. Ez egy 5V-os 8 bites processzor, mely mini USB-ről programozható és 22 db I/O lába több, mint elég erre a feladatra.

A nagy áramfelvételű fogyasztókhoz - mint amilyenek a LED szalagok - kelleni fog pár mosfet is (mivel a mikrovezérlők legfeljebb pár tíz mA-t tudnak leadni), amik működésükben a tranzisztorokhoz hasonlóak csak jóval nagyobb áramot elbír. Elméletileg akár 116 ampert is amit választottam (IRL2203) és e mellett 5V-os ún. logikai szinten vezérelhető (akár közvetlenül a mikrovezérlő lábáról). Ezek gyors kapcsolásához meghajtó chip-eket használtam - amikkel akár MHz-es tartományban is lehet működtetni a mosfet-eket. De legegyszerűbb formájában akár így is használható:

A mosfet driver egy saját tervezésű NYÁK-ra épül, mely 4 csatornát képes meghajtani 5-15V között. RGBW analóg szalaghoz készítettem PWM vezérléshez, de itt most csak két csatornát használok. Külön vezérlem a felső és alsó szalagokat.
A kábel menedzsment úgy tűnik nem az erősségem.

Ugyanakkor lehet találni többféle modult rákeresve a RGB LED strip driver module kulcsszavakra, mint pl. az alábbi három csatornás változatot, amiből akár többet is sorba köthetünk.

Ez egyébként egy PWM chip-el szerelt változat, amivel valami I2C-re emlékeztető protokollal lehet kommunikálni - van rá Arduino könyvtár, néhány utasítás az egész, szóval piszok egyszerű használni. Ennek a megoldásnak annyi előnye lehet az enyémmel szemben, hogy itt a PWM vezérlő közvetlenül a mosfetek közelében vannak és így talán kevésbé érzékeny az interferenciára. Nem úgy, ha a mikrovezérlőt és a mosfet meghajtó chip-eket 20 cm vezetékkel kötöm össze, ami felszed mindenféle zajt - mint az kiderült menet közben. Ami nem túl szerencsés, ha van egy chip-ed, ami akár MHz-es tartományban is tudja nyitni és zárni a mosfet-eket...

Kelleni fog egy kapcsoló is a kézi fel- lekapcsolásra, mely a teljesség igénye nélkül lehet: bármilyen mechanikus nyomógomb vagy billenőkapcsoló, mikrokapcsoló, kapacitív érintés érzékelő, inkrementális jeladó vagy bármilyen emberi interakcióra alkalmas szenzor.
Én érintés érzékelővel oldottam meg, de újragondolva a dolgot szerintem a rotary encoder avagy inkrementális jeladó volna a legjobb: egyszerre szolgál nyomógombként a ki- bekapcsoláshoz és egy tekerőgombként - vagy minek hívják magyarul - mondjuk fényerő szabályzáshoz. Aki nem ismerné, annak elárulom, hogy az egér görgője is egy ilyen eszköz.

A projekt része még egy fényerőszenzor és egy mozgásérzékelő.
A környezeti fényerőt egy 10kohm-os fotoellenállással mérem. Ez akármelyik elektronikai boltból beszerezhető.

A mozgásérzékelő pedig egy szokványos, Arduino-hoz való hc-sr501 PIR szenzor, mely a testünk által kibocsátott infravörös fényt érzékeli. Ez ideális egy helyiség monitorozására akár 5m-es körzetben. Van neki egy fenntartási ideje aktiválás után, hasonlóan a mozgásérzékelős reflektorokhoz, melyet egy potméterrel lehet finomhangolni.

Mindkét eszközzel voltak korábbi tapasztalataim, ismerem a működésüket így ezekre esett a választásom.
Szükség volt ezekhez egy ún. projekt dobozra, amit szintén be lehet szerezni különböző méretekben bármelyik elektronikai üzletben.
A doboz végén a kis szürke pont a fotóellenállás.

Ezt a két eszközt a konyha egy stratégiailag gondosan kiválasztott zugába szántam mivel szempont volt, hogy a mozgásérzékelő a helyiségbe belépőket lássa (fenti képen pl. a szobaajtó), ugyanakkor a két éjszakai életet élő haszonállatka nagyjából a szenzor látómezőjén kívül essenek - vagyis a padló takarásban legyen, amennyire lehet. Ha meg esetleg az egyik bátor felugrana a pultra, azt egy éjszakai irányfénnyel jutalmazza majd a vezérlő. Ugyanis az automatikus éjszakai irányfény még a másik fő funkciója, amivel kényelmesebbé tehető szüleim élete.

Szükségem volt továbbá vezetékre, melyből 0,75 mm^2 méretűre esett választásom, ez már jelentősebb feszültségesés nélkül áthúzható a konyhán.
Az érzékelőkhöz UTP kábelt vásároltam.
Aluprofilokra és kiegészítőikre, amiket LED-es szaküzletekben lehet megvásárolni.
Műanyag kábelcsatornára, amivel a vezetékeket vittem át esztétikusan egyik faltól a másikig, valamint a szekrények belsejében történő elvezetésre.

*Amennyiben 12V-os tápban gondolkozunk, úgy külön gondoskodni kell a mikrovezérlőről, mely ideálisan a stabilizált 5V-ot preferálja. Ez megoldható elegánsan egy USB töltővel (a GND-t közösítve), vagy egy DC-DC konverterrel a fő tápunkról. Esetleg a mikrovezérlő saját feszültség stabilizátorával, de ez nem mindig jó megoldás mivel a kínai klónokon a gyengébb minőségű alkatrészek kevésbé tolerálják a nagyobb feszültségeket. Olykor már 12V-on is hajlamosak tönkremenni és inkább 8-9V-ig működtethetők. Az eredeti Arduino Nano viszont 12V-ig magabiztosan táplálható, az Uno 16V-ig is.

Barkácsolás

Pár sorban összefoglalnám a lényegesebb dolgokat és az általam használt szerszámokat, amik jelentősen megkönnyítik a dolgunkat.

Tudni kell, hogy az alumínium profilokat két méteres szálakban árulják és a kívánt méretre kell szabni, melyre lehet lesz lehetőség az üzletben, de többnyire inkább nem. Ehhez szükségünk van minimum egy fémfűrészre, de jobban járunk egy sarokcsiszolóval és vágókoronggal. Egy munkaasztal és satu remek szolgálatot tehet, de némi segítséggel boldogulhatunk ezek nélkül is. A sorjás élek eldolgozására fémreszelőre lesz szükség, különben csontig hatoló vágásokat szerezhetünk szerelés közben (erre figyelni kell akkor is, ha méretre vágva vesszük). De egy kiálló sorjás él egyébként is veszélyes, így ez feltétlen szükséges lépés mielőtt valaki belenyúl vagy nekimegy elhelyezéstől függően.

Én facsavarokkal rögzítettem a síneket, amikhez először több helyen meg kellett fúrnom a sínt és kézzel süllyesztettem a furatokat, így a facsavarokkal közvetlenül tudtam rögzíteni a bútorlapra. Ez így barátságosabb megoldás, mint a szirsz*r műanyag pattintós konzolok (amik egyébként nem is olyan filléres tételek, megéri nyomtatni, aki teheti). Hátránya, hogy kell egy fúrószál készlet és fúrógép. A konzolok rögzítéséhez elvileg elég egy csavarhúzó is.

A vezetékeket próbáltam elrejteni, így a szalag végénél ejtettem egy U alakú bevágást a sín végébe (vágókorong vagy fémfűrész, esetleg reszelő), kellő szélességben a vezeték átmérőjéhez. Ezen bemetszések alatt a bútorlapot megfúrtam, előkészítve a helyet a vezetéknek.

A kábelcsatornát is kétméteres szálakban adják, de könnyebb méretre szabni. A szekrényre egyszerű feltenni, csak egy-két facsavar kérdése, viszont a falhoz szükségünk lesz tiplikre és fal fúróra.
Az útba eső polcok éleit meg fához való lapos reszelővel a legegyszerűbb bemetszeni.

A vezetékeket forrasztottam, így jó ha van egy szabályozható forrasztóállomás vagy forrasztópáka. A páka viszont általánosságban véve nem igazán alkalmas a finom munkákhoz (kivételek mindig vannak akik szeretik szívatni magukat), viszont a vezetékezéshez éppen ideális.
Forrasztóónból szerintem a legkönnyebben az ólmozottal lehet dolgozni, amiben van folyasztószer is.
A vezetékek lecsupaszításához a legjobb egy automatikus vezetékcsupaszoló; ha nincs kéznél én oldalcsípőfogót szoktam még használni.

A csupaszított végekhez zsugorcsövek kellenek - szintén elektronikai boltokból - ezt vagy hőlégfúvóval applikáljuk fel, vagy valami egyéb piromán módon.

Jól jöhet még egy ragasztópisztoly amivel szűk helyeken, sarkokban csak odabiggyesztettem egy-egy pöttyöt szakaszonként, belenyomtam a vezetéket és tart, mint a Taft. A projekt doboz is ragasztóval került fel a helyére. Meglepően szilárdan.

Az alsó szekrények alján és hátulján - már ahol odafértem - kábelrögzítő bilincseket használtam szöggel. Ehhez kalapácsra lesz szükség.

A projekt dobozhoz koronafúrót használtam a PIR szenzor méretéhez igazodva. Mellette egy furatba beragasztottam a fotoellenállást, a végére pedig vágtam egy lyukat az UTP kábelnek.

A táp méretezése és előkészítése

Itt szeretném hozzátenni, hogy a cikkben lehetnek hibák, pontatlanságok. Nincs elektronikai vagy IT végzettségem, magamat csupán műkedvelőnek tartom. Javaslom mindenkinek csak akkor végezzétek el ezeket a módosításokat, ha tudjátok mit csináltok! A hálózati feszültségű eszközök nem gyerekjátékok! Ha bármilyen nehézségbe ütköztök vagy elbizonytalanodtok, bízzátok inkább szakemberre! Az esetleges károkért felelősséget nem vállalok, az itt leírt beavatkozásokat mindenki saját felelősségére végezheti.

Először is a megfelelő teljesítmény kiszámolása...
Tudnunk kell milyen és mekkora szalagokra van szükségünk (ezt nyilván lemérjük a kívánt szakaszokra vonatkozóan és jó becslés a LED-ek számának arányosítása az 5 méteres szalaghoz), majd a LED dióda specifikációiból a maximum áramfelvétellel számolunk szorozva a LED-ek számával. Hozzá kell tennem, hogy a legtöbb ilyen dióda nyitófeszültsége 3V körül mozog, így egy 12V-os szalagnál három ilyen diódát kötnek sorba és egy előtét ellenállást a sor elejére. 24V-os szalagnál 6 db LED-et, míg 5V-os szalagnál minden lednek saját előtét ellenállása van.
Ez az áramfelvétel számolása miatt fontos, ugyanis soros kötésnél ugyanaz az áram folyik minden egyes fogyasztón: 12V-os szalagnál egy ilyen hármas csoport összes áramfelvétele lesz a 150mA (5630-as dióda esetén; 5050 az 60mA), 24V-os szalagnál egy hatos csoporté lesz ennyi, míg az 5V-os szalagnál minden egyes LED-re jut 150mA. Ebből következik, hogy egy 300 LED-es 12V-os szalagnál lesz egy hármas osztónk a képletben: 300 / 3 x 0,15A = 15A. Egy 5 méteres 5630-as szalaghoz így 15A szükséges 12 volton*. Igen, ez 180W teljesítményfelvétel lenne maximum fényerőn*. Az ilyen nagyteljesítményű szalagok fővilágításra valók, elegendő fényt ad a konyhában így előszeretettel használjuk a spot-ok helyett.

Az én esetemben így alakul a helyzet:

E szerint nekem durván 13A-t kellene biztosítanom*, amit az alábbi táp egy ágon képes leadni némi tartalékkal. Tökéletes.

ATX tápok átalakításáról már rengeteg írás született, túl hosszan nem foglalkoznék vele.
Először is a megfelelő páciens kiválasztásáról ejtenék pár szót. Figyelni kell a terhelhetőségre azon az ágon, amit használni szeretnénk. Esetemben az 5Vsb és a +12V1 vagy +12V2. Nálam mindkét ág 14 amperrel terhelhető és a készenléti 5Vsb is 2A-es ("DC Aux" a képen).

Mindenképp olyan tápot keressünk, ami egy billenőkapcsolóval áramtalanítható!

A burkolat szétszerelése után megkezdhetjük a vezetékek ritkítását, én levágtam az összes periféria tápkábelét és csak az alaplapi csatlakozót hagytam meg. Az alábbi ábra szerint azonosíthatjuk a megmaradt vezetékeket.

Nem lesz szükségünk, csak az egyik - vagy mindkét - 12V-os ágra (sárga) és mellé természetesen a GND-re (fekete). A 3,3V (narancs), 5V (piros), -12V (kék) eltávolítható. A 5Vsb (lila) lesz a mikrovezérlő áramforrása, a PS_ON (zöld) és Pwr_OK (szürke) a táp vezérléséhez kell. Minden mást vagy tőben elcsípünk kínosan ügyelve, hogy ellehetetlenítsünk bármilyen nem kívánatos kontaktust, vagy ami talán még jobb, ha némi ráhagyással csípjük le és zsugorcsövet húzunk a végekre. Így még később újra felhasználható, ha átalakul a projekt. A legletisztultabb eredményt viszont a nem kívánatos vezetékek kiforrasztásával érhetjük el, ha lehetséges.

Egyes tápok terhelés nélkül nem adnak stabil feszültséget, és így kellhet egy 10 Ohm-os 10 wattos ellenállás egy vagy több ágra. Ennek szükségességéről előzetesen tájékozódni kell, vagy letesztelni és kimérni, vagy megkérdezni egy szakértőt.

Ha ezekkel megvagyunk, akkor elméletileg nincs más dolgunk a táp bekapcsolásához, mint a PS_ON vezetéket földre húzni és voilá!

Elnézést a szegényes illusztrációért, a cikk ötlete az összeszerelés után született így nincsenek képeim a kibelezett tápról. A csavarhúzót, csípőfogót és a kezemben lévő forrasztópákát az olvasó fantáziájára bízom.

*Ezeknek a kínai LED-eknek - hacsak nem márkás terméket veszünk drágán - a valós fényereje és teljesítménye időnként sajnos elég nagy szórást mutatnak. A 150mA (avagy a 0,5W névleges teljesítmény) egy elméleti érték (kaphatók 0,25W-os változatok is), ami egyáltalán nem biztos, hogy ennyi is lesz a valóságban. Gyakorlatilag 20-200mA között bármi előfordulhat gyártástechnológiától, minőségtől és a megválasztott előtét ellenállástól függően (és természetesen az üzemi feszültségtől, hisz a legtöbb szalag autós környezetben is működőképes 14,4V-on). Simán előfordulhat, hogy két külön eladótól két azonos típusú, de mégis különböző szalagokat kapunk. Gyakran túl is becsülik a kínaiak a specifikációkat akár 50%-al is. Nem egy olyan példát találni a neten, ahol egy 5 méteres 5630-as szalag alig 18 wattot fogyasztott. Ez kb. 27 mA-re jön ki, ami elég messze van a papíron 0,5W-os, de még a 0,25W-os teljesítménytől is. Ez sajnos lutri. Vagy megveszi az ember szaküzletben a kipróbált és garantált minőséget drágán, ha biztosra akar menni.
Ami a tápot illeti, én mindig a maximális áramfelvétellel számolok. RGB esetén meg a maximális kétharmada az ökölszabály, ha nem használunk fehér színt. Ha van lehetőségünk lemérni a szalag áramfelvételét, nyilván az a legjobb megoldás.

Szalagok bekötése

Az analóg szalagok faék egyszerűek, az egyszínűeknek csupán két pólusa van: pozitív és negatív. Ha közvetlenül rákötjük egy azonos feszültségű áramforrásra máris világít. Egyedül a polaritásra kell csak figyelni, bár LED dióda lévén baj akkor sem történik, ha véletlenül fordítva próbálkozunk.
A méretre vágás az erre jelölt pontokon lehetséges a szalagon.

Érdemes lehet még szót ejteni a többszínű analóg szalagokról (akár 5 különböző színnel), melyeken az egyes színkomponensek külön csatornákon vezérelhetők, általában közös pozitív ággal - mivel az esetek többségében ún. low-side vezérlést alkalmazunk, ahol a Fet vagy tranzisztor a fogyasztó és a GND között van.

Esetemben csupán egy pozitív és egy negatív ágra kell csak figyelni. Mivel low-side vezérlést alkalmazunk az N csatornás mosfet-ek esetében, ezért az összes szalag pozitív ágát egyszerűen összekötöttem egy csillagpontba párhuzamosan a táp +12V-os vezetékével.
A pozitív ágak csillagpontba kötése.

A negatív ágakat két csoportra bontottam: alsó- és felső világítás. Majd ezeket a csoportokat szintén összekötöttem. Ezek kapcsolódnak a mosfet meghajtó egy-egy bemenetére a MOSFET felőli oldalon, ami majd a PWM vezérléstől függően kapcsolja GND-re a szalagokat, így zárva az áramkört az adott csatornán.

Illusztrációként szolgáljon az alábbi Móriczka rajz, melybe beleadtam Paint-es tudásom legjavát:

Mikrovezérlő bekötése

Ahelyett, hogy a Nano lábaira ráforrasztanánk, javaslom használjunk dupont kábeleket a mikrovezérlő tüskéihez! Ezeket félbevágva és lecsupaszítva ráforrasztottam a csatlakoztatni kívánt vezetékekre.

Vagy talán még jobb megoldás egy ilyen csavar terminál shield használata:

Ezzel megúszható a dupont kábeles önszívatás és elég csak a vezetékek lecsupaszított és ónozott végeit lecsavarozni. Valamint ezzel a vezérlőt magát egyszerűbb beszerelni és rögzíteni a végleges telepítéshez.

Úgy gondolom, mindenkinek jobb, ha nem nyúlok többet a Painthez.
A táp és a vezérlő egyszerű feladat, a fekete GND vezeték a Nano GND lábára csatlakozik, a lila +5Vsb pedig a Nano +5V lábára. A zöld PS_ON és a szürke PWR_OK bármelyik szabad GPIO lábakra mehetnek. Az érintő szenzor egyetlen vezetéke egy ADC bemenetre csatlakozik. A fotoellenállást (LDR) én két GPIO lábra tettem, ahol az egyik egy felhúzott bemenetként (INPUT_PULLUP módban), a másik pedig egy GND-re húzott kimenetként van beállítva (OUTPUT LOW módban). Itt ugye beépített felhúzó ellenállást használom külső helyett, amit általában nem javasolnak a pontatlan értéke miatt, de erre a felhasználásra tökéletes. A beépített felhúzó amúgy valójában nem is egy ellenállás, hanem egy FET, így ez nem egészen lineáris (egyébként az LDR sem az). Ez engem nem zavar, sőt állítólag ez még előnyös is ebben a felhasználásban mivel érzékenyebb így a szenzor a sötétben. Az egyetlen szempont, hogy megismételhető legyen a mérés és erre a beépített felhúzó is tökéletesen alkalmas. Megspórol nekem némi munkát.

A PIR szenzorhoz a második GND lábat és két szimpatikus GPIO portot használtam: egyik a jel bemenet, másik pedig a Vcc szerepét tölti be - OUTPUT HIGH módban. Emlékeim szerint a GPIO lábak maximum 50 mA-t képesek leadni vagy felvenni és a PIR szenzor ebbe bőven belefér.

Kellett nekem még két kábel a PWM jelekhez, ezek egyik végét egy-egy PWM képes GPIO portra, másik végét pedig a MOSFET meghajtóm jel bemenetére kötöttem.
És nagyjából itt el is ment a kedvem a kábel menedzsmenttől.
Éles használatban viszont itt zavar van az erőben... A vibráló LED-ek és a kapacitásra érzékeny PWM csatornák (elég hozzáérnem a MOSFET meghajtó bemeneteihez és látható hatással vagyok a jelre) bemeneti zajra engednek következtetni. Eredetileg 4 csatornát terveztem használni (felső- és alsó világítások oldalanként külön-külön), viszont egy csúnya gerjedés miatt az egyik meghajtó chip kiégett és nem vittem magammal tartalék alkatrészeket.

Sajnos korlátozott időm, tudásom és felszerelésem miatt nem tudtam rájönni mi okozza az interferenciát. Így raktam egy-egy 0,1uF bypass kondenzátort a bemenetekre, ami enyhítette valamelyest a problémát. Úgy gondolom koncepciójában követtem el a hibát, nem először tapasztalok pl. áthallást kettő vagy több 5V-os TTL/PWM jel között egymás melletti vezetékeken. WS2812b szalagokkal is tapasztaltam már, hogy szemetelnek a pixelek pl. lebegő bemenetnél és némi kapacitás közelében (pl. ujjaim közé veszem a vezeték másik végét), vagy két párhuzamosan futó vezetéknél az egyik szalag képe megjelent a másik éppen lebegő (vezérlőről leválasztott) vezetéken.
Ha ezt olvassa valaki hozzáértő szaki, aki tudja, konkrétan mi történik itt és volna rá megoldása, azért hálás lennék (koax kábelen kívül nyitott vagyok bármire: ferrit gyűrű, smd ferrit bead, RC szűrő, bypass kondi, ESD védelem, stb.).

Bevezető a kódba

A következő oldalakon megpróbálom bemutatni a programomat és igyekszem közérthetővé tenni, mit csinálnak a fontosabb kód részletek (így egy év múlva nekem is jól jön majd ez az írás, hehe). Máshol viszont lehet kissé felületes lesz, hisz nem célom senkit megtanítani programozni. Kérlek, ne számítsatok Hello World szintű magyarázatokra! :) A teljesen kezdőknek több bevezető cikk is született már a Logout hasábjain is, melyek közül néhányat linkeltem is a cikk előzményeiben.

Bevezetésként áttekinteném a kód függőségeit:
#include <EEPROM.h>
#include <avr/wdt.h> //watchdog
#include <ADCTouch.h>

Első kettő beépített könyvtár, egyik az EEPROM használatához kell, a másik meg a watchdoghoz (egy hardveres időzítő, ami a processzor megakadása/lefagyása esetén újraindítja azt). Viszont a harmadik az Arduino Playground-ról való mely az érintés érzékelőhöz kell. Ha te inkább egy elektromechanikus kapcsolót vagy gombot használnál, akkor ez utóbbira nem lesz szükség.

Ezután definiáltam a debug kiíratásokat a Serial monitoron:
//#define DEBUG
#ifdef DEBUG
#define debug(x) Serial.print(x)
#define debugln(x) Serial.println(x)
#else
#define debug(x)
#define debugln(x)
#endif

Ez nem csinál mást, mint beszúr vagy kivesz a programból egy rakás Serial.print() parancsot. Kicsit lerövidíti a monoton kódolást.

A GPIO lábakat általában a program elején szoktam definiálni:
#define PS_ON 3 //PSU ON signal
#define PS_GOOD 2 //PSU good signal
#define ldr A1 //input of photoresistor with internal pullup
#define ldr_gnd 7 //GND of photoresistor
#define touch A0 //touch sensor
#define pir_in 4 //Signal of PIR sensor
#define pir_pwr 8 //Vcc of PIR sensor

Ez az ajánlott módszer, így nem kell egyesével átírni az összes előfordulást, ha változik a bekötés.

Szoktam definiálni konstans értékeket, konfigurációkat, ha tetszik. Így ezek is könnyen kikereshetők a program elején, ha finomhangolni kell a beállításokat:
#define max_light 255 //max felső fényerő
#define max_b_light 180 //max alsó fényerő
#define night_light 100 //éjszakai irányfény fényereje
#define dark 850 //fotoellenállás határértéke félhomályban

Setup()

A setup-ot teljes egészében beszúrom, mert szerintem megérdemel pár szót.
void setup() {
//watchdog config
byte resetflag = MCUSR; // save flags for debug
MCUSR = 0; // reset various flags
WDTCSR |= 0b00011000; // see docs, set WDCE, WDE
WDTCSR = 0b01101001; // set WDIE, WDE, and appropriate delay
wdt_reset();

Kering a neten egy érdekes Arduino könyvtár, amivel az Atmel328p processzorokon lehet debugolni a Watchdog működésbe lépését. Annyi a trükkje, hogy a Watchdog nem csupán reseteli a processzort, hanem első lépésben megszakítja a programot, amire a processzor kiírja a stackbe (RAM-ba) a program számlálót és a függvény meg elmenti ezt az értéket az EEPROM-ba. Ezt kiolvasva tudjuk hol akadt meg a program. Aztán majd csak a második körben indítja újra a processzort. Ez a pár sor a watchdog felkonfigurálása.

Ezek a részletek egybefüggőek, de az első sorokat a fórum motor szükségszerűen behúzza formázáskor...
A GPIO lábak konfigurációi, semmi különös.
Serial.begin(9600);
pinMode(PS_ON, INPUT);
pinMode(PS_GOOD, INPUT);
pinMode(top_left, OUTPUT);
//pinMode(top_right, OUTPUT);
pinMode(bottom_left, OUTPUT);
//pinMode(bottom_right, OUTPUT);
pinMode(pir_in, INPUT);
pinMode(ldr, INPUT_PULLUP);
pinMode(ldr_gnd, OUTPUT);
pinMode(pir_pwr, OUTPUT);
digitalWrite(ldr_gnd, LOW);
digitalWrite(pir_pwr, HIGH);

touch_ref = ADCTouch.read(touch, 4000);
Itt állítom be az érintés érzékelő alapvonalát. Minden egyes beolvasáskor ehhez a referencia értékhez fogja hasonlítani a mérést.

//watchdog report
EEPROM.get(500, report);
Serial.print(F("Froze at: 0x"));
Serial.println(report * 2, HEX);
if (resetflag != 0) Serial.print(F("Reset: "));
switch (resetflag) {
case 1:
Serial.println(F("Power-on"));
break;
case 2:
Serial.println(F("External"));
break;
case 4:
Serial.println(F("Brown-out"));
break;
case 8:
Serial.println(F("Watchdog"));
break;
}

EEPROM.get(1, PIR);
}

Ez a watchdog debug riportja, ami minden egyes bootoláskor kiírja a Serial monitorra az utolsó reset paramétereit.
Alul pedig egy beállítás betöltése az EEPROM-ból. Ezt a PIR szenzor ki- és bekapcsolása esetén menti a vezérlő, de erről kicsit később.

loop() - nyomógomb

Ez kicsit hosszú lesz... Az első sorokat sajnos behúzza a formázás, de a cikk végén mellékelem majd a teljes kódot vágatlanul.
void loop() {
wdt_reset(); //reset watchdog timer

//touch sensing
touch_value = ADCTouch.read(touch, 100) - touch_ref; //érintő szenzor
if (touch_value > 25 && !button) { //megérintve
timer = millis(); //időszámláló nullázása, ez méri a gombnyomások lent- és fenntartásának idejét
button = true; //gomb megnyomva
ack = false; //nincs feldogozva még ez a gombnyomás
}
else if (touch_value <= 25) { //nem érintve
button = false; //gombnyomás nullázása
}

Elől a "kutya etetése": minden loop ciklusban meghívjuk ezt a függvényt, ami nullázza a Watchdog időzítőjét. Ha ezt nem tennénk, akkor az időzítő lejár és resetel a mikrovezérlő. Innen tudjuk, hogy megakadt a program, ha nem nullázzuk időben az időzítőt.
Ezután jön az érintés érzékelése. A referencia értékhez viszonyítva tudjuk megállapítani, hogy hozzáért-e valaki az érzékelőhöz.

Alább látható mi történik akkor, ha felengedjük a gombot kevesebb, mint 700 ms idő alatt. E határidő felett nyomva tartásként értelmezné a program, de ezt a funkciót kivettem. Eredetileg nyomva tartásra elkezdett "lélegezni" a szalag és azon a fényerőn állt meg, ahol elengedtem. Viszont a fokozatmentes szabályozásra végül nem volt igény.

if ((millis() - timer) <= 700 && !button && !ack) { //a gomb felengedése, annak feldolgozása előtt: nem járt még le a 700 ms és nincs lenyomva a gomb és "nincs feldolgozva"
if (mode != 1) { //ha nincs manuálisan bekapcsolva a világítás
mode = 1; //manuális bekapcsolás
dim = false; //eco mód alaphelyzetbe állítása (erről később)
}
else pressing++; //minden egyéb esetben gombnyomások száma +1
timer = millis(); //időzítő nullázása
ack = true; //feldolgozva. Ez azért kell, hogy a következő ciklusban ne hajtsa végre ugyanezt, amíg nem történik újabb gombnyomás
}

A mode változó egy állapotgéphez tartozik. Ha értéke 1, akkor világít minden szalag a kézi bekapcsolást követően.

Az alábbi meg kiértékeli a gombnyomásokat. (Azért írok folyton gombnyomást, mivel ha kicseréljük az ADCTouch-hoz fűződő sorokat egyszerű digitalRead()-re, akkor érintés érzékelő helyett használhatunk nyomógombot is. Persze a prell mentesítésről ilyenkor gondoskodni kell!)
Miután felengedtük a gombot, 500 ms türelmi idő után kiértékeljük. Vagyis gyors ismétlések kellenek a többszöri lenyomáshoz, mint az egéren a dupla kattintás.

if ((millis() - timer) > 500 && !button && ack) { //ismétlésre hagyott idő letelt és gomb felengedve és feldolgozva
if (pressing == 2) { //ha kétszer nyomták meg
light_tl = light_tl > 160 ? 150 : max_light; //nagy fényerőről kis fényerőre váltás és vice versa
light_bl = light_bl > 160 ? night_light : max_b_light; //ugyanez az alsó szalagokon
if (light_tl == max_light) dim = false; //itt egy bool változóval követjük a manuális váltásokat, ez az eco módhoz kell, hogy ne bíráljon felül
else dim = true;
}
else if (pressing == 1) mode = 0; //ha egyszer nyomták meg kikapcsol a világítás
else if (pressing >= 3 && PIR) { //háromszori (vagy többszöri) lenyomásra ki- vagy bekapcsoljuk a mozgásérzékelőt
PIR = false; //mozgásérzékelő kikapcsolása
EEPROM.put(1, PIR); //eeprom első címére elmenti a PIR változót
dimming();
}
else if (pressing >= 3 && !PIR) {
PIR = true; //mozgásérzékelő bekapcsolása
EEPROM.put(1, PIR);
dimming();
}
pressing = 0; //gombnyomások számának nullázása
}

Az EEPROM használata:
Ahogy azt láthattuk három különböző formában szerepelt eddig az EEPROM. Volt egy #include <EEPROM.h> a program elején, amivel elérhetővé tettük az EEPROM függvényeket. Majd a setup()-ban volt egy EEPROM.get(address, variable), feljebb meg két EEPROM.put(address, variable).
A Nano-n ha jól emlékszem 1 KB EEPROM áll a rendelkezésünkre amit beállítások, esetleg adatok tárolására használhatunk amik áramtalanítás után sem vesznek el. E két függvénnyel lényegében teljes mértékben ki is aknáztuk ezt a lehetőséget. Használatuk egyszerű, a get kiolvassa a változóba, míg a put beleírja az EEPROM-ba a függvénynek átadott változót a megadott címen.
A címzés egyszerű, de figyelni kell az adat méretére! Egy cím egy bájtot jelöl, vagyis az 1 KB tárterület 0-999 között címezhető kilobájtonként. A változó típusa határozza meg hány bájtot fogunk írni vagy olvasni. Ha 8 bites változónk van, akkor csak egy címen ír/olvas a függvény. Ha mondjuk 16 bites integer, akkor két bájton tárolja el, ahol minden további bájt a megcímzett terület után kerül lefoglalásra. Vagyis ha két bájtot adunk át a függvénynek: EEPROM.put(1, integer), akkor az 1. és 2. címekre fog írni.
Ilyenkor úgy kell megválasztani a címeket, hogy ne legyenek átfedések különben korrupt lesz az adat! A fenti példánál a következő adatot már csak a 3. címre tehetjük, mert az első kettőn lenne eltárolva a 16 bites integer.

loop() - szenzorok olvasása

Itt az LDR-t olvassuk be, majd egy itt beállított hiszterézissel megállapítjuk, hogy elég sötét van-e az éjszakai üzemmód aktiválásához.
//night
if (analogRead(ldr) > dark) night = true;
else if (analogRead(ldr) <= dark - 100) night = false;

Itt az állapotgépet váltjuk 2-es - éjszakai - üzemmódra, ha teljesülnek a feltételek.
A PIR szenzornak ugye van egy fenntartási ideje - amit a szenzoron lévő potméterrel lehet állítani -, így elég csak azt megállapítani van-e jel vagy nincs. Itt lehetne alkalmazni egy újabb millis() időzítést ha programból szeretnénk időzíteni. Én ezzel nem éltem: úgy láttam jónak, ha hardveresen szabályozható marad a timeout.

//PIR night
if (PIR && digitalRead(pir_in) && night && mode == 0) mode = 2; //ha be van kapcsolva a PIR és van mozgás és sötét van és ki van kapcsolva a világítás
else if (mode == 2) if (!PIR || !digitalRead(pir_in) || !night) mode = 0; //vagy ha aktív az éjszakai mód és (ki van kapcsolva a PIR vagy nincs mozgás vagy nincs sötét)

Beraktam még egy takarékos üzemmódot is arra az esetre, ha manuálisan felkapcsolják a világítást, de nem érzékelünk semmi mozgást a helyiségben egy adott ideig.

//PIR eco mode
if (PIR && mode == 1) { //ha be van kapcsolva a PIR és fel van kapcsolva a világítás
if (millis() > eco + 1500000 && light_tl == max_light) { //ha eltelt 25 perc mozgás nélkül és nagy a fényerő
light_tl = 150;
light_bl = night_light;
debugln(F("Entering power saving mode."));
} else if (digitalRead(pir_in) && light_tl != max_light && !dim) { //ha van mozgás és kicsi a fényerő, valamint nincs kézzel levéve
light_tl = max_light;
light_bl = max_b_light;
eco = millis(); //eco időzítő nullázása
debugln(F("Movement detected."));
} else if (digitalRead(pir_in)) eco = millis(); //ha van mozgás a helyiségben, akkor folyamatosan nullázza a türelmi időt
}

loop() - állapotgép

Ez volna a kódja a korábban látható három üzemmódnak, ezek között váltogat a program az interakciók szerint.
switch (mode) {
case 1: //felkapcsolás
psu(true); //táp bekapcsolása
if (light_tl == 0) { //világítás felkapcsolása
light_tl = max_light;
//light_tr = max_light;
}
if (light_bl == 0) {
light_bl = max_b_light;
//light_br = max_b_light;
}
updateLed(); //PWM vezérlők frissítése
break;
case 0: //lekapcsolás
psu(false); //táp kikapcsolása
light_tl = 0;
//light_tr = 0;
light_bl = 0;
//light_br = 0;
updateLed();
break;
case 2: //éjszakai üzemmód
psu(true); //táp bekapcsolása
light_bl = night_light;
//light_br = night_light;
updateLed();
break;
}

Ez a részlet pedig a fényerő fokozatos kivilágosodásáért és elsötétüléséért felel. Minden csatornának van egy saját változója a fényerőhöz, ahogy az feljebb már látható volt. Ezek használatával értékadáskor azonnal felvenné a szalag a beállított fényerőt, viszont nekem jobban tetszik, ha fokozatosan veszi fel a célértéket. Ehhez létrehoztam egy másodlagos változót minden csatornához, amivel úgymond követem a célt - az angol shadowing kifejezés után (tudom, nem a legkreatívabb változó nevek) - egy adott lépésközzel minden egyes loop() ciklusban. Így a shadow változók képviselik az aktuálisan megjelenített fényerőt, a light változók pedig a beállított fényerőt. Mivel az érintésérzékelés elég sok időt vesz igénybe és kicsit sokáig tartott az effektus, ezért kénytelen voltam meggyorsítani kicsit, amúgy elég lenne egy változó++ vagy változó-- is ciklusonként.

//set fading
if (shadow_tl < light_tl) shadow_tl += 5;
else if (shadow_tl > light_tl) shadow_tl -= 5;
//if (shadow_tr < light_tr) shadow_tr += 5;
//else if (shadow_tr > light_tr) shadow_tr -= 5;
if (shadow_bl < light_bl) shadow_bl += 5;
else if (shadow_bl > light_bl) shadow_bl -= 5;
//if (shadow_br < light_br) shadow_br += 5;
//else if (shadow_br > light_br) shadow_br -= 5;

Van még nekem továbbá egy függvényem a loop()-ban a Serial monitorból küldött parancsokhoz, de ezt csak a tesztelés erejéig használtam. Éles használatban nincs gépre dugva a mikrovezérlő*.
Ez meghívja a processSerialRead() függvényt, amit a következő oldalon tárgyalok.
while (Serial.available() > 0) processSerialRead((char)Serial.read());

*Ha használatban van az USB port a mikrovezérlőn, akkor ajánlott leválasztani a tápról, különben előfordulhatnak nem kívánt visszirányú áramok a számítógép USB portja felé!! Rosszabb esetben akár kárt is tehet a számítógépben.

Függvények

Ez a kód olvassa be a soros porton beérkező adatokat, amit továbbít egy másik függvénynek, ami a tényleges utasításokat végzi el. Itt a lényeg, hogy a Serial monitorban beállítunk egy "új sor" karaktert, ami kell az üzenet végének értelmezéséhez. Ha ezt a karaktert kapja a függvény, akkor tudjuk, hogy az a vége az üzenetnek és meg lehet hívni a parancsfeldolgozást. Minden más karaktert eltárol egy tömbben, amit majd értelmezünk feldolgozáskor.
void processSerialRead(const char inByte) {
switch (inByte) {
case '\n': // end of text
message[messageCount] = 0; // terminating null byte
// terminator reached! process input_line here ...
processMessage();
// reset buffer for next time
messageCount = 0;
break;
case '\r': // discard carriage return
break;
default:
if (messageCount < 13) message[messageCount++] = inByte;
break;
}
}

Ez pedig a függvény, ami értelmezi a kapott üzeneteket. Általában az összes programomban ezt a két függvényt használom mindenféle parancsfeldolgozásra. A CNC programozásban használatos G-kódokhoz hasonló szerkezetet szoktam használni, ahol egy parancsszó egy betűből és szóköz nélkül egy számból áll: pl. "C255". A függvény egyesével megvizsgálja a fogadott karaktereket, betűket keresve. Ha talál egy betűt, akkor megnézi milyen szám áll utána. Majd megkeresi a következő betűt és így tovább.
Ezzel a kóddal bármiféle delimiterrel vagy anélkül egyszerre fel lehet dolgozni több parancsot is: mondjuk elküldöm az "R255G0B0" karakter sorozatot (a végén a /new_line) és az algoritmus sorban végrehajtja mindet és az RGB ledem szép pirosan fog világítani (ez csak példa).
void processMessage() {
byte com;
for (byte i = 0; i <= messageCount; i++) {
if (isAlpha(message[i])) {
switch (message[i]) {
case 'c':
mode = atoi(& message[i + 1]);
break;
case 't':
light_tl = atoi(& message[i + 1]);
break;
case 'b':
light_bl = atoi(& message[i + 1]);
break;
case 'z':
light_tr = atoi(& message[i + 1]);
break;
case 'n':
light_br = atoi(& message[i + 1]);
break;
}
}
}
}

Ez itt a watchdog általi megszakításkor meghívott függvény. Kiolvassa a stack tetején lévő értéket (ami a program számláló lesz) és elmenti az EEPROM -ba.
ISR(WDT_vect, ISR_NAKED)
{
register uint8_t *upStack;
upStack = (uint8_t *)SP + 1;
report = (*upStack << 8) | *(++upStack);
eeprom_write_word((uint16_t *)500, report);
}

Ez a függvény vezérli az ATX tápot.
void psu(bool state) {
if (state) { //bekapcsolás
pinMode(PS_ON, OUTPUT);
digitalWrite(PS_ON, LOW); //lehúzza GND-re a zöld vezetéket
shutdown_time = millis(); //ez egy türelmi idő restart előtt
while (digitalRead(PS_GOOD) != HIGH) { //ez a ciklus ellenőrzi a tápot
debugln(F("Waiting for power..."));
//wdt_reset();
if (millis() > shutdown_time + 1000) { //ha 1s elteltével sincs táp, akkor lekapcsol, majd vissza
pinMode(PS_ON, INPUT);
delay(1000);
pinMode(PS_ON, OUTPUT);
digitalWrite(PS_ON, LOW);
shutdown_time = millis();
}
}
}
else { //kikapcsoláskor
if (digitalRead(PS_GOOD) == HIGH) {
if (shutdown == false) shutdown_time = millis(); //ez pedig egy türelmi idő lekapcsolás előtt
shutdown = true;
//debugln(F("PS off"));
}
}

if ((millis() - shutdown_time) > 30000 && shutdown) { //ha letelt a fél perc türelmi idő és még mindig a lekapcsolás van érvényben, akkor lekapcsol
pinMode(PS_ON, INPUT); //nagy impedanciás bemenetre kapcsolja a zöld vezetéket, ez olyan, mintha nyitott lenne az áramkör
}
else if (mode != 0) shutdown = false;
}

Ez egy vagy két felvillanást okoz a PIR szenzor ki- vagy bekapcsolásakor, amit 3+ gombnyomásra hív meg a program.
void dimming() {
digitalWrite(top_left, LOW);
delay(500);
analogWrite(top_left, light_tl);
if (PIR) {
delay(300);
digitalWrite(top_left, LOW);
delay(500);
analogWrite(top_left, light_tl);
}
}

Ez pedig a PWM vezérlők beállítása az aktuális fényerőre. Igazából a feltételeknek itt nincs nagy jelentősége, a megjelenítést még a digitális szalagoknál kötöttem feltételhez, hogy ne küldjön adatot a szalagra feleslegesen (és így megakasztva a programot arra az időre).
void updateLed() {
if (light_tl != shadow_tl) analogWrite(top_left, lightAdjustment(shadow_tl));
//if (light_tr != shadow_tr) analogWrite(top_right, lightAdjustment(shadow_tr));
if (light_bl != shadow_bl) analogWrite(bottom_left, lightAdjustment(shadow_bl));
//if (light_br != shadow_br) analogWrite(bottom_right, lightAdjustment(shadow_br));
}

Ez pedig az updateLed() függvényben látható, ez az, ami gamma szerint korrigálja a ledek fényerejét egy, a program végén lévő 256 elemű tömb (lookup table) szerint. Ennek oka, hogy a szemünk nem lineárisan érzékeli a beállított fényerőt, hanem nagyjából logaritmikusan. Ez a függvény gondoskodik arról, hogy mondjuk a beállított 128-as értéket (50%) valóban fél fényerőnek lássuk.
byte lightAdjustment(byte brightness) {
if (brightness != 255) {
return brightness = pgm_read_byte(&gamma[brightness]);
} else return brightness;
}

Végszó

Szüleimnek nagyon tetszik az eredmény, előszeretettel használják a mennyezeten lévő spot lámpák helyett. Nem is csoda, hiszem sokkal barátságosabb és egyenletesebb fényt ad ott, ahol kell: a kezünk ügyében. Szerintem nem vagyok vele egyedül, de engem kifejezetten irritál a régimódi mennyezeti világítás, főleg egy konyhában, ahol körbe a falak mentén vannak a konyhapultok: biz' Isten a fény útjában állok és árnyékot vetek pont oda, ahol látnom kéne mit csinálok.
A mozgásérzékelő meg telitalálat, mivel a konyhán keresztül vezet az út a mellékhelyiségig és így éjjel bóklászva a sötétben is biztonsággal lehet tájékozódni.

Egyetlen hiányossága van a projektnek csupán: a kábelmenedzsment. Beáldoztam a gőzelszívó feletti kis szekrényt, amit kineveztünk "projekt doboznak" és a továbbiakban nem tárolnak benne semmit. A legközelebbi karbantartáskor ezt a kuplerájt szeretném majd szakszerűen beszerelni egy normális villanyszereléshez való kötődobozba.
Valamint még ki kell találnom valamit az időnkénti enyhe vibrációra.
Jah meg nem ártana ESD védelmet tenni a kapcsolóra... Néha megcsípi az ember ujját a statikus kisülés.
(Ez bánthatja amúgy a mikrovezérlőt is.)

A teljes programkód

#include <EEPROM.h>
#include <avr/wdt.h> //watchdog
#include <ADCTouch.h>
//#define DEBUG
#ifdef DEBUG
#define debug(x) Serial.print(x)
#define debugln(x) Serial.println(x)
#else
#define debug(x)
#define debugln(x)
#endif

#define top_left 5
#define top_right 10
#define bottom_left 9
#define bottom_right 6
#define PS_ON 3 //PSU ON signal
#define PS_GOOD 2 //PSU good signal
#define ldr A1 //input of photoresistor with internal pullup
#define ldr_gnd 7 //GND of photoresistor
#define touch A0 //touch sensor
#define pir_in 4 //Signal of PIR sensor
#define pir_pwr 8 //Vcc of PIR sensor

#define max_light 255 //max top brightness
#define max_b_light 180 //max bottom brightness
#define night_light 100 //auto light at night
#define dark 850 //photoresistor value at dusk

extern const uint8_t gamma[];
char message[14];
byte messageCount = 0, mode = 0, light_tl, light_tr, light_bl, light_br, shadow_tl, shadow_tr, shadow_bl, shadow_br, stepping, pressing;
bool button, ack, fade, shutdown, PIR = 1, night, dim;
int16_t report, touch_ref, touch_value;
unsigned long timer, shutdown_time, debug, eco;

void processMessage() {
byte com;
debug(messageCount);
debug(F(": "));
debugln(message);
for (byte i = 0; i <= messageCount; i++) {
if (isAlpha(message[i])) {
switch (message[i]) {
case 'c':
mode = atoi(& message[i + 1]);
debug(F("Mode: "));
debugln(mode);
break;
case 't':
light_tl = atoi(& message[i + 1]);
debug(F("Top Left: "));
debugln(light_tl);
break;
case 'b':
light_bl = atoi(& message[i + 1]);
debug(F("Bottom Left: "));
debugln(light_bl);
break;
case 'z':
light_tr = atoi(& message[i + 1]);
debug(F("Top Right: "));
debugln(light_tr);
break;
case 'n':
light_br = atoi(& message[i + 1]);
debug(F("Bottom Right: "));
debugln(light_br);
break;
}
}
}
}

void processSerialRead(const char inByte) {
switch (inByte) {
case '\n': // end of text
message[messageCount] = 0; // terminating null byte
// terminator reached! process input_line here ...
processMessage();
// reset buffer for next time
messageCount = 0;
break;
case '\r': // discard carriage return
break;
default:
if (messageCount < 13) message[messageCount++] = inByte;
break;
}
}

byte lightAdjustment(byte brightness) {
if (brightness != 255) {
return brightness = pgm_read_byte(&gamma[brightness]);
} else return brightness;
}

void updateLed() {
if (light_tl != shadow_tl) analogWrite(top_left, lightAdjustment(shadow_tl)); //analogWrite(top_left, lightAdjustment(shadow_tl));
//if (light_tr != shadow_tr) analogWrite(top_right, lightAdjustment(shadow_tr));
if (light_bl != shadow_bl) analogWrite(bottom_left, lightAdjustment(shadow_bl));
//if (light_br != shadow_br) analogWrite(bottom_right, lightAdjustment(shadow_br));
}

void dimming() {
digitalWrite(top_left, LOW);
delay(500);
analogWrite(top_left, light_tl);
if (PIR) {
delay(300);
digitalWrite(top_left, LOW);
delay(500);
analogWrite(top_left, light_tl);
}
}

void psu(bool state) {
if (state) {
pinMode(PS_ON, OUTPUT);
digitalWrite(PS_ON, LOW);
shutdown_time = millis(); //for power supply failing
while (digitalRead(PS_GOOD) != HIGH) { //when it fails
debugln(F("Waiting for power..."));
//wdt_reset();
if (millis() > shutdown_time + 1000) {
pinMode(PS_ON, INPUT);
delay(1000);
pinMode(PS_ON, OUTPUT);
digitalWrite(PS_ON, LOW);
shutdown_time = millis();
}
}
}
else {
if (digitalRead(PS_GOOD) == HIGH) {
if (shutdown == false) shutdown_time = millis();
shutdown = true;
//debugln(F("PS off"));
}
}

if ((millis() - shutdown_time) > 30000 && shutdown) {
pinMode(PS_ON, INPUT);
}
else if (mode != 0) shutdown = false;
}

ISR(WDT_vect, ISR_NAKED)
{
register uint8_t *upStack;
upStack = (uint8_t *)SP + 1;
report = (*upStack << 8) | *(++upStack);
eeprom_write_word((uint16_t *)500, report);
}

void setup() {
//watchdog config
byte resetflag = MCUSR; // save flags for debug
MCUSR = 0; // reset various flags
WDTCSR |= 0b00011000; // see docs, set WDCE, WDE
WDTCSR = 0b01101001; // set WDIE, WDE, and appropriate delay
wdt_reset();

Serial.begin(9600);
pinMode(PS_ON, INPUT);
pinMode(PS_GOOD, INPUT);
pinMode(top_left, OUTPUT);
//pinMode(top_right, OUTPUT);
pinMode(bottom_left, OUTPUT);
//pinMode(bottom_right, OUTPUT);
pinMode(pir_in, INPUT);
pinMode(ldr, INPUT_PULLUP);
pinMode(ldr_gnd, OUTPUT);
pinMode(pir_pwr, OUTPUT);
digitalWrite(ldr_gnd, LOW);
digitalWrite(pir_pwr, HIGH);

touch_ref = ADCTouch.read(touch, 4000);
debugln(touch_ref);

//watchdog report
EEPROM.get(500, report);
Serial.print(F("Froze at: 0x"));
Serial.println(report * 2, HEX);
if (resetflag != 0) Serial.print(F("Reset: "));
switch (resetflag) {
case 1:
Serial.println(F("Power-on"));
break;
case 2:
Serial.println(F("External"));
break;
case 4:
Serial.println(F("Brown-out"));
break;
case 8:
Serial.println(F("Watchdog"));
break;
}

EEPROM.get(1, PIR);
}

void loop() {
wdt_reset(); //reset wathdog timer

//touch sensing
if (stepping == 0) { //nyomvatartás esetén hamis
touch_value = ADCTouch.read(touch, 100) - touch_ref; //érintő szenzor
//if (touch_value < 0) touch_ref += touch_value;
if (touch_value > 25 && !button) { //megérintve
//debugln(touch_value);
timer = millis(); //időszámláló nullázása
button = true; //gomb megnyomva
ack = false; //nincs feldogozva
}
else if (touch_value <= 25) { //nem érintve
//debug(touch_value);
//debugln("\t false");
button = false; //gombnyomás nullázása
}
}

if ((millis() - timer) <= 700 && !button && !ack) { //gomb felengedése, feldolgozása előtt
if (mode != 1) {
mode = 1; //bekapcsolás
dim = false; //power saving alaphelyzetbe állítása
}
else pressing++; //minden egyéb esetben gombnyomás +1
debug(pressing);
debugln(F(": "));
timer = millis();
ack = true; //feldolgozva
}

if ((millis() - timer) > 500 && !button && ack) { //gomb felengedve és feldolgozva
if (pressing == 2) { //ha kétszer nyomták meg
light_tl = light_tl > 160 ? 150 : max_light;
light_bl = light_bl > 160 ? night_light : max_b_light;
if (light_tl == max_light) dim = false;
else dim = true;
}
else if (pressing == 1) mode = 0; //ha egyszer nyomták meg
else if (pressing >= 3 && PIR) {
PIR = false; //mozgásérzékelő kikapcsolása
EEPROM.put(1, PIR);
debug(F("PIR sensor: "));
debugln(PIR);
dimming();
}
else if (pressing >= 3 && !PIR) {
PIR = true; //-||- bekapcsolása
EEPROM.put(1, PIR);
debug(F("PIR sensor: "));
debugln(PIR);
dimming();
}
//debug(pressing);
//debugln(F(": "));
pressing = 0; //számláló nullázása
}
/*if ((millis() - timer) > 700 && button && mode == 1) { //nyomva tartás
if (light_tl >= max_light) fade = false;
if (light_tl == 80) fade = true;
if (fade) {
light_tl++;
light_tr++;
}
else {
light_tl--;
light_tr--;
}
ack = true; //feldolgozva
if (stepping-- == 0) stepping = 5;
}*/

//set fading
if (shadow_tl < light_tl) shadow_tl += 5; //shadow is the actual brightness, light is the target
else if (shadow_tl > light_tl) shadow_tl -= 5;
//if (shadow_tr < light_tr) shadow_tr += 5;
//else if (shadow_tr > light_tr) shadow_tr -= 5;
if (shadow_bl < light_bl) shadow_bl += 5;
else if (shadow_bl > light_bl) shadow_bl -= 5;
//if (shadow_br < light_br) shadow_br += 5;
//else if (shadow_br > light_br) shadow_br -= 5;

//night
if (analogRead(ldr) > dark) night = true;
else if (analogRead(ldr) <= dark - 100) night = false;

//PIR night
if (PIR && digitalRead(pir_in) && night && mode == 0) mode = 2;
else if (mode == 2) if (!PIR || !digitalRead(pir_in) || !night) mode = 0;

//PIR eco mode
if (PIR && mode == 1) {
if (millis() > eco + 1500000 && light_tl == max_light) { //Power saving after the timeout.
light_tl = 150;
light_bl = night_light;
debugln(F("Entering power saving mode."));
} else if (digitalRead(pir_in) && light_tl != max_light && !dim) {
light_tl = max_light;
light_bl = max_b_light;
eco = millis();
debugln(F("Movement detected."));
} else if (digitalRead(pir_in)) eco = millis();
}

while (Serial.available() > 0) processSerialRead((char)Serial.read());
//debugln(mode);

switch (mode) {
case 1: //felkapcsolás
psu(true); //turn psu on
if (light_tl == 0) {
light_tl = max_light;
//light_tr = max_light;
}
if (light_bl == 0) {
light_bl = max_b_light; //max bottom light brightness
//light_br = max_b_light;
}
updateLed();
break;
case 0: //lekapcsolás
psu(false); //turn psu off
light_tl = 0;
//light_tr = 0;
light_bl = 0;
//light_br = 0;
updateLed();
break;
case 2: //éjszakai üzemmód
psu(true); //turn psu on
//debug(F("Night: "));
light_bl = night_light;
//light_br = night_light;
//debugln(night_light);
updateLed();
break;
}

#ifdef DEBUG
if (millis() - debug > 1000) {
debug(F("PIR: "));
debugln(digitalRead(pir_in));
debug(F("Fény: "));
debugln(analogRead(ldr));
debug(light_tl);
debug(F(", "));
debug(light_tr);
debug(F(", "));
debugln(light_bl);
debug(F(", "));
debugln(light_br);
debug(shadow_tl);
debug(F(", "));
debug(shadow_tr);
debug(F(", "));
debug(shadow_bl);
debug(F(", "));
debugln(shadow_br);
debugln();
debug = millis();
}
#endif
}

const uint8_t PROGMEM gamma[] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2,
2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5,
5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10,
10, 10, 11, 11, 11, 12, 12, 13, 13, 13, 14, 14, 15, 15, 16, 16,
17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 24, 24, 25,
25, 26, 27, 27, 28, 29, 29, 30, 31, 32, 32, 33, 34, 35, 35, 36,
37, 38, 39, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 50,
51, 52, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 66, 67, 68,
69, 70, 72, 73, 74, 75, 77, 78, 79, 81, 82, 83, 85, 86, 87, 89,
90, 92, 93, 95, 96, 98, 99, 101, 102, 104, 105, 107, 109, 110, 112, 114,
115, 117, 119, 120, 122, 124, 126, 127, 129, 131, 133, 135, 137, 138, 140, 142,
144, 146, 148, 150, 152, 154, 156, 158, 160, 162, 164, 167, 169, 171, 173, 175,
177, 180, 182, 184, 186, 189, 191, 193, 196, 198, 200, 203, 205, 208, 210, 213,
215, 218, 220, 223, 225, 228, 231, 233, 236, 239, 241, 244, 247, 249, 252, 255
};

Azóta történt

Előzmények