{"id":1017,"date":"2026-01-05T23:06:54","date_gmt":"2026-01-05T22:06:54","guid":{"rendered":"https:\/\/blog.adameczek.pl\/?p=1017"},"modified":"2026-01-11T21:17:42","modified_gmt":"2026-01-11T20:17:42","slug":"bezobslugowe-oswietlenie-choinkowe","status":"publish","type":"post","link":"https:\/\/blog.adameczek.pl\/index.php\/2026\/01\/05\/bezobslugowe-oswietlenie-choinkowe\/","title":{"rendered":"Bezobs\u0142ugowe o\u015bwietlenie choinkowe"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Dzi\u015b co\u015b z gatunku \u201eAdam S\u0142odowy\u201d. Zrobimy sterowanie lampkami choinkowymi tak, \u017ceby zapala\u0142y si\u0119 o zmierzchu i gas\u0142y w nocy, kiedy p\u00f3jdziemy spa\u0107 i nie b\u0119dziemy ich podziwia\u0107. W dzie\u0144 nie b\u0119d\u0105 \u015bwieci\u0142y, co pomo\u017ce ocali\u0107 planet\u0119 i podniesie respect factor.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Do wykonania zadania Adam S\u0142odowy wzi\u0105\u0142by deseczk\u0119, kilka gwo\u017adzi i m\u0142otek. Nasze zadanie tak\u017ce b\u0119dzie wymaga\u0107 paru fizycznych detali i zdolno\u015bci majsterkowania. Tym razem sam kod nie wystarczy. B\u0119dziemy potrzebowali: Raspberry Pi, kilku przewod\u00f3w. Konieczny te\u017c b\u0119dzie przeka\u017anik, konwerter napi\u0119cia i lampki choinkowe. Przyda si\u0119 te\u017c choinka, ale to dopiero na ko\u0144cu. Zamiast m\u0142otka we\u017a lutownic\u0119. Tymczasem zak\u0142adam, \u017ce je\u015bli masz malin\u0119, to pozosta\u0142e gad\u017cety te\u017c znajdziesz w szufladzie.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Hardware<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Potrzebny sprz\u0119t:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Rpi4\/5<\/li>\n\n\n\n<li>Konwerter napi\u0119cia <a href=\"https:\/\/www.ti.com\/product\/TXS0108E?utm_source=google&utm_medium=cpc&utm_campaign=asc-int-null-44700045336317926_prodfolderdynamic-cpc-pf-google-ww_en_int&utm_content=prodfolddynamic&ds_k=DYNAMIC+SEARCH+ADS&DCM=yes&gclsrc=aw.ds&gad_source=1&gad_campaignid=12514844049&gclid=CjwKCAiA3-3KBhBiEiwA2x7FdMgU8bChbFhYnzVa35C3oR0ShVs2D3FEf7EHXPil9da2YEUII-p1vRoC56AQAvD_BwE\">TXS0108E<\/a> <\/li>\n\n\n\n<li>Modu\u0142 przeka\u017anikowy<\/li>\n\n\n\n<li>Zegar czasu rzeczywistego RTC<\/li>\n\n\n\n<li>Lampki choinkowe i choinka<\/li>\n<\/ul>\n\n\n\n<figure class=\"wp-block-image size-large\"><img fetchpriority=\"high\" decoding=\"async\" width=\"843\" height=\"1024\" src=\"https:\/\/blog.adameczek.pl\/wp-content\/uploads\/2026\/01\/Blog-Raspberry-Pi-choinka-843x1024.gif\" alt=\"\" class=\"wp-image-1019\" srcset=\"https:\/\/blog.adameczek.pl\/wp-content\/uploads\/2026\/01\/Blog-Raspberry-Pi-choinka-843x1024.gif 843w, https:\/\/blog.adameczek.pl\/wp-content\/uploads\/2026\/01\/Blog-Raspberry-Pi-choinka-247x300.gif 247w, https:\/\/blog.adameczek.pl\/wp-content\/uploads\/2026\/01\/Blog-Raspberry-Pi-choinka-768x933.gif 768w, https:\/\/blog.adameczek.pl\/wp-content\/uploads\/2026\/01\/Blog-Raspberry-Pi-choinka-1264x1536.gif 1264w\" sizes=\"(max-width: 843px) 100vw, 843px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Elementy \u0142\u0105czymy jak na schemacie. Zegar czasu rzeczywistego pokazany na schemacie to element opcjonalny i nie b\u0119d\u0119 tu opisywa\u0142, jak go skonfigurowa\u0107 z malin\u0105. Opis\u00f3w jest du\u017co a procedura prosta. Raspbian wspiera RTC \u201ez pude\u0142ka\u201d. Zegar odpowiada za to, \u017ceby malina nie zgubi\u0142a czasu, je\u015bli w mejscu instalacji nie ma dost\u0119pu do sieci.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Konwerter napi\u0119cia zapewni wsp\u00f3\u0142prac\u0119 maliny (wej\u015bcia\/wyj\u015bcia pracuj\u0105 z napi\u0119ciem 3,3V, a element wykonawczy \u2013 przeka\u017anik jest zasilany napi\u0119ciem 5V. Teoretycznie stan wysoki wyj\u015bcia maliny 3,3V powinien by\u0107 powy\u017cej progu stanu wysokiego takiego modu\u0142u (pewnie ok. 2,4V) ale je\u015bli chcemy mie\u0107 pewno\u015b\u0107, \u017ce to zadzia\u0142a, to konwerter nie zaszkodzi. Jest jeszcze jeden pow\u00f3d stosowanie konwertera. W przypadku przypadkowego podania na wej\u015bcie napi\u0119cia 5V nawet na chwil\u0119, mamy prawie na pewno usma\u017cony obw\u00f3d wej\u015bciowy takiego wej\u015bcia. Nie wnikaj\u0105c teraz w elektronik\u0119, powiem tylko, \u017ce mocno zalecam stosowanie konwertera. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Pod\u0142\u0105czenie obci\u0105\u017cenia do modu\u0142u przeka\u017anikowego nie nastr\u0119czy Ci trudno\u015bci, ani nie wymaga uprawnie\u0144 SEP, ale zalecam ostro\u017cno\u015b\u0107 i stosowanie zasad bezpiecze\u0144stwa, jak przy ka\u017cdej pracy z napi\u0119ciem sieciowym.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Software<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Program w C# b\u0119dzie sterowany zdarzeniami. Sk\u0142ada si\u0119 z podstawowego modu\u0142u zrealizowanego jako BackgroundService, kt\u00f3ry nas\u0142uchuje, czy wyst\u0105pi\u0142o zdarzenie kt\u00f3re ma w\u0142\u0105czy\u0107 lub wy\u0142\u0105czy\u0107 przeka\u017anik. Skoro co\u015b nas\u0142uchuje, to nale\u017cy tak\u017ce utworzy\u0107 modu\u0142y generuj\u0105ce zdarzenia. Potrzebne b\u0119d\u0105 dwa zdarzenia: nadchodzi zmierzch (w\u0142\u0105cz lampki) oraz id\u0119 spa\u0107 o 23.00 (wy\u0142\u0105cz). Oba modu\u0142y generuj\u0105ce zdarzenia tak\u017ce b\u0119d\u0105 BackgroundService\u2019ami. Konieczny te\u017c b\u0119dzie modu\u0142 steruj\u0105cy fizycznymi wyj\u015bciami maliny. Wszystkie elementy om\u00f3wimy po kolei. Na koniec zostanie wygenerowany kod dla maliny z gotowej aplikacji utrorzymy serwis systemowy.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Na pocz\u0105tek utw\u00f3rz projekt konsolowy i dodaj pakiety nuget potrzebne do zbudowania hosta. Dodamy te\u017c logger. Po wykonaniu pierwszego polecenia zmie\u0144 folder na <code>\/choinka<\/code> i wykonaj kolejne polecenia.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>dotnet new console -n choinka\ndotnet add package System.Device.Gpio --version 4.0.1\ndotnet add package Microsoft.Extensions.Hosting\ndotnet add package Microsoft.Extensions.Hosting.Abstractions\ndotnet add package Serilog<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Nast\u0119pnie utw\u00f3rz host, kt\u00f3ry za chwil\u0119 b\u0119dziemy wype\u0142nia\u0107 tre\u015bci\u0105<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-csharp\" data-lang=\"C#\"><code>using choinka.Triggers.SolarTime;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\nusing Serilog;\n\ntry\n{\n    var hostBuilder= Host.CreateDefaultBuilder();\n    hostBuilder.Build().Run();\n}\ncatch (Exception ex)\n{\n    Log.Fatal(&quot;Fatar error: {Message}&quot;, ex.Message);\n}\nfinally\n{\n    Log.Information(&quot;Shutdown complete&quot;);\n    Log.CloseAndFlush();\n}<\/code><\/pre><\/div>\n\n\n\n<h3 class=\"wp-block-heading\">Zegar astronomiczny<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Lampki b\u0119dziemy zapala\u0107 o zachodzie s\u0142o\u0144ca. Nie ma sensu ustala\u0107 sztywnej godziny, bo w zimie moment zapadania ciemno\u015bci zmienia si\u0119 bardzo dynamicznie. Na przyklad. ju\u017c w 10 dni po przesileniu, s\u0142o\u0144ce zachodzi ok. 30 minut p\u00f3\u017aniej. S\u0142owem, nie ma co si\u0119 m\u0119czy\u0107 i nieustannie przetawia\u0107 czas w\u0142\u0105czenia lampek, Niech si\u0119 dzieje samo! Wykorzystamy bibliotek\u0119 <em>SolarCalculator<\/em>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>dotnet add package SolarCalculator<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Niech godzina zachodu odpowiada lokalizacji geograficznej. Solar Calculator obliczy zach\u00f3d s\u0142o\u0144ca w oparciu o podany czas i lokalizacj\u0119 (koordynaty geograficzne). Dodajmy miejca. Dla mnie wystarcz\u0105 Gda\u0144sk i Warszawa. Ty dodaj swoje:<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism off-numbers lang-csharp\" data-lang=\"C#\"><code>public class Places\n{\n    public IEnumerable<Coordinates> Coordinates { get; init; } = [\n        new Coordinates()\n        {\n            Name = &quot;Warsaw&quot;, Latitude = 52.2298, Longitude = 21.0117\n        },\n        new Coordinates()\n        {\n            Name = &quot;Gdansk&quot;, Latitude = 54.35, Longitude = 18.6667\n        },\n    ];\n}\n\npublic class Coordinates\n{\n    public string Name { get; init; } = null!;\n    public Angle Latitude { get; init; } = Angle.Empty;\n    public Angle Longitude { get; init; } = Angle.Empty;\n}<\/code><\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Potem dodamy <code>Places<\/code> do kontenera DI, bo za chwil\u0119 wstrzykniem je do serwisu. Ta akurat choinka b\u0119dzie w Warszawie. Wobec tego pobieram koordynaty Warszawy w konstruktorze. Ty pobierz swoje koordynaty, kt\u00f3re wcze\u015bniej zdefiniowa\u0142e\u015b. Kalkulator mo\u017ce policzy\u0107 czasy wyst\u0105pienie bardzo r\u00f3\u017cnych zjawisk astronomicznych. Ja potrzebuj\u0119 czas zachodu s\u0142o\u0144ca.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism off-numbers lang-csharp\" data-lang=\"C#\"><code>internal class SolarCalculator : ISolarCalculator\n{\n    private readonly Coordinates _warsaw;\n    private readonly Places _places;\n\n    public SolarCalculator(Places places)\n    {\n        _places = places;\n        _warsaw = _places.Coordinates.First(c => c.Name.Equals(\n            &quot;Warsaw&quot;, StringComparison.Ordinal));\n    }\n\n    public DateTime GetWarsawSunset(DateTimeOffset? date = null)\n    {\n        var time = new SolarTimes(date ?? DateTimeOffset.Now, _warsaw.Latitude, _warsaw.Longitude);\n        return time.Sunset;\n    }\n}\n\ninternal interface ISolarCalculator\n{\n    DateTime GetWarsawSunset(DateTimeOffset? date = null);\n}<\/code><\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Teraz przychodzi czas na nieco ciekawszy kawa\u0142ek kodu. B\u0119dzie to <em>BackgroundService<\/em> sprawdzaj\u0105cy ka\u017cdego dnia czy nadszed\u0142 zach\u00f3d s\u0142o\u0144ca. W odpowiednim momencie wywo\u0142a on <code>event<\/code>, kt\u00f3rego obs\u0142uga b\u0119dzie polega\u0142a na w\u0142\u0105czeniu lampek. Ten kawa\u0142ek kodu obja\u015bni\u0119 nieco szerzej ju\u017c nied\u0142ugo.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Solar Service<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Zawiera dwa istotne elementy: Jeden to `event EventHandler SunsetOccurred` czyli zdarzenie, na kt\u00f3re aplikacja zareaguje w\u0142\u0105czaj\u0105c lampki. Drugi element to logika obliczaj\u0105ca czas zachodu s\u0142o\u0144ca i \u015bledz\u0105ca czy ten czas ju\u017c nadszed\u0142. Jest ona zaszyta w metodzie <code>ExecuteAsync<\/code> serwisu dzia\u0142aj\u0105cego w tle <em>BackgroundService<\/em>. <code>ExecuteAsync<\/code> uruchamia <em>Task<\/em>, kt\u00f3ry na pocz\u0105tek bada, czy aplikacja nie zosta\u0142a uruchomiona ju\u017c po zachodzie s\u0142o\u0144ca. W takim przypadku wywo\u0142uje zdarzenie <code>SunsetOccurred<\/code>. Po sprawdzeniu przechodzi do niesko\u0144czonej p\u0119tli (linia 52), w kt\u00f3rej, podobnie jak wcze\u015bniej, oblicza moment zachodu (ale ju\u017c kolejnego dnia), oblicza te\u017c ile czasu zosta\u0142o do tego zachodu, po czym przechodzi w u\u015bpienie (linia 63). Kiedy czas minie event jest wywo\u0142ywany pod warunkiem, \u017ce proces nie jest zamykany (linia 46). Dok\u0142adnie to <em>CancellationToken<\/em> przekazywany do metody <code>ExecuteAsync<\/code> wchodzi w stan <em>canceled<\/em>, kiedy wywo\u0142ana jest metoda <code>StopAsync<\/code> <em>BackgroundService\u2019u<\/em>. A <code>StopAsync<\/code> jest wo\u0142ana, kiedy <em>host<\/em> otrzyma wezwanie do zatrzymania od systemu operacyjnego. Na jedno wychodzi, ale warto by\u0107 dok\u0142adnym \ud83d\ude09<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Zwr\u00f3\u0107 uwag\u0119 na to, \u017ce pobranie czasu nie jest zakodowane w tasku <code>RunEventLoopAsync<\/code>, funkcja obliczaj\u0105ca jest parametrem wywo\u0142ania (linia 16). W ten spos\u00f3b \u0142atwiej wprowadza\u0107 zmiany, albo dodawa\u0107 kolejne taski <em>eventLoop<\/em> (np. SunriseOccurred). B\u0119d\u0105 one mia\u0142y analogiczn\u0105 struktur\u0119. Nale\u017cy tylko w wywo\u0142aniu poda\u0107 inny delegat <em>Func<><\/em> i string <em>reason<\/em>. Zwr\u00f3\u0107 te\u017c uwag\u0119, \u017ce ka\u017cdy handler jest wywo\u0142ywany w oddzielnym bloku <em>try\/catch<\/em> (linia 128). To chroni aplikacj\u0119 przed b\u0142\u0119dami, jakie mog\u0142yby wyst\u0105pi\u0107 w metodzie obs\u0142ugi zdarzenia (<em>handlerze<\/em>). Normalnie, po b\u0142\u0119dzie, kolejne handlery zasubskrybowane do zdarzenia nie by\u0142yby wywo\u0142ane. Warto zapami\u0119ta\u0107 ten wzorzec.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Kod obs\u0142uguje wyj\u0105tki <em>OperationCanceledException<\/em>, kt\u00f3re b\u0119d\u0105 rzucane przy zamykaniu aplikacji. Osobi\u015bcie uwa\u017cam, \u017ce ten wyj\u0105tek zawsze powinien by\u0107 obs\u0142u\u017cony i kiedy zamykamy aplikacj\u0119, to nie powinny temu towarzyszy\u0107 wyj\u0105tki w logach. Ma\u0142a rzecz, a cieszy \ud83d\ude42<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-csharp\" data-lang=\"C#\"><code>internal class SolarNotifierService(\n    ILogger<SolarNotifierService> logger,\n    IServiceScopeFactory scopeFactory) : BackgroundService\n{\n    public event EventHandler SunsetOccurred;\n\n    protected override async Task ExecuteAsync(CancellationToken stoppingToken)\n    {\n        await RunEventLoopAsync(\n            &quot;Zach\u00f3d s\u0142o\u0144ca&quot;,\n            (calc, date) => calc.GetWarsawSunset(date),\n            reason => InvokeSunsetEvent(reason),\n            stoppingToken);\n    }\n\n    private async Task RunEventLoopAsync(\n        string eventName,\n        Func<ISolarCalculator, DateTimeOffset?, DateTime> getEventTime,\n        Action<string> invokeEvent,\n        CancellationToken token)\n    {\n        try\n        {\n            \/\/ Initial check + possible catch-up\n            using (var scope = scopeFactory.CreateScope())\n            {\n                var calculator = scope.ServiceProvider.GetRequiredService<ISolarCalculator>();\n                var todayTime = getEventTime(calculator, null);\n                var now = DateTimeOffset.Now;\n\n                if (now > todayTime)\n                {\n                    logger.LogWarning(&quot;Aplikacja uruchomiona po {EventName}. Wywo\u0142uj\u0119 event natychmiast (catch-up).&quot;, eventName);\n                    invokeEvent($&quot;Zaleg\u0142y {eventName} (start aplikacji po czasie)&quot;);\n                }\n                else\n                {\n                    logger.LogInformation(&quot;Czekam na dzisiejszy {EventName}: {EventTime}&quot;, eventName, todayTime);\n                    await WaitUntil(todayTime, token);\n\n                    if (!token.IsCancellationRequested)\n                        invokeEvent($&quot;Dzisiejszy {eventName}&quot;);\n                }\n            }\n\n            \/\/ Daily loop\n            while (!token.IsCancellationRequested)\n            {\n                try\n                {\n                    var tomorrow = DateTimeOffset.Now.AddDays(1);\n                    using var scope = scopeFactory.CreateScope();\n                    var calculator = scope.ServiceProvider.GetRequiredService<ISolarCalculator>();\n                    var nextTime = getEventTime(calculator, tomorrow);\n\n                    logger.LogInformation(&quot;Nast\u0119pny {EventName} zaplanowany na: {EventTime}&quot;, eventName, nextTime);\n\n                    await WaitUntil(nextTime, token);\n\n                    if (!token.IsCancellationRequested)\n                        invokeEvent($&quot;Planowy {eventName}&quot;);\n                }\n                catch (OperationCanceledException)\n                {\n                    \/\/ cancellation requested - exit loop\n                    break;\n                }\n                catch (Exception ex)\n                {\n                    logger.LogError(ex, &quot;Unexpected error in {EventName} loop&quot;, eventName);\n                    try\n                    {\n                        await Task.Delay(TimeSpan.FromSeconds(5), token);\n                    }\n                    catch (OperationCanceledException)\n                    {\n                        break;\n                    }\n                }\n            }\n        }\n        catch (OperationCanceledException)\n        {\n            \/\/ cancelled before starting - ignore\n        }\n        catch (Exception ex)\n        {\n            logger.LogError(ex, &quot;Fatal error while starting {EventName} loop&quot;, eventName);\n        }\n    }\n\n    private async Task WaitUntil(DateTime targetTime, CancellationToken token)\n    {\n        var delay = targetTime - DateTime.Now;\n        if (delay.TotalMilliseconds > 0)\n        {\n            try\n            {\n                await Task.Delay(delay, token);\n            }\n            catch (TaskCanceledException)\n            {\n                logger.LogWarning(&quot;Task cancelled&quot;);\n                \/\/ ignore\n            }\n        }\n    }\n\n    private void InvokeSunsetEvent(string reason)\n    {\n        logger.LogInformation(&quot;EVENT: ZACH\u00d3D S\u0141O\u0143CA ({reason})&quot;, reason);\n        SafeInvoke(SunsetOccurred, EventArgs.Empty, &quot;SunsetOccurred&quot;);\n    }\n\n    private void SafeInvoke(EventHandler? handler, EventArgs args, string eventName)\n    {\n        if (handler == null)\n            return;\n\n        var invocationList = handler.GetInvocationList();\n        foreach (var @delegate in invocationList)\n        {\n            try\n            {\n                if (@delegate is EventHandler eventHandler)\n                    eventHandler(this, args);\n            }\n            catch (Exception ex)\n            {\n                logger.LogError(ex, &quot;Exception thrown by handler for {EventName}; continuing with other handlers&quot;, eventName);\n            }\n        }\n    }\n}\n<\/code><\/pre><\/div>\n\n\n\n<h4 class=\"wp-block-heading\">Wsch\u00f3d s\u0142o\u0144ca<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">Wspomnia\u0142em. \u017ce <code>SolarNotifierService<\/code> mo\u017ce obs\u0142ugiwa\u0107 wi\u0119cej zdarze\u0144. Gdyby\u015bmy chcieli co\u015b zrobi\u0107 np. o \u015bwicie, to wystarczy zdefiniowa\u0107 kolejne zdarzenie i uruchomi\u0107 <em>task <\/em>realizuj\u0105cy logik\u0119 jego wywo\u0142ywania. Zamiast linii 10-18 mieliby\u015bmy co\u015b takiego:<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism off-numbers lang-csharp\" data-lang=\"C#\"><code>    public event EventHandler SunriseOccurred;\n    public event EventHandler SunsetOccurred;\n\n    protected override async Task ExecuteAsync(CancellationToken stoppingToken)\n    {\n        var sunriseTask = RunEventLoopAsync(\n            &quot;Wsch\u00f3d s\u0142o\u0144ca&quot;,\n            (calc, date) => calc.GetWarsawSunrise(date),\n            reason => InvokeSunriseEvent(reason),\n            stoppingToken);\n\n        var sunsetTask = RunEventLoopAsync(\n            &quot;Zach\u00f3d s\u0142o\u0144ca&quot;,\n            (calc, date) => calc.GetWarsawSunset(date),\n            reason => InvokeSunsetEvent(reason),\n            stoppingToken);\n\n        await Task.WhenAll(sunriseTask, sunsetTask);<\/code><\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Oczywi\u015bcie nale\u017ca\u0142oby doda\u0107 definicj\u0119 <code>sunriseTask<\/code> w spos\u00f3b, kt\u00f3ry opisa\u0142em wcze\u015bniej. Trzeba te\u017c rozszerzy\u0107 interfejs <code>ISolarCalculator <\/code>o medod\u0119 <code>GetWarsawSunrise <\/code>i j\u0105 zaimplementowa\u0107. Maj\u0105c jednak wzorzec, to ju\u017c pestka.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Alarm Clock Service<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u017beby wy\u0142\u0105czy\u0107 lampki potrzebujemy zdarzenia, \u017ce nadesz\u0142a 23:00. To jest zrealizowane analogicznie, jak w us\u0142udze obs\u0142uguj\u0105cej zach\u00f3d s\u0142o\u0144ca. <em>BackgroundService <\/em>zawiera publiczny <code>event EventHandler? Alarm2300Triggered<\/code>, do kt\u00f3rego za chwil\u0119 zasubskrybujemy procedur\u0119 wy\u0142\u0105czenia pr\u0105du. Serwis zawiera tak\u017ce <em>task<\/em> <code>alarm2300Task<\/code> uruchamiany przy jego starcie. Po szczeg\u00f3\u0142y odsy\u0142am do kodu na <a href=\"https:\/\/github.com\/madameczek\/choinka\">Githubie<\/a>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">GpioController<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Teraz wypada\u0142o by obs\u0142u\u017cy\u0107 zdarzenia i spowodowa\u0107 zapalanie lampek, Normalnie u\u017cyliby\u015bmy bezpo\u015brednio sterownika <code>GpioController<\/code> pochodz\u0105cego z  pakietu <em>System.Device.Gpio<\/em>. Ma on jednak pewn\u0105 cech\u0119, kt\u00f3r\u0105 chc\u0119 zmodyfikowa\u0107. Ot\u00f3\u017c tw\u00f3rcy biblioteki zak\u0142adaj\u0105, \u017ce aplikacja mo\u017ce uruchomi\u0107 <code>GpioController<\/code>, ustawi\u0107 jaki\u015b stan i zako\u0144czy\u0107 prac\u0119. Przy czym konfiguracja i stan wyj\u015bcia pozostaje taki jaki zosta\u0142 ustawiony. Nie jest przywracany stan \u201espoczynkowy\u201d. Jest to po\u017c\u0105dane przy np. zadaniach uruchamianych wg harmonogramu. Manipuluje si\u0119 wtedy stanem wyj\u015b\u0107 i ko\u0144czy prac\u0119. Jednak w tym przypadku chc\u0119, aby to dzia\u0142a\u0142o inaczej. Chodzi o to, by ko\u0144cz\u0105c prac\u0119, aplikacja zmieni\u0142a stan na zdefiniowany jako \u201espoczynkowy*. Ma to zapobiec sytuacji, polegaj\u0105cej na tym, \u017ce je\u015bli mi\u0119dzy zachodzem s\u0142o\u0144ca a 23:00 aplikacja lub system operacyjny zostan\u0105 zamkni\u0119te, to lampki pozostan\u0105 zapalone po 23:00.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Stan wyj\u015bcia GPIO<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">Musz\u0119 zdefiniowa\u0107 i utrzyma\u0107 stan wyj\u015bcia w aplikacji. B\u0119dzie on trzymany w klasie <code>PinState<\/code>. <\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism off-numbers lang-csharp\" data-lang=\"C#\"><code>public class PinState(GpioPin pin, PinValue onClose, PinValue? value)\n{\n    public static PinState CreateState(GpioPin pin, PinValue onCloseValue, PinValue? value) =>\n        new(pin, onCloseValue, value);\n\n    public GpioPin Pin { get; set; } = pin;\n    public PinValue OnCloseValue { get; set; } = onClose;\n    public PinValue? Value { get; set; } = value;\n}<\/code><\/pre><\/div>\n\n\n\n<h4 class=\"wp-block-heading\">GpioControllerWithPinRestore<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">Stan b\u0119dzie ustawiany w kontrolerze <code>GpioControllerWithPinRestore<\/code>, kt\u00f3ry wy\u0142\u0105czy pr\u0105d i zdeaktywuje konwerter napi\u0119cia przy zamykaniu aplikacji. W tym celu rozszerzam <code>GpioController<\/code>. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Konstruktor jest \u201eDI friendly\u201d i przyjmuje wstrzykni\u0119ty Ilogger. Ten konstruktor zostanie wybrany przez kontener DI przy instancjonowaniu obiektu. .NET zawsze wybiera konstruktor (o ile ma wyb\u00f3r) kt\u00f3ry pozwoli na wstrztykni\u0119cie jak najwi\u0119kszej liczby zarejestrowanych serwis\u00f3w. Opr\u00f3cz tego jest konstruktor bezparametrowy, kt\u00f3ry si\u0119 przyda w aplikacji bez kontenera DI.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Przed u\u017cyciem wyj\u015bcia, trzeba wywo\u0142a\u0107 <code>OpenPin()<\/code> (linia 14) i poda\u0107 stan wyjcia, jaki chcemy mie\u0107 po zamkni\u0119ciu aplikacji (<code>onCloseValue<\/code>). Je\u015bli tego nie zrobisz, to pr\u00f3ba wykonania <code>Write()<\/code> (linia 21) rzuci wyj\u0105tek z bazowego <code>GpioController<\/code>. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Kontener DI przy zamykaniu aplikacji wo\u0142a <code>Dispose()<\/code> (linia 34) dla wszystkich serwis\u00f3w, kt\u00f3re implenetuj\u0105 <code>IDisposable<\/code>. W tym momencie wyj\u015bcia zostan\u0105 ustawione w stan <code>onCloseValue<\/code>. Lampki zgasn\u0105. Sporo roboty, \u017ceby zgasi\u0107 \u015bwiat\u0142o \ud83d\ude09<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-plain\"><code>public class GpioControllerWithPinRestore : GpioController, IDisposable\n{\n    private readonly ConcurrentDictionary<int, PinState?> _pins = [];\n    private readonly ILogger<GpioControllerWithPinRestore>? _logger;\n\n    public GpioControllerWithPinRestore(ILogger<GpioControllerWithPinRestore> logger) : base()\n    {\n        _logger = logger;\n    }\n\n    public GpioControllerWithPinRestore() : base()\n    { }\n\n    public GpioPin OpenPin(int pinNumber, PinMode mode, PinValue initialValue, PinValue onCloseValue)\n    {\n        var pin = base.OpenPin(pinNumber, mode, initialValue);\n        _pins.TryAdd(pinNumber, PinState.CreateState(pin, onCloseValue, initialValue));\n        return pin;\n    }\n\n    public new void Write(int pinNumber, PinValue value)\n    {\n        var isPinExisting = _pins.TryGetValue(pinNumber, out var pinState);\n        if (isPinExisting)\n        {\n            pinState!.Value = value;\n            _pins[pinNumber] = pinState;\n        }\n        base.Write(pinNumber, value);\n    }\n\n    \/\/ other commands omitted for brevity\n\n    public new void Dispose()\n    {\n        Dispose(true);\n        GC.SuppressFinalize(this);\n    }\n\n    public new void Dispose(bool disposing)\n    {\n        foreach (var pin in _pins)\n        {\n            try\n            {\n                Write(pin.Key, pin.Value.OnCloseValue);\n            }\n            catch (Exception)\n            {\n                \/\/ ignore\n            }\n        }\n        base.Dispose(disposing);\n    }\n}<\/code><\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Prawie wszystkie sk\u0142adowe s\u0105 gotowe. Czas na ostatni, kt\u00f3ry je powi\u0105\u017ce w funkcjonaln\u0105 ca\u0142o\u015b\u0107.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">G\u0142\u00f3wny serwis GpioWorker<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Bardzo wa\u017cny, ale najprostszy z dotychczasowych serwis robi niewiele. Jego zadaniem jest zasubskrybowanie handler\u00f3w (tak, to te obrzydliwe metody <code>void<\/code> lub nawet gorzej <code>async void<\/code>) do zdarze\u0144. Na tym m\u00f3g\u0142by zako\u0144czy\u0107 prac\u0119. Musi jednak by\u0107 utrzymany przy \u017cyciu po to, \u017ceby od\u015bmiecacz <em>Garbage collector<\/em> nie usun\u0105\u0142 instancji a wraz z ni\u0105 <em>event handler\u00f3w<\/em> trzymaj\u0105cych referencje do metod obs\u0142ugi.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Dobra praktyka nakazuje usun\u0105\u0107 z listy subskrybcyjnej nieu\u017cywane handlery. To si\u0119 dzieje w metodzie <code>StopAsync()<\/code> wo\u0142anej przez host przy jego zamykaniu.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism off-numbers lang-csharp\" data-lang=\"C#\"><code>internal class GpioWorker(\n    ILogger<GpioWorker> logger,\n    GpioControllerWithPinRestore gpioController,\n    SolarNotifierService solarNotifier,\n    AlarmClockService alarmClockService) : BackgroundService\n{\n    const int _pinChoinka= 26;\n    const int _pinLevelConverter= 6;\n\n    protected override async Task ExecuteAsync(CancellationToken stoppingToken)\n    {\n        try\n        {\n            \/\/ subscribe to notifications and ensure level converter is enabled\n            solarNotifier.SunsetOccurred += Choinka_OnEventOccurred;\n            alarmClockService.Alarm2300Triggered += Choinka_OffEventOccurred;\n       \n            gpioController.OpenPin(_pinLevelConverter, PinMode.Output, PinValue.High, PinValue.Low);\n            if (gpioController.Read(_pinLevelConverter) == PinValue.Low)\n                gpioController.Write(_pinLevelConverter, PinValue.High);\n\n            await Task.Delay(Timeout.Infinite, stoppingToken);\n        }\n        catch (OperationCanceledException)\n        {\n            logger.LogInformation(&quot;GpioWorker cancellation requested&quot;);\n        }\n        catch (Exception ex)\n        {\n            logger.LogError(ex, &quot;Error initializing GpioWorker. In case of error 13, try to elevate privileges and run the application with &#39;sudo&#39;.&quot;);\n        }\n    }\n\n    public override Task StopAsync(CancellationToken cancellationToken)\n    {\n        logger.LogInformation(&quot;GpioWorker stopping, unsubscribing events and cleaning up pins&quot;);\n\n        try\n        {\n            solarNotifier.SunsetOccurred -= Choinka_OnEventOccurred;\n            alarmClockService.Alarm2300Triggered -= Choinka_OffEventOccurred;\n        }\n        catch (Exception ex)\n        {\n            logger.LogWarning(ex, &quot;Error while stopping GpioWorker&quot;);\n        }\n\n        return base.StopAsync(cancellationToken);\n    }\n\n    private void Choinka_OnEventOccurred(object? sender, EventArgs e)\n    {\n        if(!gpioController.IsPinOpen(_pinLevelConverter))\n            gpioController.OpenPin(_pinLevelConverter, PinMode.Output, PinValue.High, PinValue.Low);\n\n        if (gpioController.Read(_pinLevelConverter) == PinValue.Low)\n            gpioController.Write(_pinLevelConverter, PinValue.High);\n\n        if (!gpioController.IsPinOpen(_pinChoinka))\n            gpioController.OpenPin(_pinChoinka, PinMode.Output, PinValue.Low, PinValue.Low);\n\n        if (gpioController.Read(_pinChoinka) == PinValue.Low)\n            gpioController.Write(_pinChoinka, PinValue.High);\n    }\n\n    private void Choinka_OffEventOccurred(object? sender, EventArgs e)\n    {\n\n        if (!gpioController.IsPinOpen(_pinChoinka))\n            gpioController.OpenPin(_pinChoinka, PinMode.Output, PinValue.Low, PinValue.Low);\n        else\n            gpioController.Write(_pinChoinka, PinValue.Low);\n\n        logger.LogInformation(&quot;Stan wyj\u015bcia: {StanChoinka}&quot;, gpioController.Read(_pinChoinka));\n    }\n}<\/code><\/pre><\/div>\n\n\n\n<h3 class=\"wp-block-heading\">Rejestracja us\u0142ug<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">W <em>Program.cs<\/em> przed lini\u0105 9 (z pierwszego listingu) dodajemy sekcj\u0119 rejestruj\u0105c\u0105 serwisy. To, co tu mo\u017ce zwr\u00f3i\u0107 uwag\u0119, to spos\u00f3b rejestracji us\u0142ug t\u0142a <em>BackgroundService<\/em>. Nie rejestruj\u0119 ich najpopularniejszym sposobem <code>AddHostedService<T>()<\/code>, tylko rejestuj\u0119 singleton (kt\u00f3rym i tak s\u0105), ale u\u017cywam przeci\u0105\u017cenia bior\u0105cego funkci\u0119 <code>AddHostedService(Func<IServiceProvider, T> func)<\/code>. Tak mam zarejestowany singleton, kt\u00f3ry mog\u0119 wstrzykn\u0105\u0107 do <code>GpioWorker<\/code>, i jednocze\u015bnie uruchamiam serwis w tle. To jest jeden z alternatywnych sposob\u00f3w rejestracji. Przy okazji przestrzegam przed pomys\u0142em typu <code><s>AddHostedService(sp => new AlarmClockService())<\/s><\/code>. To mo\u017ce i zadzia\u0142a pozornie (po dodaniu konstruktora bezparametrowego, ale kontener DI nie b\u0119dzie zarz\u0105dza\u0142 cyklem \u017cycia takiego obiektu i mo\u017ce to mie\u0107 nie przewidziane konsekwencje.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism off-numbers lang-csharp\" data-lang=\"C#\"><code>    hostBuilder.ConfigureServices((ctx, services) =>\n    {\n        services.AddSingleton(sp => new Places());\n        \/\/ sun time services\n        services.AddSingleton(sp => new Places());\n        services.AddScoped<ISolarCalculator, SolarCalculator>();\n        services.AddSingleton<SolarNotifierService>();\n        services.AddHostedService(sp => sp.GetRequiredService<SolarNotifierService>());\n        \/\/ timed event service\n        services.AddSingleton<AlarmClockService>();\n        services.AddHostedService(sp => sp.GetRequiredService<AlarmClockService>());\n\n        services.AddSingleton<GpioControllerWithPinRestore>();\n        services.AddHostedService<GpioWorker>();\n    });<\/code><\/pre><\/div>\n\n\n\n<h3 class=\"wp-block-heading\">Wsparcie mened\u017cera systemu i us\u0142ug w Linuxie<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Raspian (system operacyjny RaspberryPi) pochodzi od Ubuntu i jest systemem linuxowym. Wygodnie b\u0119dzie wdro\u017cy\u0107 lampki jako usu\u0142ug\u0119 systemow\u0105. To daje wygod\u0119 polegaj\u0105c\u0105 m.in na tym, \u017ce us\u0142uga sama wstaje razem z systemem. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Na pocz\u0105tek dodaj opcjonalny pakiet Systemd. Poprawia on wsp\u00f3\u0142prac\u0119 z Mened\u017cerem systemu i us\u0142ug w systemie Linux.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>dotnet add package Microsoft.Extensions.Hosting.Systemd<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">I w <em>Program.cs<\/em> (po linii 8 pierwszego listingu) dodaj:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>hostBuilder.UseSystemd(); <\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Tu ko\u0144czy si\u0119 praca nad kodem C#. Pe\u0142na dzia\u0142aj\u0105ca wersja jest na moim Githubie <\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Publikacja projektu<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Teraz troch\u0119 pracy <em>DevOps<\/em>. Je\u015bli chcesz zbudowa\u0107 projekt i uruchomi\u0107 na malinie, to czytaj dalej. Jak ju\u017c masz ca\u0142y kod i buduje si\u0119 on lokalnie, to czas wys\u0142a\u0107 go do maliny. Na pocz\u0105tek opublikuj lokalnie aplikacj\u0119 ustawiaj\u0105c Linux jako docelowy OS. Zbudujemy aplikacj\u0119, kt\u00f3ra nie b\u0119dzia potrzebowa\u0142a obecno\u015bci .net na malinie (\u2013self-contained true) i pakuje wszysko do jednego pliku (-p:PublishSingleFile=true). Rasbian ma ju\u017c od dawna oficjaln\u0105 wersj\u0119 64-bitow\u0105. Je\u015bli masz wersj\u0119 32-bitow\u0105, to wybierz odpowiedni\u0105 komend\u0119 (prze\u0142\u0105cznik -r linux-arm64 lub -r linux-arm).<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Raspberry Pi 64-bit\ndotnet publish .\/choinka.csproj -c Release -r linux-arm64 -p:PublishSingleFile=true --self-contained true -o .\/bin\/Release\/net8.0\/publish\/linux-arm64\/\n# Raspberry Pi 32-bit\ndotnet publish .\/choinka.csproj -c Release -r linux-arm -p:PublishSingleFile=true --self-contained true -o .\/bin\/Release\/net8.0\/publish\/linux-arm\/<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Prze\u015blij kod na malin\u0119 i ustaw w\u0142a\u015bciciela plik\u00f3w oraz uprawnienia do wykonywania polece\u0144 (execute). Zmodyfikuj polecenia, aby odpowiada\u0142y nazwie u\u017cytkownika, jakiej u\u017cywasz. W przyk\u0142adzie u\u017cytkownikiem jest <em>pi<\/em>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>scp -r .\/bin\/Release\/net8.0\/publish\/linux-arm64\/* pi@raspberrypi:.\/choinka\nssh pi@raspberrypi'sudo chown -R pi:pi .\/choinka'\nssh pi@raspberrypi'sudo chmod +x .\/choinka\/choinka'<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Us\u0142uga systemu Linux<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Czas zarejestrowa\u0107 aplikacj\u0119, jako us\u0142ug\u0119 w Linuxie. Utw\u00f3rz plik <em>\/etc\/systemd\/system\/choinka.service<\/em>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>[Unit]\nDescription=choinka background service\nAfter=network.target\n\n[Service]\nType=simple\nUser=pi\nGroup=pi\nWorkingDirectory=\/home\/pi\/choinka\nExecStart=\/home\/pi\/choinka\/choinka\nRestart=always\nRestartSec=10\nEnvironment=ASPNETCORE_ENVIRONMENT=Production\nEnvironment=TZ=Europe\/Warsaw\n\n[Install]\nWantedBy=multi-user.target<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Ju\u017c prawie koniec. Wystarczy prze\u0142adowa\u0107 demona mened\u017cera i uruchomi\u0107 us\u0142ug\u0119<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl daemon-reload\nsudo systemctl enable choinka.service\nsudo systemctl start choinka.service\n\n# tak mo\u017cesz podejrze\u0107 logi\nsudo journalctl -u choinka.service -f\n# a tak status us\u0142ugi\nsudo systemctl status choinka.service<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">To ju\u017c definitywny koniec wpisu. Choinka \u015bwieci po zachodzie s\u0142o\u0144ca a ga\u015bnie o 23:00. Przy okazji pokaza\u0142em, jak dzia\u0142a aplikacja sterowana zdarzeniami i jak uruchomi\u0107 us\u0142ug\u0119, kr\u00f3ra przetwa restrart systemu w Linuxie. Mi\u0142ego kodzenia \ud83d\ude42<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Repozytorium na <a href=\"https:\/\/github.com\/madameczek\/choinka\">Githubie<\/a>:<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Dzi\u015b co\u015b z gatunku \u201eAdam S\u0142odowy\u201d. Zrobimy sterowanie lampkami choinkowymi tak, \u017ceby zapala\u0142y si\u0119 o zmierzchu i gas\u0142y w nocy, kiedy p\u00f3jdziemy spa\u0107 i nie b\u0119dziemy ich podziwia\u0107. W dzie\u0144 nie b\u0119d\u0105 \u015bwieci\u0142y, co pomo\u017ce ocali\u0107 planet\u0119 i podniesie respect factor. Do wykonania zadania Adam S\u0142odowy wzi\u0105\u0142by deseczk\u0119, kilka gwo\u017adzi i m\u0142otek. Nasze zadanie tak\u017ce [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":822,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"ocean_post_layout":"","ocean_both_sidebars_style":"","ocean_both_sidebars_content_width":0,"ocean_both_sidebars_sidebars_width":0,"ocean_sidebar":"","ocean_second_sidebar":"","ocean_disable_margins":"enable","ocean_add_body_class":"","ocean_shortcode_before_top_bar":"","ocean_shortcode_after_top_bar":"","ocean_shortcode_before_header":"","ocean_shortcode_after_header":"","ocean_has_shortcode":"","ocean_shortcode_after_title":"","ocean_shortcode_before_footer_widgets":"","ocean_shortcode_after_footer_widgets":"","ocean_shortcode_before_footer_bottom":"","ocean_shortcode_after_footer_bottom":"","ocean_display_top_bar":"default","ocean_display_header":"default","ocean_header_style":"","ocean_center_header_left_menu":"","ocean_custom_header_template":"","ocean_custom_logo":0,"ocean_custom_retina_logo":0,"ocean_custom_logo_max_width":0,"ocean_custom_logo_tablet_max_width":0,"ocean_custom_logo_mobile_max_width":0,"ocean_custom_logo_max_height":0,"ocean_custom_logo_tablet_max_height":0,"ocean_custom_logo_mobile_max_height":0,"ocean_header_custom_menu":"","ocean_menu_typo_font_family":"","ocean_menu_typo_font_subset":"","ocean_menu_typo_font_size":0,"ocean_menu_typo_font_size_tablet":0,"ocean_menu_typo_font_size_mobile":0,"ocean_menu_typo_font_size_unit":"px","ocean_menu_typo_font_weight":"","ocean_menu_typo_font_weight_tablet":"","ocean_menu_typo_font_weight_mobile":"","ocean_menu_typo_transform":"","ocean_menu_typo_transform_tablet":"","ocean_menu_typo_transform_mobile":"","ocean_menu_typo_line_height":0,"ocean_menu_typo_line_height_tablet":0,"ocean_menu_typo_line_height_mobile":0,"ocean_menu_typo_line_height_unit":"","ocean_menu_typo_spacing":0,"ocean_menu_typo_spacing_tablet":0,"ocean_menu_typo_spacing_mobile":0,"ocean_menu_typo_spacing_unit":"","ocean_menu_link_color":"","ocean_menu_link_color_hover":"","ocean_menu_link_color_active":"","ocean_menu_link_background":"","ocean_menu_link_hover_background":"","ocean_menu_link_active_background":"","ocean_menu_social_links_bg":"","ocean_menu_social_hover_links_bg":"","ocean_menu_social_links_color":"","ocean_menu_social_hover_links_color":"","ocean_disable_title":"default","ocean_disable_heading":"default","ocean_post_title":"","ocean_post_subheading":"","ocean_post_title_style":"","ocean_post_title_background_color":"","ocean_post_title_background":0,"ocean_post_title_bg_image_position":"","ocean_post_title_bg_image_attachment":"","ocean_post_title_bg_image_repeat":"","ocean_post_title_bg_image_size":"","ocean_post_title_height":0,"ocean_post_title_bg_overlay":0.5,"ocean_post_title_bg_overlay_color":"","ocean_disable_breadcrumbs":"default","ocean_breadcrumbs_color":"","ocean_breadcrumbs_separator_color":"","ocean_breadcrumbs_links_color":"","ocean_breadcrumbs_links_hover_color":"","ocean_display_footer_widgets":"default","ocean_display_footer_bottom":"default","ocean_custom_footer_template":"","_jetpack_memberships_contains_paid_content":false,"ocean_post_oembed":"","ocean_post_self_hosted_media":"","ocean_post_video_embed":"","ocean_link_format":"","ocean_link_format_target":"self","ocean_quote_format":"","ocean_quote_format_link":"post","ocean_gallery_link_images":"on","ocean_gallery_id":[],"footnotes":""},"categories":[10,21],"tags":[15,27,28],"class_list":["post-1017","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-net","category-c","tag-csharp","tag-iot","tag-linux","entry","has-media"],"jetpack_featured_media_url":"https:\/\/blog.adameczek.pl\/wp-content\/uploads\/2024\/05\/Csharp_logo.png","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/posts\/1017","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/comments?post=1017"}],"version-history":[{"count":35,"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/posts\/1017\/revisions"}],"predecessor-version":[{"id":1095,"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/posts\/1017\/revisions\/1095"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/media\/822"}],"wp:attachment":[{"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/media?parent=1017"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/categories?post=1017"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/tags?post=1017"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}