Raspberry Pi i topla voda
Raspberry Pi računari su sve moćniji, pa ih mnogi koriste kao medija centre, pa čak i kao kompjutere opšte namene. Najzanimljivije su ipak samogradnje – ovoga puta govorimo o iskustvima sa Raspberry Pi-jem kao kontrolerom za bojler
Pre oko šest godina konstruisali smo kontroler za bojler. Bio je to zanimljiv projekat koji smo veoma iskomplikovali, radili smo na razvoju preko godinu dana (dobro… nije nam to bio jedini posao u životu) i najzad napravili “dvoglavu aždaju”. Arduino Mega je bio zadužen za očitavanje temperature vode, paljenje i gašenje releja i slične operacije, dok je Raspberry Pi komunicirao sa korisnikom preko Web interfejsa, kontrolisao Arduino i čuvao logove. Pi i Arduino su komunicirali preko I2C serijskog interfejsa, dok je Arduino koristio OneWire za očitavanje podataka sa DS18B20 sonde. O detaljima projekta možete da čitate u PC#268 ili na pc.pcpress.rs/tekst.php?id=15637. Ukratko rečeno, sjajno smo se zabavljali, naučili mnogo toga i dobili veoma koristan proizvod koji štedi struju i unapređuje kućni komfor.
Kontroler za bojler radi savršeno već pet godina. U funkcionalnom smislu nema šta da mu se zameri, ali je počeo da se nazire “zamor materijala”. Displej već neko vreme ne funkcioniše, što i nije naročito bitno (sve informacije se vide na Web strani bojlera), a pregled logova pokazuje sve veći broj grešaka pri komunikaciji Raspberry Pi – Arduino. U početku je ta komunikacija tekla bez kontrole, a onda sam primetio neke probleme i uveo jednostavni checksum kako bi se eliminisali povremeni neprijatni efekti u radu. Posle se ispostavilo da taj jednostavni checksum nije dovoljan, dešavalo se da zbir podataka u paketu daje dobar čeksum po modulu 256, ali su podaci pogrešno preneti. Onda sam uveo daleko snažniji Fletcher checksum koji ne propušta greške, ali vidim da ponekad Arduino mora po 5-6 puta da pokušava dok ispravno ne prenese paket podataka. Nije to toliko bitno, ništa se suštinski ne menja ako se bojler uključi ili isključi nekoliko sekundi kasnije nego što bi trebalo, ali se broj grešaka uvećava iz meseca u mesec. Trebalo bi otvoriti kontroler i proveriti sve veze u njemu, ali kada se samo setim koliko je u kutiji raznih žica… prosto nemam želje da se time bavim. I tako smo odlučili da napravimo novi kontroler, ovoga puta mnogo jednostavniji i… eh… savršeniji.
Raspberry Pi je dovoljan
Kompletnu funkcionalnost uređaja ovoga puta smo poverili Raspberry Pi-ju, koji je u međuvremenu napredovao za dve generacije, što za ovu primenu i nije toliko bitno. Dodali smo mu dva 220 V releja kojima može da uključuje i isključuje napajanje bojlera, a onda ga direktno povezali sa DS18B20 sondom, istom onom kakvu je u prethodnoj verziji projekta koristio Arduino. Stavili smo i znatno bolji OLED displej.
Što je najlepše, sve je to spakovano u kutiju koja ovoga puta nije morala da se oblikuje testericom: Boris ju je odštampao na 3D printeru, pa sve deluje mnogo lepše i profesionalnije. Tu su čak i nastavci namenjeni kačenju na zid.
Sa softverske strane nismo ništa menjali – pošto je trebalo ostaviti mogućnost da se koristi što veći deo starog koda, ostali smo pri jeziku C++, uz korišćenje g++ kompajlera. Vredi napomenuti da bi se kod nekog većeg projekta komfor pri radu mogao znatno povećati tako što se u Visual Studio Code instalira SSH ekstenzija i onda je VS Code direktno vezan na Raspberry Pi, pišete kod u njemu i samo kliknete da se source prenese na Pi i tamo kompajlira. Može da se koristi i C++ Cross Compiler tj. da se program prevodi na PC-ju a samo izvršna verzija prenosi na Pi. No za ovu priliku sam i dalje pisao programe u editoru na PC-ju, prenosio source na Pi preko FTP servera (znam, SFTP je sigurniji, ali ovaj kontroler je zatvoren u moju kućnu mrežu, a tamo ga valjda niko neće hakovati) i tamo ga prevodio.
Deljena memorija
Iako je ovoga puta u kontroleru samo jedan računar, koncepcija je ostala ista: treba da postoji proces koji će se vrteti u beskonačnoj petlji i uključivati bojler kad god je temperatura ispod željene za konkretno doba dana, a isključivati ga kada se željena temperatura postigne. Drugi proces se pokreće povremeno, kada treba promeniti neki od parametara, recimo zadati nove dnevne temperature, uključiti/isključiti boost (hitno potrebna topla voda), proveriti sadržaj sistemskog dnevnika i slično.
Proces koji je stalno aktivan, zamena za Arduino, nazvali smo looper dok je kontrolni proces (p)ostao control. Tokom razvoja looper se pokreće jednostavnim looper & (treba predvideti komandu kojom se ovaj proces zaustavlja, kada ga treba zameniti novom verzijom), a u finalnoj verziji se startuje iz fajla /etc/rc.local. Kontrolni proces se pokreće po potrebi, najčešće sa Web strane bojlera, iz PHP-a, konstrukcijom nalik na $output1 = shell_exec(’sudo /bojler/control parametri’); Dobro je sa sudo visudo definisati da ovaj program ima root privilegije kada ga pokreće Apache Web server, kako bi mogao da komunicira sa hardverom, komandom nalik na:
www-data ALL=(ALL) NOPASSWD: /bojler/control
Kako da control i looper komuniciraju? Postoji nekoliko načina (ne, serijski interfejs sa čeksumima nije jedan od njih) od kojih smo mi izabrali deljenu memoriju. Looper rezerviše blok memorije za željenu strukturu podataka, a onda control takođe pristupa toj memoriji. Svaki od procesa može da unosi promene koje onaj drugi trenutno vidi.
Realizacija je čudesno jednostavna i prikazana je na listingu 1. Deljena memorija se referencira preko neke datoteke bilo gde u fajl sistemu, u ovom slučaju /bojler/shmfile (ova datoteka čak i ne mora fizički da postoji, ali je dobro napraviti je), i proizvoljno izabranog broja, u našem slučaju 65. Mogle bi se, izborom drugih brojeva, formirati razne deljene zone za komunikaciju sa raznim programima, ali je nama bila dovoljna jedna. U drugom programu se povežemo na ovu zonu koristeći iste parametre, i svi podaci u memoriji su odmah dostupni, za čitanje i upis. Pojedinačne vrednosti iz zone se referenciraju konstrukcijama nalik na int wanted=shdata->wanttemp.
Komunikacija sa sondom
Rešenje za komunikaciju sa sondom koja meri temperaturu je prilično neobično. Sondu treba inicijalizovati sa par komandi a onda dođe vreme za očitavanje temperature. Treba ići u folder /sys/bus/w1/devices/28-449f2f126461 (broj zavisi od konkretne sonde). U tom folderu ima nekoliko pseudo-fajlova (nisu to fajlovi na disku odnosno SD kartici), između ostalih i w1_slave. U tom pseudo fajlu se nalazi ASCII tekst nalik na:
# cat w1_slave
a0 01 ff ff 7f ff ff ff cc : crc=cc YES
a0 01 ff ff 7f ff ff ff cc t=26452
To je 64-bitni ID u Macintosh stilu a temperatura upisana kao ASCII tekst. Dakle, treba parsirati tekstualni fajl i odatle zaključiti da je temperatura 26.452 stepena. Kod svakog očitavanja “oseti” se pauza od možda četvrtine sekunde koja je očito potrebna da bi pseudo-fajl bio kreiran.
Y2038 problem
Svi su već blaženo zaboravili problem 2000. godine (Y2K), kada se smatralo da će razni programi, koji su datume beležili u obliku DDMMYY, 1. januara 2000. godine početi da rade naopako, pa će elektrane prestati da funkcionišu, liftovi će se zaglaviti, banke “izgubiti” novac klijenata, a avioni padati na zemlju – ljudi vole katastrofične scenarije. Nije se desilo ništa, možda i zato što je u rešavanje ovog problema uloženo mnogo truda i novca.
Mnogo ozbiljniji problem nas očekuje 19. januara 2038. godine u 3:14:07 ujutru. U danima kada je, kasnih šezdesetih godina prošlog veka, nastajao Unix, njegovi tvorci su došli na zlosrećnu ideju da vreme broje u sekundama počev od neke arbitrarne tačke koju su nazvali “Unix epohom” – izabran je 1. januar 1970. godine. Da bi stvari bile još gore, odlučili su da to bude označeni broj, kako bi njegove negativne vrednosti izražavali vreme pre 1. januara 1970. godine. Broj je 32-bitni, dakle vrednosti su između -231 i +231-1. Kada stignemo do 2,147,483,648 sekunde počev od 1. januara 1970. godine, vrednost će postati negativan broj i biti neupotrebljiva. A ta dve milijarde i neka sekunda počev od 1. januara 1970. nastupa upravo 19. januara 2038. godine.
Kako su tvorci Unix-a mogli da budu tako kratkovidi? Pre svega, teško da su pomišljali da će operativni sistem koji prave “potrajati” 68 godina; pravili su mali operativni sistem, jedan od mnogih koji su tih dana nastajali i nestajali. Osim toga, tada je mogućnost da se ide u prošlost značila više nego pogled u daleku budućnost – da su se opredelili za neoznačeni ceo broj, epoha bi trajala do 7. februara 2106. godine, što danas izgleda “bezbedno daleko” (doći će dan kada baš i neće tako izgledati, i ne bismo se čudili ako bi Unix i tada postojao), ali zato oni ne bi mogli da naznače datum svog rođenja, iz četrdesetih ili pedesetih godina, u nekoj bazi podataka. Jedino pravo rešenje bilo bi korišćenje 64-bitnih celih brojeva, ali o tome se još nije ni razmišljalo.
U neformalnim razgovorima Unix gurui će reći da je Y2038 problem uglavnom rešen, da ne postoji na modernijim platformama i da je možda prisutan samo na prastarim sistemima, recimo onima koji kontrolišu ispaljivanje nuklearnih projektila. Zato smo razvijajući softver radili “po pravilu službe”, koristeći uobičajene Unix funkcije za rad sa vremenom (Raspbian operativni sistem koji koristi Raspberry Pi je varijanta Debian distribucije Linux-a) dok jednom, čisto provere radi, nismo ispitali koliki se brojevi koriste za računanje vremena. Dragička: sizeof(time_t) kaže: 4 bajta. Dakle, Y2038 problem je itekako prisutan na Raspberry Pi računarima.
Sistemska i druga rešenja
Da li treba nešto preduzeti? Godina 2038. i dalje deluje prilično daleko, ali ovaj uređaj je nešto što bi moglo da služi do tada. A pitanje je hoćemo li 2038. biti “u formi” da menjamo program ili kontroler. Osim toga, sve ovo radimo da bismo naučili neke nove stvari, zašto ostajati na C-u koji smo upoznali još u školskim klupama… Da vidimo kako se problem rešava.
ChatGPT (eh, a koga bi drugog čovek pozvao u pomoć) kaže da treba koristiti chrono biblioteku i da odgovarajuće promenljive treba deklarisati kao long long odnosno 64-bitne; tada će Linux epoha trajati do kraja univerzuma i još mnogo posle toga. Funkcija koja računa vreme (u minutima – bojleru su sekunde nebitne) od početka epohe data je kao listing 2. Isprobali smo je, radi, ali nije baš lako shvatiti šta se tu zapravo dešava. Više nam se svidelo da napišemo svoj kod, koji ćemo dobro razumeti a koji će takođe raditi “zauvek”.
Setili smo se funkcija za računanje rednog broja dana od početka Nove ere koji su uključeni u PPC ROM za HP41C programabilni kalkulator. To su lepo napisane i proverene funkcije koje će poslužiti i danas i u dalekoj budućnosti, a lako ih je prevesti na C. Listing 3 prikazuje rezultat, kome onda nije teško dodati sate i minute.
Finalna unapređenja
Pomenimo i lekciju koju smo naučili na teži način. Nije dovoljno što čuvate izvorni kod svojih programa na PC-ju; ako se nešto ozbiljno zabrlja, moraćete da krenete od nule i ponovo instalirate i podesite sve i svašta na Raspberry Pi-ju, što je spor i dosadan posao. Zato povremeno treba napraviti backup kompletne MicroSD kartice sa koje se Pi podiže. Jedan od načina da se to uradi je da se kartica izvadi, ubode u PC slot i sačuva programom Win32 Disk Imager (ako hoćete da sa PC-ja pristupate i konkretnim fajlovima na ovoj kartici, instalirajte Ext2Fsd-0.69.exe). Druga mogućnost je da na samom Pi-ju instalirate rpi-clone, pa onda ubacite drugu karticu (u USB adapteru) i napravite kopiju sistemske kartice bez potrebe da uopšte koristite PC.
Pri finalizaciji projekta naišli smo na razne sitne probleme koji su poticali od toga što se biblioteke menjaju, recimo procedura koja šalje e-mail sa dnevnim izveštajem o radu bojlera, koja je savršeno radila godinama, u novoj verziji nije uspevala da preda poruku Dreamhost mail serveru. Ispostavilo se da je u To polju poruke ranije moglo da se napiše “Ime <adresa.com>” a sad mora samo <adresa.com>. No najzad je sve došlo na svoje mesto. Treba još isprobati neku grafičku biblioteku za lepo prikazivanje dnevnih rezultata, unaprediti Web stranu bojlera tako da ne izgleda kao da je pravljena 1995. godine (tu ćemo se rado osloniti na ChatGPT) i još par sitnica, pa će kontroler biti spreman da zameni svog prethodnika, koji ide u kućni muzej…
Dejan Ristanović i Boris Stanojević
Objavljeno u PC#323, septembar 2024.
Listing 1: Korišćenje deljene memorije na Unix-u
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cstring>
#include <unistd.h>
struct SharedData {
int curtemp;
int wanttemp;
int relay;
int logdata[1500];
int control;
};
key_t key;
int shmid;
SharedData *shdata;
...
key = ftok("/bojler/shmfile", 65);
shmid = shmget(key, sizeof(SharedData), 0666|IPC_CREAT);
shdata = (SharedData*) shmat(shmid, (void*)0, 0);
if (shdata == (void*) -1) {
std::cerr << "Failed to attach to shared memory segment.\n";
exit(1);
}
shdata->curtemp=28;
shdata->wanttemp=40;
shdata->relay=0;
shdata->control=0;
...
// Povezivanje na deljenu memoriju iz drugog programa
key = ftok("/bojler/shmfile", 65); // Generate unique key
shmid = shmget(key, sizeof(SharedData), 0666);
if (shmid < 0) {
std::cerr << "Shared memory segment does not exist.\n";
return 1;
}
shdata = (SharedData*) shmat(shmid, (void*)0, 0);
Listing 2: Funkcije za rad sa vremenom imune na Y2038 problem
#include <chrono>
#include <ctime>
long long getCurrentTimeInMinutes() {
auto now = std::chrono::system_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::minutes>(now.time_since_epoch());
return duration.count();
}
string minutesToDateTimeString(long long minutes_from_epoch) {
// Convert minutes to seconds
auto seconds_from_epoch = std::chrono::duration_cast<std::chrono::seconds>
std::chrono::minutes(minutes_from_epoch));
// Convert to system_clock::time_point
std::chrono::system_clock::time_point tp(seconds_from_epoch);
// Convert to time_t
std::time_t tt = std::chrono::system_clock::to_time_t(tp);
// Convert to tm structure
std::tm *tm_ptr = std::localtime(&tt);
// Create a string stream to format the date and time
std::ostringstream oss;
oss << std::put_time(tm_ptr, "%d/%m/%Y-%H:%M");
return oss.str();
}
Listing 3: Računanje broja dana od početka Nove ere
unsigned long date2jdn (int d, int m, int g)
// Calendar Date to Day Number
// based on the HP-41C program by Roger Hill, PPC ROM, 1981
// C++ port by Dejan Ristanovic, 2024
{
double gp, izlaz;
gp=m-2.85;
gp=g+gp/12.0;
izlaz=int(367*gp)-int(gp)-0.75*int(gp)+d;
izlaz=int(izlaz)-0.75*int(gp/100);
izlaz=int(izlaz)+1721115;
return int(izlaz);
}
void jdn2date (unsigned long jdn, int& d, int& m, int& y)
// Day Number to Calendar Date
// based on the HP-41C program by Roger Hill, PPC ROM, 1981
// C++ port by Dejan Ristanovic, 2024
{
double n, c, np, yp, npp, mp;
n=jdn-1721119;
c=int((n-0.2)/36524.25);
np=n+c-int(c/4);
yp=int((np-0.2)/365.25);
npp=np-int(365.25*yp);
mp=int((npp-0.5)/30.6);
d=int(npp-30.6*mp+0.5);
if (mp<=9) { m=mp+3; y=yp; }
else { y=yp+1; m=mp-9; }
}