Az idő utazása
Tovább bonyolódik az óra fejlesztés, megérkeztek az időzónák
Úgy érzem magam, mint a Die Hard filmek magyar címeinek kitalálói érezhették magukat a negyedik rész környékén, amikor már nem nagyon tudtak több élet-szerű címmel előállni. Én is kezdek kifogyni az idővel kapcsolatos címekből, pedig ismét haladt előre egy kicsit a projekt.
Az előző részekben a CLion nem igazán szerette volna azt csinálni, amit mi szeretnénk és a PyCharm se volt a végletekig együttműködő ((Nincs még) Itt az idő), de legalább sikerült egy aranyos kis doboz-kezdeményt nyomtatni az óra köré (Dobozba zárt idő), amit most nem használunk.
Ma azzal folytatjuk, hogy visszalépünk egy kicsit és megpróbáljuk még egyszer megszelídíteni a CLion-t, azután pedig megtapasztalhatjuk azt is, hogy milyen borzalmakat rejtenek magukban az időzónák.
Fejlesztői környezet és a Pi
A hobbi projektek elsődleges célja (legalábbis nálam), hogy lehetőség szerint remekül szórakozzak (kinek mit jelent a szórakozás, ugye, de ezt majd később meglátjuk). Mivel foglalkozok eleget Python-nal a hétköznapok során, ezért szívesebben néztem volna más jellegű kihívások elé. Meg hát makacs is vagyok.
Az eredeti ötlet az volt, hogy fogok egy rendes Raspberry Pi-t, a Picoprobe (Debugprobe) helyett rákötöm a Pico-t a Raspberry Pi GPIO-jára, rádugok egy monitort/billentyűzetet/egeret és azon kezdek el fejleszteni. Az ötlet egy árnyalatnyit módosult, amikor nem találtam itthon megfelelő HDMI átalakítót a Rasberry Pi-hoz. Az új terv az lett, hogy megpróbálom meggyőzni a Windows gépen futó CLiont, hogy használja SSH-n keresztül a Raspberry Pi-t build környezetként.
Elméletileg a Pi a legalkalmasabb környezet a Pico fejlesztéshez. Néhány egyszerű parancs után mindennek működnie kell:
$ wget https://raw.githubusercontent.com/raspberrypi/pico-setup/master/pico_setup.sh
$ chmod +x pico_setup.sh
$ ./pico_setup.sh
Már csak egy kis várakozás és kész is... kivéve, ha véletlenül egy Raspberry Pi 5 egyre kevésbé boldog tulajdonosai vagyunk, mert arra a bejegyzés írásakor még nem volt felkészítve ez a script. Szerencsére megtaláltam ezt a pull request-et, ami a megoldás feléig eljuttatott, de szükségem volt még egy új OpenOCD interface konfigra is, amit már nem emlékszem honnan ollóztam össze:
/usr/local/share/openocd/scripts/interface/raspberrypi5.cfg
adapter driver linuxgpiod
adapter gpio swclk 24 -chip 4
adapter gpio swdio 23 -chip 4
Így már sikerült megfuttatni az egyik példaprogramot.
$ openocd -f interface/raspberrypi5.cfg -f target/rp2040.cfg -c "program gfx_pack_demo.elf verify reset exit"
Annak ellenére, hogy ezt sikerült megoldani, valamiért mégsem ebbe az irányba mentem, talán azt olvastam valahol, hogy ez a driver lassabb, mint a Picoprobe, úgyhogy visszadugdostam az eredeti felállást és USB-n kötöttem rá a Pi-ra a Pico-t, így az OpenOCD a már megszokott paranccsal működött:
$ openocd -f interface/cmsis-dap.cfg -f target/rp2040.cfg -c "program gfx_pack_demo.elf verify reset exit"
A munka oroszlánrésze
Már csak a CLiont kell rábírni az együttműködésre. Elméletileg megvan benne a támogatás, lássuk mire megyünk vele. Először is kell neki egy SSH konfig a Pi-hoz:
Aztán egy remote toolchain, ami ezt az SSH kapcsolatot használja:
Valószínűleg érdemes egy deployment-et is beállítani, hogy a fájl másolgatás jól működjön (ennek ellenére nem fog, de erre mindjárt visszatérünk):
És végül egy debug konfiguráció, hogy futtatni tudjuk a kódunkat.
Itt van egy pár érdekesség, amiről érdemes szót ejteni. A GDB-nél a remote host-on futót választottam, így a 'target remote' args csatlakozhat localhost-ra (a gdb és az openocd is a Pi-on fut). A "GDB Server args" környékén régen elég volt az első kettő paraméter (interface és target beállítás), az adapter speed valószínűleg opcionális. A program pedig... hát az elég érdekes.
Egy ideális világban az "Upload Executable" rész az mondjuk "If updated" lenne "Never" helyett, de ha az be van kapcsolva, akkor a Windows-os gép elérési útján próbálná megkeresni a fájlt, ami nincs ott, mert a Pi-on generálódott ELF fájlokat a CLion nem tölti le.
Ha kikapcsolom a ticket-ben említett beállítást és nyomok egy Tools > Resync with Remote Host opciót, akkor ugyan letölti az ELF fájlt, de valahogy még mindig nem kerül rá a Pico-ra. A kimenet alapján mintha elkezdené, de végül csak nem történik semmi. Vajon minek kell egyáltalán letöltenie (és aztán visszatöltenie a Pi-ra), ha minden ott van már a Pi-on? Rejtély.
Szóval inkább meghívom a program-ot kézzel, de így a reset miatt megszakad a debugger kapcsolata, ami a debug-olás esetén nem annyira kellemes. A vége az lett, hogy két konfigurációt csináltam, az egyik feltölteni tud (benne van a program rész), a másik meg csak debug-olni (nincs benne a program rész). Nem ideális a rendszer, de használható.
A konzolos kimenetet pedig a Pi-on futtatott minicom -b 115200 -o -D /dev/ttyACM0 paranccsal tudom elérni.
A pontos idő
A Python-os változatban elkészült már a pontos idő beszerzése, ezt kénytelenek vagyunk megtenni most a C SDK segítségével is. A valóságban ez még a Python-os változat előtt elkészült, de ettől most tekintsünk el.
Először is van egy kis konfigurációs része, ami a CMakeLists.txt-be kerül:
add_definitions(
-DSNTP_SERVER_DNS=1
-DSNTP_SERVER_ADDRESS="hu.pool.ntp.org"
-DSNTP_SET_SYSTEM_TIME=sntp_set_system_time
-DSNTP_STARTUP_DELAY=0
)
Amit itt érdemes megfigyelni, hogy hivatkozunk egy C-s függvényre (sntp_set_system_time), amit majd meg kell valósítanunk. Szükségünk lesz ugyanott pár könyvtárra is az SDK-ból:
target_link_libraries(
pico-clock
hardware_rtc
pico_cyw43_arch_lwip_threadsafe_background
pico_lwip_sntp
pico_stdlib
pico_time
)
A korábban említett függvény, ami kezeli az NTP szervertől kapott időt:
#include "hardware/rtc.h"
#include "lwip/apps/sntp.h"
#include "pico/cyw43_arch.h"
#include "pico/stdlib.h"
#include "pico/time.h"
#include "pico/util/datetime.h"
bool sntp_finished = false;
void sntp_set_system_time(uint32_t sec) {
datetime_t datetime;
time_to_datetime(sec, &datetime);
rtc_set_datetime(&datetime);
sntp_stop();
cyw43_arch_disable_sta_mode();
sntp_finished = true;
}
A sikeres idő szinkronizáció után kikapcsoljuk az SNTP modult és lejövünk a wifi-ről is. Már csak el kell indítanunk mindent:
int main() {
stdio_init_all();
cyw43_arch_init_with_country(CYW43_COUNTRY_HUNGARY);
rtc_init();
cyw43_arch_enable_sta_mode();
while (cyw43_arch_wifi_connect_timeout_ms("SSID", "secret", CYW43_AUTH_WPA2_AES_PSK, 30000)) {
printf("failed to connect.\n");
}
sntp_setoperatingmode(SNTP_OPMODE_POLL);
sntp_init();
while (!sntp_finished) {
printf("waiting for time.\n");
sleep_ms(100);
}
datetime_t now_utc;
char datetime_buf[256];
while (true) {
rtc_get_datetime(&now_utc);
datetime_to_str(&datetime_buf[0], sizeof(datetime_buf) / sizeof(char), &now_utc);
printf("%s\n", datetime_buf);
sleep_ms(1000);
}
}
Ha minden jól megy, akkor a Pico kap internet elérést, felveszi a kapcsolatot az NTP szerverrel és a konzolon másodpercenként elkezdenek jönni az üzenetek a pontos időről:
Tuesday 27 August 20:47:13 2024
Tuesday 27 August 20:47:14 2024
Tuesday 27 August 20:47:15 2024
...
Nagy bánatunkra UTC-ben, úgyhogy kell valami megoldást találnunk rá, hogy a helyi időt megkapjuk.
Elidőztem az időzónákkal
Nem sokat érne egy asztali óra, ha csak UTC-ben lenne képes megjeleníteni az időt. Az NTP szervertől egy unix timestamp-et kapunk, de az RTC (Real-time clock) modul már egy datetime_t struct-ot vár és azt is tudunk kinyerni belőle, így az idő megjelenítésénél ezzel vagyunk kénytelenek dolgozni.
Elméletileg az mktime tud csinálni strukturált dátum és idő adatból unix timestamp-et és a localtime pedig unix timestamp alapján tud csinálni megfelelő időzónában lévő dátum és idő struct-ot. A pico/util/datetime.h ad néhány segédfüggvényt is, a későbbiekben ezeket fogjuk használni, de belül itt is az mktime és a localtime fut. A datetime_to_time egy datetime_t-ből csinál unix timestamp-et, a time_to_datetime pedig unix timestamp-ből csinál helyi időt (egy datetime_t-ben).
De van egy kis csavar. Mindkét irány helyi idővel dolgozik és meg kell tudnunk mondani az egyiknek, hogy UTC-ből dolgozzon, a másiknak pedig, hogy milyen időzónában vagyunk éppen. De hogyan is lehetne ezt beállítani?
Az egyetlen ötletem, amin el tudtam indulni az volt, hogy van a tzdata csomag, ami időzóna adatokat tartalmaz, hátha ott valamiből ki tudok indulni, mondjuk egy időzóna fájl tartalmát valahogy feljuttatni a Pico-ra. Nem ez vezetett a megoldáshoz, de megtudtam, hogy az időzóna meghatározásához használt pl. Europe/Budapest megnevezések tényleges fájlok a /usr/share/zoneinfo könyvtárban.
Összefutottam a leírásokban a TZ nevezetű környezeti változóval is, aminek az értéke lehet mondjuk Europe/Budapest, amiről most már tudjuk, hogy igazából a /usr/share/zoneinfo/Europe/Budapest fájl, amivel nem vagyunk előrébb.
Némi utánajárás után egy leírás alapján az is kiderült, hogy a TZ nem csak elérési utakat tartalmazhat, hanem egy az egyben időzóna definíciót is. A Europe/Budapest definíciója valahogy így néz ki a fenti leírás és a Wikipédia ide vonatkozó adatai alapján:
TZ=CET-1CEST,M3.5.0/2,M10.5.0/3
Az alap időzónánk a CET, amiből 1-et kell levonnunk, hogy megkapjuk az UTC-t. A nyári időszámításunk a CEST. A nyári időszámítás kezdete március utolsó vasárnapja (M3.5.0), hajnal 1 óra UTC szerint, ami a 2 órát jelenti CET-ben (mivel ebben a formátumban a helyi idő szerint kell megadnunk az órát). A nyári időszámítás vége október utolsó vasárnapja (M10.5.0), szintén hajnal 1 óra UTC szerint, ami 3 óra a CEST szerint.
Ha jól értem a dolgokat, akkor a fájl annyival tudna többet, hogy nem csak az aktuális szabály van benne, hanem visszamenőleg megvannak a régi szabályok is (vagy előremenőleg, ha valami szabály módosulás lesz a jövőben). Ha futtatunk mondjuk egy zdump -v Europe/Budapest | less parancsot, akkor látszik, hogy például 1916-ban még április utolsó vasárnapján este 11 órakor volt a nyári időszámításra váltás.
Így most már az RTC modultól kapott időt a megfelelő időzónára tudjuk alakítani, csak a TZ értéke legyen megfelelő az datetime_to_time és a time_to_datetime hívása előtt.
datetime_t now_utc;
datetime_t now_local_time;
char datetime_buf[256];
while (true) {
rtc_get_datetime(&now_utc);
setenv("TZ", "UTC", 1);
time_t timestamp;
datetime_to_time(&now_utc, ×tamp);
setenv("TZ", "CET-1CEST,M3.5.0/2,M10.5.0/3", 1);
time_to_datetime(timestamp, &now_local_time);
datetime_to_str(&datetime_buf[0], sizeof(datetime_buf) / sizeof(char), &now_utc);
printf("utc time: %s\n", datetime_buf);
datetime_to_str(&datetime_buf[0], sizeof(datetime_buf) / sizeof(char), &now_local_time);
printf("local time: %s\n", datetime_buf);
sleep_ms(1000);
}
Minden jónak tűnik... az első futásnál.
utc time: Tuesday 27 August 21:02:03 2024
local time: Tuesday 27 August 23:02:03 2024
utc time: Tuesday 27 August 21:02:04 2024
local time: Tuesday 27 August 21:02:04 2024
utc time: Tuesday 27 August 21:02:05 2024
local time: Tuesday 27 August 21:02:05 2024
De csak az elsőnél, aztán gyorsan félremennek a dolgok. Mintha a két függvény valahogy egymás lábára lépne és elállítódnának a dolgok. Jó ideig nézegettem különböző implementációit a függvényeknek, de nem lettem tőle okosabb. Végül azzal a félmegoldással mentem, hogy a kernel kódjából hoztam el egy mktime implementációt, ami nem foglalkozik időzónákkal, így már működtek a dolgok.
Az igazi megvilágosodás akkor ért, amikor már ezt a bejegyzést írtam és újra elolvastam a TZ dokumentációját. Lehet, hogy csak az kell neki, hogy mennyi a különbség az UTC-hez képest még akkor is, ha UTC-t szeretnénk? Ez szerencsére megoldotta a problémát, a setenv("TZ", "UTC", 1)-et át kellett írni setenv("TZ", "UTC+0", 1)-ra. A sima UTC talán a /usr/share/zoneinfo/UTC fájlra vonatkozna, ami nekünk ugye itt nincs kéznél.
utc time: Tuesday 27 August 21:08:56 2024
local time: Tuesday 27 August 23:08:56 2024
utc time: Tuesday 27 August 21:08:57 2024
local time: Tuesday 27 August 23:08:57 2024
utc time: Tuesday 27 August 21:08:58 2024
local time: Tuesday 27 August 23:08:58 2024
...
Minden szépen és jól működött, de úgy 10 perc után az óra ismét az UTC-s időt mutatta. Ezen a ponton azért már erősen megkérdőjelezhető, hogy vajon tényleg ez-e nekem a szórakozás.
Belevetettem magam a debug-olásba, de egy ponton túl már csak assembly kódot mutatott a CLion, valahonnan össze kellene szedni a forrást. Némi kutakodás után megtaláltam, hogy a Newlib nevezetű C könyvtárból jön többek között az mktime és a localtime is.
A Pi-on egy sudo apt install newlib-source parancs kiadásával megkaptam a forrást a /usr/src/newlib/newlib-3.3.0.tar.xz fájlban, amit aztán már be tudtam adni a CLion-nak. A dolgok mélyén végül azt találtam, hogy amikor a localtime megpróbálja betölteni a TZ környezeti változó értékét, akkor nem talál ott semmit. Ez elirányított a setenv irányába, ami nyilvánvalóan meg volt hívva ugyan, de a hatezredik futás környékén mintha már nem tette volna a dolgát.
Hosszas kutakodás után ennél a résznél kötöttem ki:
if (!((*p_environ)[offset] = /* name + `=' + value */
_malloc_r (reent_ptr, (size_t) ((int) (C - name) + l_value + 2))))
{
ENV_UNLOCK;
return -1;
}
Le is ellenőriztem, a setenv hívás tényleg -1-et adott vissza, és bár nem vagyok egy C mágus, de a malloc-ból arra a következtetésre jutottam, hogy azért, mert elfogyott a memória. Készítettem egy rövid kis programot, hogy kipróbáljam:
int counter = 0;
int res;
while (true) {
res = setenv("TZ", "UTC+0", 1);
if (res == -1) {
printf("baj van: %d\n", counter);
break;
}
res = setenv("TZ", "CET-1CEST,M3.5.0/2,M10.5.0/3", 1);
if (res == -1) {
printf("baj van: %d\n", counter);
break;
}
++counter;
}
Ahogy azt sejteni lehetett, meg is kaptam a várt üzenetet:
baj van: 5217
Hiába nézegettem annyit az mktime-ot és a localtime-ot, a hiba végig a setenv-ben volt. Ha megnézzük a környező kódot, látszik, hogy ha az új környezeti változó értéke hosszabb, mint a régi érték volt, akkor új memóriaterületet foglal le neki, amit úgy tűnik nem szabadít fel senki se. A mi ciklusunk meg nagyjából másról se szól, mint hosszabb értékek beállításáról. Viszont ebből az következik, hogy ha ugyanolyan hosszúak, akkor nem lesz semmi baj, ugye? Feltöltöttem hát szóközökkel a rövidebbet.
int counter = 0;
int res;
while (true) {
res = setenv("TZ", "UTC+0 ", 1);
if (res == -1) {
printf("baj van: %d\n", counter);
break;
}
res = setenv("TZ", "CET-1CEST,M3.5.0/2,M10.5.0/3", 1);
if (res == -1) {
printf("baj van: %d\n", counter);
break;
}
++counter;
}
Tény és való, ez már nem írja ki azt, hogy "baj van", megoldódott a probléma, visszarakom a kódba az idő konvertálgatást is, hogy lássam egyben működni...
utc time: Tuesday 27 August 21:38:21 2024
local time: Tuesday 27 August 22:38:21 2024
utc time: Tuesday 27 August 21:38:22 2024
local time: Tuesday 27 August 22:38:22 2024
utc time: Tuesday 27 August 21:38:23 2024
local time: Tuesday 27 August 22:38:23 2024
...
Ez új. Valahogy elveszett egy óra. De nem estem kétségbe, ezen a ponton már elég rutint szedtem magamra, hogy sejtsem, a sok extra szóköz miatt nem tudja értelmezni rendesen az mktime a TZ értékét. Raktam egy extra vesszőt az UTC+0 után, hátha az meglágyítja a parser kő szívét...
setenv("TZ", "UTC+0, ", 1);
...és meglepő módon bevált:
utc time: Tuesday 27 August 21:39:22 2024
local time: Tuesday 27 August 23:39:22 2024
utc time: Tuesday 27 August 21:39:23 2024
local time: Tuesday 27 August 23:39:23 2024
utc time: Tuesday 27 August 21:39:24 2024
local time: Tuesday 27 August 23:39:24 2024
...
Sikerült. Több napnyi szenvedés móka és kacagás után végül elértük, hogy a Pico óra a megfelelő helyi időt mutatja. Remélhetőleg a téli és nyári időszámítás közötti váltást is jól fogja kezelni. Majd október utolsó vasárnapján kiderül. Addigra remélhetőleg már az LCD kijelzőre is fogunk valamit rajzolni.