<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Krystian&#39;s Keep</title>
    <link>https://krystianch.com/</link>
    <description>Recent content on Krystian&#39;s Keep</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en</language>
    <lastBuildDate>Sat, 04 Oct 2025 00:00:00 +0000</lastBuildDate><atom:link href="https://krystianch.com/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Mistakes I made while making a self-balancing robot</title>
      <link>https://krystianch.com/sbr/</link>
      <pubDate>Sat, 06 Sep 2025 00:00:00 +0000</pubDate>
      
      <guid>https://krystianch.com/sbr/</guid>
      <description>Every control engineering graduate has to show off by building a self-balancing robot for some reason. I&rsquo;m a part of this clique, so I had no choice. This robot usually manifests itself as a two-wheeled device that keeps itself upright automatically by moving in the same direction it&rsquo;s falling. Imagine balancing a broomstick on your palm or a Segway.
I&rsquo;ve attempted 5 different designs since 2018 and now, after over 7 years, I can confidently say that the last one is quite good, and it was worth it, I guess. I decided to compile the mistakes I made along the way and my solutions, so other miserable control engineering students can finally get it over with. Let&rsquo;s start with the proof to earn your trust.
Download the video The robot&rsquo;s main parts are the stepper motors, an inertial measurement unit (IMU), an AVR microcontroller and a Li-ion battery pack. The microcontroller runs a cascade PID controller. The outer loop controls the inclination and the outer one &ndash; the linear velocity of the robot. The inclination angle is measured by the IMU and cleaned up with a complementary filter. The robot can be controlled with a gamepad via Bluetooth both while balancing and in horizontal mode. Browse to the source code repository to get the schematics, CAD models, bill of materials and, of course, the source code. Now let&rsquo;s proceed to the problems and solutions.
Making it too heavy, especially at the top When you look for hobby balancing robots on the internet you&rsquo;ll notice that most of them are tall and have the batteries placed high up. Their authors all seem to justify it by bringing up the same argument that increasing the relevant moment of inertia decreases the angular acceleration caused by a force, which in the case of gravity slows down the falling. That makes sense, but it may not exactly be what you want. Making the moment of inertia high also increases the motors&rsquo; torque needed to achieve a desired angular acceleration. Top-heavy robots will be more sluggish than others, and they need beefier motors.
If you dig deeper you&rsquo;ll notice that the most agile robots have their weight distributed lower. A few commenters on the Internet also came to the same conclusion. My eureka moment was when I noticed that MiniRyś by KNR Bionik lifts a bit when accelerating (or decelerating, depending on the direction) in horizontal mode. That gave me a hint that it must have a low moment of inertia.
I wanted to control my robot with a gamepad while it&rsquo;s balancing, and I wanted it to be fast and maneuverable. That&rsquo;s why I decided to make it short and place the heavy batteries low, between the motors. The current chassis is 16.5 cm tall with the batteries under the axle. This one is very stable, fast and is able to stand up by itself.
While I was shortening the robot, I also tried to make it lighter. I stripped out the unnecessary bits and replaced heavy materials with lighter ones. I 3D-printed the chassis with a low infill value &ndash; 15%. To make it stronger i increased the perimeter. My robot now weighs only 1003 g. Thanks to the lightness it responds quickly, is stable, mobile, the battery life is longer, and the inevitable collisions are less dramatic.
To demonstrate the instability and sluggishness of top-heavy robots I fastened a heavy metal egg (why do I own one?) to the robot using a long threaded rod. Then, I used the gamepad to drive it around a little. The experiment ended quickly and spectacularly.
Download the video Underestimating the sampling process Before I read PID Without a PhD by Tim Wescott I thought that, within reason, the higher the sample rate the better. Forum posts didn&rsquo;t help either, claiming you need around 1 kHz to balance such a robot well. This was not true in my case. I had problems with high-frequency oscillations and parameter tuning until I randomly decided to try halving my sample rate to 400 Hz. It worked like a charm. The noise in my derivative term decreased by a lot so I could crank it. The oscillations were gone, and the robot started to balance way more smoothly. A quote from the book sums it up well. &ldquo;You should treat the sampling rate as a flexible quantity.&rdquo;
Another sampling mistake I made was neglecting the delay between the angle measurement and the motor frequency update. At first, I also didn&rsquo;t configure the IMU&rsquo;s output data rate so it used the default of 100 Hz and the control loop frequency was 800 Hz. This introduced a lot of delay, which PID controllers don&rsquo;t like. Also, the microcontroller was triggering a read after updating the controller rather than before, which caused an additional delay.
Not examining the signals early Without the ability to examine the signals related to the controllers it was very hard for me to diagnose regulation problems. Being able to see the angles, velocities, errors, setpoints, control efforts is indispensable, especially if you can do it live. Having almost no experience with GTK, I&rsquo;ve whipped up a terrible GUI that could plot variables received via Bluetooth and change the controller parameters.
The first problem I caught with this setup was that the gyro range I&rsquo;ve set was too small, and it was saturating. Now my accelerometer range is +-2g and +-500 deg/s for the gyro.
Another one was the gyro offset, which I overlooked. It prevented me from setting the complementary filter&rsquo;s parameter (gyro weight) to a high value without causing a tug of war between both sensors. Fortunately, the IMU chip has something called fast offset compensation, so I could remove the offset by lying the robot down and running the procedure.
The last one was the noise. The complementary filter got rid of most of the accelerometer noise, but there was a lot of high frequency noise in the gyro readings. In my case, adding a simple low-pass IIR filter for just the gyro almost eliminated it and improved the stability by a lot.
Causing glitches in step signals To drive stepper motors, we must generate STEP signals for each motor. When the motor driver detects a rising edge on the STEP pin, it performs a single (micro)step and the motor&rsquo;s shaft turns a tiny amount. Unfortunately, there is one small problem with generating variable frequency square waves using digital counters. The ATmega328PB datasheet sums it up pretty well.
Changing [the counter&rsquo;s maximum value] to a value close to [0] while the counter is running must be done with care [&hellip;]. If the new value written to [the output compare register] is lower than the current value of [the counter], [it] will miss the compare match. The counter will then count to its maximum value [0xFFFF] and wrap around starting at [0] before the compare match will occur.
To fix that, we can enable the PWM mode, which provides double buffering of the output compare register. But that introduces another problem. If we set the counter&rsquo;s frequency to a very low value, let&rsquo;s say 0.1 Hz, we cannot generate another edge on the output until the counter counts to the maximum value, which in this case takes 10 seconds. This means that setting that low of a frequency will essentialy freeze the motor for the duration of one counter cycle.
My simple and effective solution to this problem is to set a lower bound on the frequency. This happens naturally by setting the counter frequency to the highest possible value (prescaler disabled). With the system clock set to 20 MHz, the minimum STEP signal frequency is 20 MHz / 2 / 65536 which is around 150 Hz. This might sound like a big jump from 0 to 150 Hz, but I didn&rsquo;t notice any problems caused by that in practice, probably thanks to 1/32 microstepping.
Another cause of glitches in my STEP signals was updating counter registers in a wrong order. When I changed the stepper motor drivers from DRV8825 to TMC2209 the robot started to jerk once every couple of seconds while balancing. It seems that the new drivers were less tolerant to glitches and I had to fix them. Making sure that the direction outputs are updated before the output compare registers eliminated the perceivable glitches entirely.
Making step changes to the velocity setpoint It was impossible to control the robot when I mapped the velocity setpoint directly to the position of one of the gamepad&rsquo;s joysticks. The abrupt changes to the velocity setpoint caused the robot to oscillate and fall over every time I touched the joystick. Low-pass filtering the setpoint (like the gyro) fixed this problem. While I was at it, I applied an identical filter to the angular speed for the controls to be consistent.
Underestimating the balance point My robot&rsquo;s center of mass is not directly above the axle. It has been way easier to tune the regulators since I&rsquo;ve taken it into account. I found an angle setpoint that made my robot balance in place with zero linear velocity. I disabled the outer (velocity) PID while leaving the inner (angle) one on. This way, imbalance was easier to spot as the smallest deflection caused the robot to quickly accelerate. When I found the angle, I slowly increased and decreased it to find a range of stable setpoints. I then placed my setpoint right in the middle of this range to get a more precise value. Even small changes in the construction of the robot influence the balance point, so I repeated this procedure after every change to the chassis.
Trying to use UART synchronously on Linux My first implementation of the telemetry module was bad. There was a special UART command telem that responded with telemetry data. This made the PC-side implementation simple. Send telem command, wait for response, parse it, send another telem command and so on. Synchronous and simple.
Apparently such an approach makes it very slow on Linux. I got a throughput of 2.5 kbps which was only 63 samples per second. My control loop executed 400 times a second, so there was no chance of recording data after every iteration. Setting the low_latency serial port flag helped a lot (28 kbps, 700 sps). Ultimately, redesigning telemetry so that it just sends data without waiting for a command was the way to go. Now the bottleneck is the robot&rsquo;s processor time, which is how it should have been from the beginning.
Trying to use fixed-point math My computer architecture lecturer, Grzegorz Mazur, said that when one thinks about using floating-point operations, they should reconsider. If you don&rsquo;t NEED floating-point, use fixed-point, he said. Well, either I&rsquo;m wrong (likely) or he&rsquo;s wrong (unlikely) because I couldn&rsquo;t make my control loop faster using fixed-point ops to save my life. My best attempt was 1040 us (fixed) vs 757 us (float), measured by connecting an oscilloscope to a pin and toggling it when entering and exiting the controller update routine. I used the sat accum (Q16.16) type everywhere, to eliminate conversions, but it was no use. It may be because I kept the floating-point atan routine. Was my implementation that bad or are floating-point operations that well optimized on AVR? That I don&rsquo;t know, but I don&rsquo;t think it&rsquo;s worth it to explore this now. With floats, the robot&rsquo;s microcontroller still has a bit of time for tasks other than updating the controllers, like processing commands and sending telemetry.
Not limiting acceleration With steppers, I overlooked the fact that while I can change the frequency of the step signal arbitrarily that doesn&rsquo;t mean that the motor will always obediently perform the steps. My robot currently doesn&rsquo;t have wheel encoders, so there&rsquo;s no wheel position feedback. Limiting the acceleration helped a lot with a smooth transition from horizontal mode to balancing. One exception, where I allow high acceleration is breaking. This makes it possible for the robot to use its momentum to stand up.
I&rsquo;m happy with the current design of my self-balancing robot. It&rsquo;s so much fun to play with not only by myself, but controlling it is so easy it makes for a great activity when I have people over.
If you are a control engineering graduate, I hope this article helps you to free yourself from the self-balancing robot hell. If you are a regular person, I hope it inspires you to try building one. Now it&rsquo;s time for me to shelve this project for another seven years. But before I do that, here&rsquo;s one last video showing how the robot changed since I started this project.
Download the video </description>
    </item>
    
    <item>
      <title>Instrukcja obsługi serwera NAS</title>
      <link>https://krystianch.com/nas/</link>
      <pubDate>Sat, 30 Nov 2024 00:00:00 +0000</pubDate>
      
      <guid>https://krystianch.com/nas/</guid>
      <description>Podłączanie serwera Do serwera należy podłączyć jedynie zasilacz (złącze DC) i jeden przewód sieciowy (w dowolne z dwóch gniazd).
Opcjonalnie, można podłączyć drugi przewód sieciowy w celu uzyskania nadmiarowości połączenia.
Wyłączanie serwera Sprawdzić czy serwer nie jest już wyłączony. Serwer jest wyłączony, gdy świeci się tylko czerwona dioda.
Aby wyłączyć serwer należy jeden raz krótko wcisnąć przycisk POWER. Następnie, należy odczekać aż zgasną wszystkie diody oprócz czerwonej. Serwer, jeśli jest bezczynny, wyłącza się ok. 6 sekund.
Włączanie serwera Sprawdzić czy serwer nie jest już włączony. Serwer jest włączony, gdy oprócz czerwonej diody świecą się niebieskie.
Serwer włącza się automatycznie po podłączeniu zasilania. Jeśli został wcześniej wyłączony za pomocą przycisku można włączyć go, wcickając jeden raz krótko przycisk POWER. Serwer uruchamia się ok. 1 minutę.
Odszyfrowywanie dysków Po włączeniu serwera dyski są nadal zaszyfrowane. Jest to zabezpieczenie przed kradzieżą. Jeśli serwer zostałby ukradziony, sprawca nie uzyska dostępu do danych, jeśli nie będzie znał hasła. Zatem, aby móc korzystać z archiwum, należy najpierw odszyfrować dyski.
Wejdź na stronę archiwum.local, wpisz hasło, które otrzymasz od administratora i kliknij przycisk Odszyfruj. Po pomyślnym odszyfrowaniu można korzystać z archiwum.
Szyfrowanie dysku Jeśli praca z archiwum została zakończona i nie jest wymagany dostęp zdalny można zwiększyć bezpieczeństwo danych, szyfrując dyski poprzez wyłączenie serwera. Zob. rozdział Wyłączanie serwera.
Konfiguracja dostępu do archiwum z sieci biurowej Windows 10: Na komputerze, który ma mieć dostęp do archiwum kliknij przycisk *Start* i wyszukaj program *Ten komputer*, ale nie uruchamiaj go. Po prawej stronie, pod ikoną i napisem *Ten komputer* wybierz z listy opcję *Mapuj dysk sieciowy*. Windows 11: Na komputerze, który ma mieć dostęp do archiwum kliknij przycisk *Start* i wyszukaj program *Komputer*, uruchom go. Kliknij przycisk z trzema kropkami *&ctdot;* i wybierz opcję *Mapuj dysk sieciowy*. W otwartym oknie, w pole Folder wpisz \\archiwum.local\archiwum. Zwróć uwagę na kierunek ukośników. Poprawny znak to \, a nie /. Kliknij przycisk Zakończ.
W otwartym oknie wpisz nazwę użytkownika archiwum i hasło dostępowe do archiwum, które otrzymasz od administratora. Zaznacz pole Zapamiętaj moje poświadczenia, jeśli chcesz, aby hasło zostało zapamiętane i kliknij przycisk OK.
Otworzy sie okno, w którym widoczna jest zawartość archiwum. Jest ono też teraz dostępne jako dysk sieciowy w eksploratorze plików.
Konfiguracja dostępu do archiwum spoza sieci biurowej Dostęp do archiwum spoza sieci biurowej (np. z domu) odbywa się przez wirtualną sieć prywatną (VPN) z dwóch powodów. Po pierwsze infrastruktura operatora internetu w biurze znacznie utrudnia dostęp do sieci wewnętrznej z zewnątrz. Po drugie, VPN szyfruje ruch między komputerem użytkownika, a serwerem Archiwum. Poprawna konfiguracja VPN powoduje, że jedynie ruch do sieci biurowej przechodzi przez VPN. Pozostały ruch sieciowy podąża normalnymi ścieżkami (nie przechodzi przez VPN), nawet gdy tunel jest włączony.
Zainstaluj program WireGuard ze strony wireguard.com/install i uruchom go.
Kliknij przycisk Dodaj Tunel w lewym dolnym rogu okna programu. Wybierz plik konfiguracyjny VPN otrzymany od administratora. Jeśli tunel nie jest aktywny, aktywuj go przyciskiem Aktywuj.
Wykonaj instrukcje w rozdziale Konfiguracja dostępu do archiwum z sieci biurowej ale użyj innego adresu serwera. Zamiast archiwum.local użyj adresu serwera w VPN, otrzymanego od administratora. Reszta ścieżki powinna pozostać niezmieniona.
Rozwiązania często spotykanych problemów Komunikat o braku uprawnień do dysku sieciowego Upewnij się, że dysk nie jest zaszyfrowany. Zob. rozdział Odszyfrowywanie dysku.
Komunikat o braku możliwości nawiązania połączenia Upewnij się, że serwer jest włączony. Zob. rozdział Włączanie serwera.
</description>
    </item>
    
    <item>
      <title>How I manage photos</title>
      <link>https://krystianch.com/photos/</link>
      <pubDate>Thu, 10 Oct 2024 00:00:00 +0000</pubDate>
      
      <guid>https://krystianch.com/photos/</guid>
      <description>I&rsquo;m not a photographer and this post is targeted to non-photographers.
I think sharing photos with family and friends is an unsolved problem. Hear me out. Let&rsquo;s say you&rsquo;re at a family gathering. You and others take a few pictures. How will you share them with each other? Well, maybe you&rsquo;ll use Google Drive or Dropbox or other service. You&rsquo;ll create a new folder for the new album and put your photos there. If you&rsquo;re in luck, others will send you their photos, and you can pull them into your folder. You&rsquo;ll then share this folder with them. You&rsquo;ll add their accounts if they have one, or create a special link and send it to them. Alternatively, you already have a folder that you&rsquo;ve shared with others. In this case, you&rsquo;d create a new folder for the album in this shared folder and put the photos there.
I&rsquo;ve witnessed variations of this story many times. And there are a couple of problems with it. Who owns and takes care of the storage account? Will they keep doing it for years? What happens when the free space runs out? Who pays for more? What if multiple people share photos from the same event from different accounts? How do you aggregate them? How do elderly people access digital family photos? Do they need an app? Do they know how to use it? What if Google, Dropbox, etc. decides to lock you out of your account?
I think that to solve these problems we need to reach into the past when we didn&rsquo;t all have pocket cameras and bottomless albums. My solution is to take responsibility for the photos. I feel that we&rsquo;ve become intoxicated by the fact that we can take thousands of pictures without any consequences. And I think we need a rehab. We need a habit of organizing our photos and not taking so many of them that organizing them is a dreaded task. Remember your grandma that had all your family photos nicely sorted in albums? I think we could learn a thing or two from her.
After almost three years of intermittent experimenting and tweaking I think I have a solid system of storing, organizing and sharing photos. Let&rsquo;s start with sorting. I keep my photos segregated in directories by year, month and album name. Album names are determined by directory names. E.g. photos from vacation in Japan are in 2023/2023-08 Japan and random photos from August 2024 are in 2024/2024-08. The limit of two levels of nesting makes it easy to browse and search, while keeping the number of child directories relatively small and easy to scan. Having a directory per album is also convenient for sharing. You can share the whole directory with somebody to share the album without selecting individual photos. There&rsquo;s one more rule. Inside some album directories there&rsquo;s a child directory named other. This directory is ignored most of the time and is never shared. It&rsquo;s basically a trash directory. Photos that I rejected because they were not good or interesting enough. I delete them after some time to reclaim disk space.
Now hardware. I have a NAS based on Odroid H3+ with two 4 TiB IronWolf hard drives. They make up a mirrored ZFS pool, with a special dataset that&rsquo;s only for photos. It&rsquo;s periodically backed up to a similar NAS at my parents house. This photo filesystem is accessible after authentication via SMB from local network or from the internet through a WireGuard VPN. For photo ingest I use Rapid Photo Downloader.
However, SMB is mostly good for album management and ingest. It&rsquo;s not good for browsing because of small but annoying delays when loading large photos even through a 1 Gbps link. It&rsquo;s also not well-suited for sharing albums. For that I created photobrowser. It&rsquo;s a lightweight web app that lets me browse all albums from my PC or phone. It uses the filesystem as an album database. Furthermore, it generates preview images, thumbnails and proxy video clips so that I can view the albums even with a slow internet connection. But the main selling point of it is the album sharing system. I can create sharable secret links and attach any number of albums to them. So I have one for my parents, one for my father&rsquo;s side of the family, one for my mother&rsquo;s side and more for my friend groups.
Photobrowser is also designed to run as multiple instances on the same machine. On my NAS there are two: one for me and one for my fiancé. Cumulatively, we have three photo libraries: one is mine (for my personal photos), one is hers (for her personal photos) and the third one is common (for our and some family photos). You might wonder how there are three libraries but only two photobrowser instances. There is a symlink to the common directory in each of our personal datasets and photobrowser follows it. It&rsquo;s quite convenient because the name of the symlink is common, so common albums&rsquo; paths start with common/ (e.g. common/2023/2023-12 Christmas).
The most challenging part of writing photobrowser was generating the photo and video previews. Photobrowser stores the proxies next to the photos in special directories: .sh_thumbnails for proxy images and .sh_proxies for proxy clips (inspired by XDG Thumbnail Managing Standard). I wanted to generate them on the fly as photos are added to the library, so I used inotify. The hard part was handling the various events properly. Photos can be copied or moved into or out of the library or moved around inside the library. They can be deleted or renamed. The same goes for directories where the photos reside. All of these events must be handled perfectly, or the previews become out of sync with the photos. Fortunately, it seems to be working quite well with a few bugs already fixed and probably a few still waiting to be discovered and fixed.
I&rsquo;m aware that this system doesn&rsquo;t solve all the problems that I named above. But it&rsquo;s proven to be convenient and useful. I already got some compliments from family members about how fast photobrowser is compared to the solutions they&rsquo;re used to. I also got some bug reports from friends, which is not as supporting but admittedly more useful. My fiancé and I use it often to show or share our photos to family and friends. All in all, people were thrilled that someone finally made the effort to organize photos from events and were glad to have access to my photo library. See you next time!
</description>
    </item>
    
    <item>
      <title>Dostęp SSH do kontenerów w Kubernetes [PL]</title>
      <link>https://krystianch.com/cgc-ssh/</link>
      <pubDate>Mon, 30 Sep 2024 00:00:00 +0000</pubDate>
      
      <guid>https://krystianch.com/cgc-ssh/</guid>
      <description>W Comtegrze umożliwiamy naszym klientom korzystanie z kontenerów z dostępem do GPU na zasadzie chmury: Comtegra GPU Cloud (CGC). Dla wielu z nich dostęp z poziomu przeglądarki jest wygodny (np. Jupyter Notebook). Z biegiem czasu jednak pojawiły się pytania o bardziej bezpośredni dostęp do powłoki kontenerów. Zainspirowani mechanizmem działania Gita przez SSH, stworzyliśmy rozwiązanie, dzięki któremu użytkownicy mogą zdalnie połączyć się do powłoki swoich kontenerów w Kubernetes. W tym artykule pierwszy raz rzucamy światło na komponenty CGC aby podzielić się efektami naszej pracy z szerszym gronem odbiorców.
Żeby użytkownik mógł bezpiecznie zalogować się do powłoki kontenera, musieliśmy rozwiązać 4 problemy.
Uwierzytelnienie: kto próbuje się zalogować? Czy jest tym, za kogo się podaje? Autoryzacja: do których kontenerów powinien mieć dostęp? Powłoka: połączyć użytkownika do powłoki odpowiedniego kontenera. Zabezpieczenia: użytkownik powinien móc jedynie komunikować się z powłoką kontenera. Pozostałe akcje powinny być zablokowane. Użytkownicy mogą zalogować się do powłok swoich kontenerów przez SSH. Uwierzytelniani i autoryzowani są za pomocą par kluczy, których publiczną część dodali wcześniej do swojego konta w CGC. Do tego wykorzystujemy opcję AuthorizedKeysCommand serwera OpenSSH (sshd). Kontener wybierają, dodając jego nazwę do polecenia SSH (np. ssh cgc@cgc-api.comtegra.cloud kontener1). Z powłoką łączymy użytkowników za pomocą wywołania systemowego exec i programu kubectl exec.
Uwierzytelnienie Już od wersji OpenSSH 6.2, opublikowanej w marcu 2013, sshd posiada opcję AuthorizedKeysCommand. Umożliwia ona dynamiczne generowanie list kluczy, które są uprawnione do łączenia się z serwerem. Wcześniej, listy mogły być jedynie ładowane z systemu plików. Oprócz kluczy listy te zawierają opcje, które są przydatne do kontroli dostępu np. wyłączenie przekierowywania portów, agentów i X11, wyłączenie alokacji PTY, wykonywania zawartości plików ~/.ssh/rc.1 Do tych opcji należy też wymuszenie użycia konkretnej powłoki.
W CGC podobnie jak w GitHubie, GitLabie itp. użytkownicy mogą dodać do swojego konta publiczne części kluczy, którymi chcą się posługiwać w komunikacji SSH z platformą. Dzięki temu, możemy zidentyfikować użytkownika na podstawie klucza, jakim próbuje się zalogować. Weryfikacją klucza zajmuje się sshd. Zobaczmy teraz, jak w praktyce możemy uwierzytelnić użytkownika za pomocą opcji AuthorizedKeysCommand na podstawie bazy kluczy publicznych.
W konfiguracji sshd dodajemy następującą linię, która instruuje usługę, aby podczas każdej próby logowania uruchamiała program cgc-keys z argumentami: nazwa użytkownika (%u), typ klucza (%t), klucz (%k).2 To, co ten program wypisze na standardowe wyjście, zostanie potraktowane przez sshd jak zawartość pliku authorized_keys.
AuthorizedKeysCommand /usr/local/bin/cgc-keys &#34;%u&#34; &#34;%t&#34; &#34;%k&#34; Program cgc-keys najpierw sprawdza, czy użytkownik loguje się jako cgc (ssh cgc@host), a następnie odpytuje bazę danych o użytkownika na podstawie klucza publicznego. Na tym etapie sshd już zweryfikował, że użytkownik posiada prywatną parę do podanego klucza publicznego. Zatem jeśli klucz znaleziono w bazie, to tożsamość pasującego użytkownika można uznać za potwierdzoną.
Przykładowe fragmenty kodu w tym artykule zostały napisane w języku powłoki kompatybilnej z /bin/sh. Obsługa błędów w przykładach została ograniczona dla zwięzłości przykładów.
user=&#34;$1&#34; keytype=&#34;$2&#34; key=&#34;$3&#34; if [ &#34;$user&#34; != &#34;cgc&#34; ]; then exit 0 fi user=$(query_db &#34;username&#34; &#34;$keytype&#34; &#34;$key&#34;) if [ -z &#34;$user&#34; ]; then exit 0 fi Autoryzacja W CGC użytkownicy mają dostęp do wszystkich kontenerów w przestrzeni nazw, do której należą. Zatem wystarczy, że określimy przestrzeń nazw, do której przyporządkowan=y jest właściciel klucza. Można to zrobić, zastępując zapytanie do bazy poniższym.
namespace=$(query_db &#34;namespace&#34; &#34;$keytype&#34; &#34;$key&#34;) if [ -z &#34;$namespace&#34; ]; then exit 0 fi Powłoka Na tym etapie użytkownik został uwierzytelniony i autoryzowany do pewnej przestrzeni nazw. Pozostaje rozwiązanie problemu połączenia go do powłoki odpowiedniego kontenera. Tym zajmie się program cgc-shell.
Wspomniałem wcześniej, że program cgc-keys powinien wypisać na standardowe wyjście tekst w formacie authorized_keys. Możemy wykorzystać opcje tego formatu do wymuszenia wykonania programu cgc-shell przez użytkownika. Jeśli wypiszemy linię jak niżej, to użytkownik zostanie zalogowany do serwera, ale zamiast powłoki serwera lub innego, podanego polecenia (np. ssh cgc@host ls) w jego imieniu zostanie uruchomiony program cgc-shell z argumentem $namespace.
echo &#34;command=\&#34;cgc-shell $namespace\&#34; $keytype $key cgc&#34; Program cgc-keys jest świadomie rozdzielny z programem cgc-shell, gdyż pełnią one inną funkcję. Pierwszy z nich, za pomocą opcji AuthorizedKeysCommandUser uwierzytelnia użytkownika, natomiast drugi stanowi jego powłokę. Strumienie wejścia/wyjścia procesu cgc-shell są połączone z terminalem użytkownika.
Kubernetes umożliwia wykonywanie dowolnych poleceń w kontenerach za pomocą polecenia kubectl exec. Możemy zatem wykonać odpowiednie polecenie kubectl w imieniu użytkownika, aby połączyć go do powłoki żądanego kontenera. Podmianę procesu cgc-shell na kubectl z odpowiednimi argumentami można zrealizować wywołaniem systemowym exec.
W CGC użytkownicy wybierają kontener poprzez podanie nazwy deploymentu (rozlokowania). Takiemu deploymentowi w danej chwili prawie zawsze przypada jeden pod, do którego z kolei przypisany jest zwykle jeden kontener. Oznacza to, że po nazwie deploymentu, którą nadaje użytkownik można zidentyfikować odpowiedni kontener. Warto również zauważyć, że polecenie kubectl exec umożliwia podanie deploymentu. Wtedy samo spróbuje zidentyfikować odpowiedni kontener. Jednak najpierw należy zdobyć od użytkownika odpowiednie argumenty, a mianowicie nazwę deploymentu i polecenie, które zostanie wykonane w kontenerze.
Argumenty te użytkownik może przekazać, dopisując je do polecenia ssh (np. ssh cgc@host deployment1 ls -la). sshd przekaże te argumenty do ssh-shell w zmiennej środowiskowej SSH_ORIGINAL_COMMAND. Pozostaje jedynie oddzielenie nazwy deploymentu od polecenia, które ma zostać wykonane w kontenerze. Do tego można wykorzystać set, $1, shift oraz $@ jak pokazano poniżej.
namespace=$1 set -- $SSH_ORIGINAL_COMMAND resource=&#34;$1&#34; if [ -z &#34;$resource&#34; ]; then echo &#34;Error: resource name not specified&#34; &gt;&amp;2 exit 1 fi shift cmd=&#34;$@&#34; if [ -z &#34;$cmd&#34; ]; then cmd=&#39;/bin/sh&#39; fi exec kubectl --namespace &#34;$namespace&#34; \ exec --stdin --tty deployment/&#34;$resource&#34; -- $cmd Zabezpieczenia Kod i konfiguracja w tym artykule stanowią jedynie przykłady i nie były poddane audytowi bezpieczeństwa.
Pozostało zadbać o bezpieczeństwo tego rozwiązania. Jest klika rzeczy, które możemy zrobić w celu jego poprawy. Wspominałem już, że format authorized_keys umożliwia ustawienie dodatkowych opcji. Jedną z nich jest restrict, która włącza wszystkie ograniczenia. Należy do nich zablokowanie alokacji tty, która jest niezbędna do działania interaktywnej powłoki. Możemy jednak użyć opcji restrict i dodać wyjątek dla blokady tty za pomocą opcji pty.
echo &#34;restrict,pty,command=\&#34;cgc-shell $namespace\&#34; $keytype $key cgc&#34; Oprócz opcji restrict możemy również umocnić konfigurację samego serwera. Poniżej znajdują się przydatne opcje sshd i ich wartości.
PermitRootLogin no AuthorizedKeysFile /dev/null AuthorizedKeysCommandUser nobody PasswordAuthentication no KbdInteractiveAuthentication no GatewayPorts no PermitUserEnvironment no Programy cgc-keys i cgc-shell wymagają innego zestawu uprawnień. cgc-keys wymaga jedynie dostępu do bazy danych, więc jest uruchamiany jako użytkownik systemowy o właśnie takich uprawnieniach (opcja AuthorizedKeysCommandUser). cgc-shell wymaga natomiast jedynie dostępu do powłoki kontenerów i jest uruchamiany dopiero po pomyślnym uwierzytelnieniu i autoryzacji użytkownika, co zmniejsza powierzchnię ataku na taki system. W tym przykładzie cgc-shell zostanie uruchomiony jako użytkownik systemowy cgc.
A propos uprawnień do powłoki kontenerów, uprawnienia użytkownika Kubernetes, którego używa kubectl powinny być minimalne. W tym przypadku wymagane są jedynie poniższe uprawnienia.
apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: exec-all rules: - {apiGroups: [&#39;apps&#39;], resources: [&#39;deployments&#39;], verbs: [&#39;get&#39;]} - {apiGroups: [&#39;batch&#39;], resources: [&#39;jobs&#39;], verbs: [&#39;get&#39;]} - {apiGroups: [&#39;&#39;], resources: [&#39;pods&#39;], verbs: [&#39;list&#39;]} - {apiGroups: [&#39;&#39;], resources: [&#39;pods/exec&#39;], verbs: [&#39;create&#39;]} Dodatkowym zabezpieczeniem może być uruchomienie sshd na oddzielnym serwerze, maszynie wirtualnej, kontenerze. Może być to dobrym sposobem na izolację tej usługi i dalsze zmniejszenie powierzchni ataku.
CGC-SSH w praktyce Pokazaliśmy jak wykorzystać opcję AuthorizedKeysCommand i kilka innych mechanizmów serwera OpenSSH do stworzenia rozwiązania, które umożliwia użytkownikom połączenie się do powłok kontenerów Kubernetes przez klienta SSH. Po wprowadzeniu tej funkcji do CGC jeden z naszych najbardziej wymagających klientów napisał:
[&hellip;] uważamy, że narzędzia cgc przez ostatnie parę miesięcy wzbogaciły się o bardzo dużo świetnej z naszego punktu widzenia funkcjonalności [&hellip;] Co tu dużo mówić - kawał dobrej roboty po Państwa stronie :) Dziękujemy.
Takie wiadomości powodują, że my — inżynierowie — mamy zapał do pracy i do dalszego rozwijania produktów.
Podziękowania Dziękujemy Drew DeVault, założycielowi platformy sourcehut za publikację artykułu What happens when you push to git.sr.ht, and why was it so slow?, który stanowił dla nas inspirację i źródło wiedzy na temat niestandardowych aplikacji serwera OpenSSH.
Copyright 2024 Comtegra S.A.
https://man.openbsd.org/sshd.8#AUTHORIZED_KEYS_FILE_FORMAT&#160;&#x21a9;&#xfe0e;
https://man.openbsd.org/sshd_config.5#TOKENS&#160;&#x21a9;&#xfe0e;
</description>
    </item>
    
    <item>
      <title>Transparent editing of Base64-encoded Kubernetes secrets</title>
      <link>https://krystianch.com/keditb64/</link>
      <pubDate>Wed, 29 May 2024 00:00:00 +0000</pubDate>
      
      <guid>https://krystianch.com/keditb64/</guid>
      <description>I&rsquo;ve been using Kubernetes a lot at work recently. One of the many frustrating things about it is that the contents of secrets are viewed and edited in Base64 encoding. To add insult to injury, a lot of third-party software for Kubernetes store configuration as secrets. Viewing them is not so painful. You can just pipe them to the base64 program, and you&rsquo;re set. But if you want to edit them, prepare for a decode-edit-encode dance every time.
One time I broke and spent a few hours working on a tool that just lets me edit secrets in my favorite editor. Today I&rsquo;d like to show you my Base64 Kubernetes secret editor: keditb64 (mirrored on SourceHut and on Codeberg). It retrieves and decodes any secret you point it at. It then opens $EDITOR or vim and lets you edit it. When you close the editor it encodes and writes contents back to the secret. It also supports gzipped secrets with flag -z, because I needed that for debugging Prometheus configuration at some point.
Let&rsquo;s assume you have a secret defined by the following manifest:
apiVersion: v1 kind: Secret metadata: namespace: mynamespace name: mysecret data: mykey: bXlzZWNyZXRjb250ZW50cw== You can then call keditb64 to edit the value of mysecretkey like this:
keditb64 -n mynamespace mysecret mykey Before that, I did this: kubectl get secret mysecret -n mynamespace -o jsonpath='{.data.mykey}' | base64 -d, copy, vim, paste, edit, copy, base64 -w0, paste, Ctrl+d, copy, kubectl edit secret mysecret -n mynamespace, paste, save, close.
This process could probably be optimized without resorting to writing a new tool, but I figured, I&rsquo;d just write one that does exactly what I want and how I want. Below are some real-world usage examples:
# Editing configuration of Alertmanager used with Prometheus Operator keditb64 -n monitoring alertmanager-main alertmanager.yaml # Viewing Prometheus configuration keditb64 -p -z -n monitoring prometheus-k8s prometheus.yaml.gz # Editing HTTP Basic Auth credentials keditb64 -n apps auth-admin-docs users # Editing TLS certificates keditb64 -n monitoring blackbox-exporter-tls tls.crt Although the tool already does everything I wanted it to, as always, contributions are welcome. If it proved useful to you, and you want to improve it, please send patches to my public inbox (address below) or submit them on Codeberg. See you next time!
</description>
    </item>
    
    <item>
      <title>I spent 4 weeks working only on free software</title>
      <link>https://krystianch.com/2024break/</link>
      <pubDate>Wed, 31 Jan 2024 00:00:00 +0000</pubDate>
      
      <guid>https://krystianch.com/2024break/</guid>
      <description>The SafeDNN project ended in December 2023. I decided to take January off to rest and work on free software. Here&rsquo;s a summary of what I worked on after returning from a week-long ski trip.
Let&rsquo;s start with Alpine Linux. I&rsquo;ve packaged the new libcamera-based Raspberry Pi camera stack. I built it, solved some problems, tested it and submitted the package to aports. The old raspicam API is deprecated, so this package, once it is merged, will be a convenient way to switch to the new one.
Thanks to ikke&rsquo;s hint on IRC (#alpine-devel on OFTC), I&rsquo;ve also written a wiki page on an alternative way to upgrade the kernel on diskless Alpine on Raspberry Pi. It&rsquo;s handy when you need to upgrade to a version that is available in the package repositories but is not yet part of an Alpine release.
I revamped my own Alpine package repository, which is now available at alpine.krystianch.com and includes some packages that are not available in the official ones. To complete it I set up a Raspberry Pi B+ as a dedicated builder so besides packages for x86_64, there are also some built for the armhf architecture.
Since I started using WireGuard, I dreaded adding new devices to the network. Generating keys, assigning addresses, creating and importing configs, testing the connection all required some careful work. I&rsquo;ve searched for a solution for that, but I didn&rsquo;t really like anything that was available. So I made wgpeer. It&rsquo;s a convenient shell script for adding new peers to a network. But please note that it mainly caters to WireGuard setups with a central hub.
I&rsquo;ve installed OpenWrt on an Edgerouter X in my home network. It was pretty painless and the software is way better than the Ubiquiti&rsquo;s stock EdgeOS. Its default configuration was surprisingly good. For example, local name resolution under the .lan domain is set up out of the box. Configuring the firewall, Wireguard, and DDNS is very easy and convenient. The LuCI web interface is an exceptional piece of software.
Speaking of OpenWrt&rsquo;s ddns-scripts, I&rsquo;ve done some work on a new script that sends updates to Hexonet. The basic functionality is there, and it is running on my router and updating correctly as I write this, but few ddns-scripts settings and features are currently supported. Nevertheless, you can find it here.
There was also some time for experimental projects such as my new home video security system. It&rsquo;s a development of an idea I had in the past but this time more polished and on a larger scale (supports multiple cameras). It includes live previews in the browser, cameras with interchangeable lenses (thanks to the RPi HQ Camera), quick manual on/off switch and recording LEDs for privacy. Everything done by integrating free software components such as ffmpeg.
I published a post about my HSV night light on this blog and added a demonstration video to an older one. I took some time to organize my notes using nb. I&rsquo;m still new to this tool, but I&rsquo;m liking it so far. I also brushed up on neural network math so that I&rsquo;m sharp before the first day of my new job.
Speaking of which, tomorrow is that first day. I&rsquo;m going to work at Comtegra as an AI / DevOps engineer. I&rsquo;m very excited. It&rsquo;s a medium-size Polish company located in Warsaw that specializes in system integration and have their own cloud solution called Comtegra GPU Cloud, which I&rsquo;ll be working on. I&rsquo;ve used it when I worked at the Warsaw University of Technology and quite liked it for the simplicity of its user interface. I can&rsquo;t wait to look at the internals. See you next time!
</description>
    </item>
    
    <item>
      <title>HSV night light</title>
      <link>https://krystianch.com/light/</link>
      <pubDate>Fri, 19 Jan 2024 00:00:00 +0000</pubDate>
      
      <guid>https://krystianch.com/light/</guid>
      <description> Download the video The idea of an LED lamp with smooth control of hue, saturation and brightness was floating around in my head for quite a while now. I knew I finally had to make one when I found an old ceiling light with a large conical frosted glass cover.
[Source code] [Schematic and PCB layout] [3D models]
The enclosure is 3D printed. The top transparent part acts as a light diffuser. Inside there is a 3 W RGB LED. The bottom part houses the electronics. The front panel features an on-off switch and three knobs, which control the hue, saturation and brightness of the light. On the back there is a barrel jack socket to connect a 12 V power supply.
Front panel
For its size, the lamp is surprisingly bright.
First prototype First prototype. Conical part reused from a ceiling light.
The first iteration of the project was a single prototype that uses the cover for an old ceiling light as a diffuser. The conical glass part sits on top of a 3D printed base, which also provides some space for the front panel. Of course this is not a sustainable way to create more lamps. This is why I designed a smaller, wholly 3D-printable enclosure. The prototype also has three 3 W LEDs which makes it a 9 W lamp. It also has a hue knob with continuous rotation (see section: Issues). The prototype&rsquo;s frosted glass cover diffuses the light way better than the transparent 3D printed plastic, but I only had one of those.
Theory of operation The three knobs set the coordinates in the HSV color space. These coordinates are translated to RGB and respective LED channels are driven with power proportional to the R, G, and B coordinates.
This process is performed by a microcontroller in a loop. First, the position of all knobs is read by the built-in 10-bit ADC. Then, the knob positions are mapped to the correct ranges, which is 0.0 - 360.0 degrees for hue and 0.0 - 1.0 for saturation and brightness. Next, these formulas are used to convert HSV coordinates to RGB. Finally, RGB coordinates (range 0.0 - 1.0) are mapped to 8-bit integers (range 0 - 255) and duty cycle of respective PWM channels is set to these values.
The PCB The PCB
The main PCB features a three channel LED driver based on the LM3407 IC, an ATtiny85 microcontroller, a 7805 linear voltage regulator, bypass capacitors, pin headers and pads for soldering wires. The circuit and layout of the LED driver channels is taken straight from the LM3407 datasheet.
The LED current is set for each channel by parallel resistor pairs R1&amp;R2, R4&amp;R5, R7&amp;R8. The resistor pairs are 1 and 1.5 Ohms so the parallel configuration yields 0.6 Ohm equivalent resistance. The formula for calculating the set current is 0.198 V divided by the equivalent resistance of the sense resistors, which in this case is 330 mA. The maximum rated constant current is 370 mA, so the LEDs are underdriven to extend their lifetime.
Cooling When it comes to LEDs, cooling can&rsquo;t be disregarded. High-power ones get hot quick and with inadequate cooling they die soon after that. In order to ensure a long life of the lamp I had to make sure that all the heat can be safely dissipated.
In the original large 9 W prototype the cooling solution is basically built-in. LEDs are soldered to an aluminum PCB, which is fastened to the metal base of the ceiling light. This dissipates heat so well that the LEDs stay very cool.
However, the second iteration only had a small aluminum PCB with nothing heat-conductive to mount it to. Fortunately, there were some flat aluminum profiles in my dad&rsquo;s scrap metal bin. He cut it for me into several 60 x 60 x 5 mm plates. I could then glue the PCBs to the plates with a thermally conductive adhesive ensuring good heat dissipation.
The manufacturers of such LEDs recommend that the temperature of the LED enclosure should stay below 80 degrees C. With this heat dissipation solution the greatest temperature I recorded was 62 degrees with a 21 degree ambient temperature, measured with a thermocouple.
Internals. Diffuser removed.
Issues The first issue became apparent very quickly after running the first version of the program. The potentiometer with continuous rotation has an issue of being scratchy on boundaries of the open section. What I mean is that there are two very narrow ranges of angles where the resistance between the wiper and the lugs change very quickly and unpredictably. In practice this meant that there were very short but noticeable flashes of blue light while rotating the hue knob near +/- 5 degrees.
Visualization of continuous potentiometer&rsquo;s range. Linear section in blue. Open section in gray. Scratchy sections in red.
The next issue is the lack of calibration. Due to the different luminous flux per milliamp of current for each of the LED channels, the hue, saturation and brightness controls are not fully independent. For example, the hue and saturation of the light also change a little when turning the brightness knob. This effect is most noticeable when brightness is low. A possible solution would be to compensate for this effect, but that would call for some kind of color calibration tool and I haven&rsquo;t looked into that yet. Maybe a DSLR would be enough.
8-bit PWM allows setting only 256 distinct power levels to the LED channels. Levels below 11 cause flickering because they provide barely enough power to light up the LEDs. This leaves us with 245 power levels. Unfortunately this is not enough to prevent a stepping effect while slowly rotating the knobs. A microcontroller with a 16-bit PWM channel would solve this problem.
When designing the PCB I thought I won&rsquo;t need a programming connector because I can always pull out the microcontroller from the socket and put it into a breadboard set up for programming. However, the constant pulling the chip in and out of the socket turned out to be very inconvenient during development. I&rsquo;d also want to include a fuse holder or a polyfuse on the next revision of the PCB so that I won&rsquo;t have to mount an external fuse in the enclosure. I also didn&rsquo;t think much about connecting the power wires to the PCB and the large soldering pads is not a good solution for this. Next time I&rsquo;ll probably just use through-holes.
The 3D-printed enclosure also needs some more attention. While the thick frosted glass of the first prototype diffuses the light beautifully, the transparent PETG plastic doesn&rsquo;t. Even a 3 mm thick layer of it refracts light in such a way that the primary colors peek through, especially in the lower part that is closest to the LEDs inside. I&rsquo;ve experimented with various levels of infill for the prints to mitigate this effect. However, both values I tried had some downsides. 15% infill causes vertical stripes to be visible when light shines though the part. 100% infill gives a sparkly lattice on the top side of the cube and still doesn&rsquo;t completely get rid of the refraction effect. Other things I&rsquo;d like to try in the future are different infill patterns, even more thickness on the transparent part and printing an additional inner diffuser.
Summary I wanted to own such a lamp for a long time. The smooth control of the hue is very satisfying. Despite many issues with the design, it&rsquo;s very functional. My partner and I use it a lot. We placed the large 9 W one in the corner of the living room, where it lights up the walls beautifully. After eight months, my partner still turns it on and chooses a different color almost every evening. She also put one of the 3 W lamps on her desk and turns it on when the sun starts to set. I&rsquo;ve also given one as a gift for Christmas and the feedback was very positive.
</description>
    </item>
    
    <item>
      <title>Ad hoc home video surveillance with OBS Studio</title>
      <link>https://krystianch.com/cctv/</link>
      <pubDate>Mon, 11 Sep 2023 00:00:00 +0000</pubDate>
      
      <guid>https://krystianch.com/cctv/</guid>
      <description>Recently it came up that the previous tenant of my apartment still had keys to the mailbox after almost a year since he moved out. This left me wondering what else he still has access to. Since I was then soon leaving for vacation, in addition to replacing all the lock inserts I wanted to set up an ad hoc video surveillance system for the time of my absence.
I had no time to wait for delivery, and it was supposed to be a one-time thing, so my first requirement was: (1) it should use only the hardware I already had. I wanted to have access to the recordings even if there was an incident and the local hardware was destroyed, so the next requirement was: (2) it should record straight to a remote server. I wanted to be able to check on the apartment while I&rsquo;m on vacation, so the third requirement was: (3) it should provide a live video feed accessible from my Android phone. In case something did happen I wanted to have access to the recordings, so the last requirement was: (4) it should provide remote access to video recordings from my phone.
Hardware My solution to requirement (1) was to use a laptop with a built-in webcam, connect a Logitech C270 USB webcam I had lying around and point it in the direction opposite to the built-in camera. The C270 has a convenient mount that allowed me to install it directly on the laptop&rsquo;s open lid. I placed the laptop in a good spot so that I could see the most critical spots like the main and patio doors through one camera or the other. The only thing left to do was to connect a charger and a network cable for improved reliability.
Stream publishing As for requirements (2) and (3), I settled for OBS Studio and nginx-mod-rtmp over WireGuard. After a rough internet search RTMP seemed like a stable and battle-tested streaming protocol. Also, the NGINX RTMP module was a convenient choice because it is available in the Alpine Linux package depositories and I already had NGINX running on the server. I used the following configuration:
rtmp { server { # Listen on a WireGuard interface listen 10.0.0.5:1935; application security { # Allow stream publishing and playing only from VPN allow publish 10.0.0.0/24; deny publish all; allow play 10.0.0.0/24; deny play all; # Enable live mode live on; # Fix mpv video player only playing audio wait_video on; # Record stream to files in 15-min chunks. record all; record_path /media/volume/security/; record_suffix -%Y-%m-%d-%H-%M-%S.flv; record_unique on; record_interval 15m; } } } On my laptop, I then created a scene in OBS Studio that contained feeds from both cameras. To publish the stream I went to Settings -&gt; Stream, selected a custom service and entered rtmp://10.0.0.5/security in the Server field. The stream key can be an arbitrary string, but the same string is later used for live playback.
Stream playing I tested the live feed on my PC by running
mpv &#39;rtmp://10.0.0.5/security/streamkey&#39; where streamkey was the stream key configured in OBS Studio. It worked fine, so I proceeded to viewing it on my Android phone.
The phone was already connected to the WireGuard network by WireGuard for Android, so to play the stream I just had to open the above URL in a video player that supports RTMP. At first, I tried mpv, but it had issues with displaying frames before the first received keyframe. My second choice, VLC, worked well.
Access to recordings Samba was my solution to requirement (4). With the following config I enabled access to the directory with recordings.
[security] path = /media/volume/security/ On the Android side my tried-and-true solution for accessing Samba shares is CIFS Documents Provider.
OBS Time Source While playing the live stream I noticed that it was lacking something. The video feed of home security cameras is mostly static, so it&rsquo;s difficult to perceive issues. To solve this I wanted to add an overlay that displays current date and time, so I can see the seconds change.
OBS Studio doesn&rsquo;t have the ability to do that out of the box. To my astonishment, the go-to solution for this seems to be a JavaScript program running in the OBS&rsquo;s built-in browser source. That&rsquo;s an awful lot of complexity literally just to render a clock.
After a bit of research I concluded that writing a date and time renderer for OBS Studio cannot be that hard. This is how OBS Time Source was created. It&rsquo;s a plugin that provides a new source that you can add to a scene to show current date and time. The format, font and color and outline can be configured in the source&rsquo;s properties.
Screenshot of OBS Time Source properties in OBS Studio
Misc The last task was to make sure the inside of the apartment is clearly visible during both day and night. For this purpose, I set up two 7 Watt LED lights to light the areas visible on the camera feeds. Setting the webcams&rsquo; frame rates to 10 FPS also helped to achieve a better exposition. I tested the setup by closing all the blinds to simulate nighttime. After some tweaking of the position and orientation of the lights, the video was exposed well.
To mitigate possible OBS crashes I ran it in a loop with the --startstreaming flag so that it restarts if it exits. I used the following shell command.
while true; do obs --startstreaming; sleep 1; done Summary This setup has worked well for the whole 14 days I was away. I successfully checked the live feed a couple of times and after coming back, I verified that the entire stream was correctly recorded. If you need a way to monitor your home for a short period of time, consider replicating this setup. If you do, please send me an email and tell me how it went.
</description>
    </item>
    
    <item>
      <title>Reverse engineering the Pulse-Eight CEC adapter</title>
      <link>https://krystianch.com/cec/</link>
      <pubDate>Wed, 19 Jul 2023 00:00:00 +0000</pubDate>
      
      <guid>https://krystianch.com/cec/</guid>
      <description>Consumer Electronics Control (CEC) is a feature of HDMI that allows devices to control each other. I use it to set my TV to standby mode when my living room PC is suspended and turn the TV back on when the PC is resumed. Graphics cards don&rsquo;t generally support CEC, but there is a solution to this problem. The Pulse-Eight USB - CEC Adapter functions as an HDMI passthrough and adds CEC support to a PC via USB.
Pulse-Eight USB - CEC Adapter
Pulse-Eight USB - CEC Adapter (top view)
The common way to interface with this adapter is libCEC and the cec-client program. Let&rsquo;s see how long it takes to send standby and power on commands to a TV.
$ echo &#34;standby 0&#34; | time cec-client --log-level 1 --single-command log level set to 1 opening a connection to the CEC adapter... real	0m 3.31s user	0m 0.00s sys	0m 0.01s $ echo &#34;on 0&#34; | time cec-client --log-level 1 --single-command log level set to 1 opening a connection to the CEC adapter... real	0m 3.31s user	0m 0.01s sys	0m 0.00s Changing the power state of the TV takes 3.3 s. This means that when the PC is resumed, the TV will turn on over 3 seconds after the system is ready. In practice this significantly increases frustration when starting to use the PC. In the case of my setup, the TV turns on after around 11 seconds after pressing a key to resume the PC. This means that it takes around 8 seconds until the PC is ready1 and 3 more seconds until the TV is on.
I could not believe that it has to take whole 3 seconds to send a command through the USB adapter. If I reduced this time to a negligible amount, the total system resume time would be cut by around 30%, which would be a significant improvement for me. Time to get hacking!
Investigating cec-client I wanted to check why exactly it takes so long to send a command. I ran cec-client with debug logs enabled.
Debug log of cec-client $ echo &#34;on 0&#34; | cec-client --single-command opening a connection to the CEC adapter... DEBUG: [ 1]	Broadcast (F): osd name set to &#39;Broadcast&#39; DEBUG: [ 1]	connection opened, clearing any previous input and waiting for active transmissions to end before starting DEBUG: [ 6]	communication thread started DEBUG: [ 61]	usbcec: enabling controlled mode NOTICE: [ 232]	connection opened DEBUG: [ 232]	&lt;&lt; Broadcast (F) -&gt; TV (0): POLL DEBUG: [ 232]	processor thread started TRAFFIC: [ 232]	&lt;&lt; f0 DEBUG: [ 232]	usbcec: updating line timeout: 3 DEBUG: [ 372]	&gt;&gt; POLL sent DEBUG: [ 372]	TV (0): device status changed into &#39;present&#39; DEBUG: [ 372]	&lt;&lt; requesting vendor ID of &#39;TV&#39; (0) TRAFFIC: [ 372]	&lt;&lt; f0:8c TRAFFIC: [ 619]	&gt;&gt; 0f:87:08:00:46 DEBUG: [ 619]	TV (0): vendor = Sony (080046) DEBUG: [ 619]	expected response received (87: device vendor id) DEBUG: [ 620]	registering new CEC client - v6.0.2 DEBUG: [ 620]	&gt;&gt; TV (0) -&gt; Broadcast (F): device vendor id (87) DEBUG: [ 674]	usbcec: autonomous mode = enabled DEBUG: [ 730]	usbcec: logical address = Recorder 1 DEBUG: [ 785]	usbcec: device type = recording device DEBUG: [ 790]	usbcec: logical address mask = 206 DEBUG: [ 860]	usbcec: physical address = 3000 DEBUG: [ 915]	usbcec: auto power on = disabled DEBUG: [ 915]	SetClientVersion - using client version &#39;6.0.2&#39; NOTICE: [ 915]	setting HDMI port to 1 on device TV (0) DEBUG: [ 915]	SetConfiguration: double tap timeout = 200ms, repeat rate = 0ms, release delay = 500ms DEBUG: [ 916]	detecting logical address for type &#39;recording device&#39; DEBUG: [ 916]	trying logical address &#39;Recorder 1&#39; DEBUG: [ 916]	&lt;&lt; Recorder 1 (1) -&gt; Recorder 1 (1): POLL TRAFFIC: [ 916]	&lt;&lt; 11 DEBUG: [ 1000]	CEC transmission - received response - TRANSMIT_FAILED_ACK TRAFFIC: [ 1000]	&lt;&lt; 11 DEBUG: [ 1085]	CEC transmission - received response - TRANSMIT_FAILED_ACK DEBUG: [ 1085]	&gt;&gt; POLL not sent DEBUG: [ 1085]	using logical address &#39;Recorder 1&#39; DEBUG: [ 1085]	Recorder 1 (1): device status changed into &#39;handled by libCEC&#39; DEBUG: [ 1085]	Recorder 1 (1): power status changed from &#39;unknown&#39; to &#39;on&#39; DEBUG: [ 1085]	Recorder 1 (1): vendor = Pulse Eight (001582) DEBUG: [ 1085]	Recorder 1 (1): CEC version 1.4 DEBUG: [ 1085]	AllocateLogicalAddresses - device &#39;0&#39;, type &#39;recording device&#39;, LA &#39;1&#39; DEBUG: [ 1085]	usbcec: updating ackmask: 0002 DEBUG: [ 1140]	Recorder 1 (1): osd name set to &#39;CECTester&#39; DEBUG: [ 1140]	Recorder 1 (1): menu language set to &#39;eng&#39; DEBUG: [ 1140]	GetPhysicalAddress - trying to get the physical address via ADL DEBUG: [ 1140]	GetPhysicalAddress - ADL returned physical address 0000 DEBUG: [ 1140]	GetPhysicalAddress - trying to get the physical address via nvidia driver DEBUG: [ 1140]	GetPhysicalAddress - nvidia driver returned physical address 0000 DEBUG: [ 1140]	GetPhysicalAddress - trying to get the physical address via drm files DEBUG: [ 1140]	GetPhysicalAddress - drm files returned physical address 3000 DEBUG: [ 1140]	using auto-detected physical address 3000 DEBUG: [ 1140]	Recorder 1 (1): physical address changed from ffff to 3000 DEBUG: [ 1140]	&lt;&lt; Recorder 1 (1) -&gt; broadcast (F): physical address 3000 TRAFFIC: [ 1140]	&lt;&lt; 1f:84:30:00:01 NOTICE: [ 1305]	CEC client registered: libCEC version = 6.0.2, client version = 6.0.2, firmware version = 12, firmware build date: Tue Apr 28 14:20:49 2020 +0000, logical address(es) = Recorder 1 (1) , physical address: 3.0.0.0, git revision: &lt;unknown&gt;, compiled on 2022-11-22 19:33:35 by buildozer@localhost on Linux 5.15.12-0-lts (x86_64), features: P8_USB, DRM, P8_detect, randr, Linux DEBUG: [ 1305]	&lt;&lt; Recorder 1 (1) -&gt; TV (0): OSD name &#39;CECTester&#39; TRAFFIC: [ 1305]	&lt;&lt; 10:47:43:45:43:54:65:73:74:65:72 TRAFFIC: [ 1339]	&gt;&gt; 01:46 DEBUG: [ 1339]	&gt;&gt; TV (0) -&gt; Recorder 1 (1): give osd name (46) DEBUG: [ 1616]	&lt;&lt; requesting power status of &#39;TV&#39; (0) TRAFFIC: [ 1616]	&lt;&lt; 10:8f DEBUG: [ 1616]	&lt;&lt; Recorder 1 (1) -&gt; TV (0): OSD name &#39;CECTester&#39; TRAFFIC: [ 1671]	&lt;&lt; 10:47:43:45:43:54:65:73:74:65:72 TRAFFIC: [ 2042]	&gt;&gt; 01:8c DEBUG: [ 2042]	&lt;&lt; Recorder 1 (1) -&gt; TV (0): vendor id Pulse Eight (1582) TRAFFIC: [ 2042]	&lt;&lt; 1f:87:00:15:82 DEBUG: [ 2042]	&gt;&gt; TV (0) -&gt; Recorder 1 (1): give device vendor id (8C) TRAFFIC: [ 2112]	&gt;&gt; 01:90:00 DEBUG: [ 2112]	TV (0): power status changed from &#39;unknown&#39; to &#39;on&#39; DEBUG: [ 2112]	expected response received (90: report power status) DEBUG: [ 2112]	&gt;&gt; TV (0) -&gt; Recorder 1 (1): report power status (90) NOTICE: [ 2112]	&lt;&lt; powering on &#39;TV&#39; (0) TRAFFIC: [ 2112]	&lt;&lt; 10:04 DEBUG: [ 2305]	unregistering all CEC clients NOTICE: [ 2305]	unregistering client: libCEC version = 6.0.2, client version = 6.0.2, firmware version = 12, firmware build date: Tue Apr 28 14:20:49 2020 +0000, logical address(es) = Recorder 1 (1) , physical address: 3.0.0.0, git revision: &lt;unknown&gt;, compiled on 2022-11-22 19:33:35 by buildozer@localhost on Linux 5.15.12-0-lts (x86_64), features: P8_USB, DRM, P8_detect, randr, Linux DEBUG: [ 2305]	Recorder 1 (1): power status changed from &#39;on&#39; to &#39;unknown&#39; DEBUG: [ 2305]	Recorder 1 (1): vendor = Unknown (000000) DEBUG: [ 2305]	Recorder 1 (1): CEC version unknown DEBUG: [ 2305]	Recorder 1 (1): osd name set to &#39;Recorder 1&#39; DEBUG: [ 2305]	Recorder 1 (1): device status changed into &#39;unknown&#39; DEBUG: [ 2305]	usbcec: updating ackmask: 0000 DEBUG: [ 2360]	usbcec: disabling controlled mode DEBUG: [ 2415]	unregistering all CEC clients DEBUG: [ 3247]	communication thread ended There&rsquo;s a lot of stuff there. First, notice the number before each log message. It&rsquo;s the number of milliseconds elapsed since the program started. This can be used to pinpoint parts that take the most time to execute.
We can see that the power on command is not sent until over 2 seconds (!) into program execution. So what is going on before that? There are things like retrieving the vendor ID and power status of the TV and setting the OSD name. This takes a lot of time and is useless to me. After examining the source code of cec-client it looked like most of this stuff happens in ICECAdapter::Open. Another conclusion was that the code is very complicated and hard to read. There are abstractions over abstractions, and it gets difficult to follow the execution path. I needed another way.
Reverse engineering the protocol While looking at the source code for libCEC and cec-client I noticed that the adapter behaves like a serial port and is present in my system as /dev/ttyACM0. A quick search for baud in libCEC revealed that the baudrate is 38400. The next task was to sniff the serial communication between cec-client and the adapter. One internet search later I had found slsnif and Andrew Ruder&rsquo;s fork, which looked like just the tool I needed. I built and ran slsnif and then ran cec-client.
$ slsnif --nolock --log log --speed 38400 --unix98 /dev/ttyACM0 &amp; Serial Line Sniffer. Version 0.4.4 Copyright (C) 2001 Yan Gurtovoy (ymg@dakotacom.net) Started logging data into file &#39;log&#39;. Opened pty: /dev/pts/0 Failed to open temp. file to save opened pty name, continuing...: Permission denied Opened port: /dev/ttyACM0 Baudrate is set to 38400 baud. $ echo &#34;on 0&#34; | cec-client --log-level 1 --single-command /dev/pts/0 log level set to 1 opening a connection to the CEC adapter... Synchronizing ports...Done! $ kill %1 [1]+ Terminated ./slsnif --nolock --log log --speed 38400 --unix98 /dev/ttyACM0 Log contents Host --&gt; (255) &lt;SOH&gt; (001) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;SOH&gt; (001) (254) Host --&gt; (255) &lt;NAK&gt; (021) (254) Device --&gt; (255) &lt;NAK&gt; (021) &lt;NUL&gt; (000) &lt;FF&gt; (012) (254) Host --&gt; (255) &lt;CAN&gt; (024) &lt;SOH&gt; (001) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;CAN&gt; (024) (254) Host --&gt; (255) &lt;ETB&gt; (023) (254) Device --&gt; (255) &lt;ETB&gt; (023) ^ (094) (168) ; (059) (193) (254) Host --&gt; (255) ( (040) (254) Device --&gt; (255) ( (040) &lt;SOH&gt; (001) (254) Host --&gt; (255) &lt;CR&gt; (013) &lt;ETX&gt; (003) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;CR&gt; (013) (254) Host --&gt; (255) &lt;SO&gt; (014) &lt;NUL&gt; (000) (254) (255) &lt;FF&gt; (012) (240) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;SO&gt; (014) (254) (255) &lt;BS&gt; (008) &lt;FF&gt; (012) (254) Device --&gt; (255) &lt;DLE&gt; (016) (254) Host --&gt; (255) &lt;SO&gt; (014) &lt;NUL&gt; (000) (254) (255) &lt;VT&gt; (011) (240) (254) (255) &lt;FF&gt; (012) (140) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;SO&gt; (014) (254) (255) &lt;BS&gt; (008) &lt;VT&gt; (011) (254) (255) &lt;BS&gt; (008) &lt;FF&gt; (012) (254) Device --&gt; (255) &lt;DLE&gt; (016) (254) Device --&gt; (255) E (069) &lt;SI&gt; (015) (254) Device --&gt; (255) F (070) (135) (254) Device --&gt; (255) F (070) &lt;BS&gt; (008) (254) Device --&gt; (255) F (070) &lt;NUL&gt; (000) (254) Device --&gt; (255) (198) F (070) (254) Host --&gt; (255) &lt;EM&gt; (025) (254) Device --&gt; (255) &lt;EM&gt; (025) &lt;SOH&gt; (001) (254) Host --&gt; (255) &lt;ESC&gt; (027) (254) Device --&gt; (255) &lt;ESC&gt; (027) &lt;SOH&gt; (001) (254) Host --&gt; (255) ! (033) (254) Device --&gt; (255) ! (033) &lt;SOH&gt; (001) (254) Host --&gt; (255) &lt;GS&gt; (029) (254) Device --&gt; (255) &lt;GS&gt; (029) &lt;STX&gt; (002) &lt;ACK&gt; (006) (254) Host --&gt; (255) % (037) (254) Device --&gt; (255) % (037) C (067) E (069) C (067) T (084) e (101) s (115) t (116) e (101) r (114) (254) Host --&gt; (255) &lt;US&gt; (031) (254) Device --&gt; (255) &lt;US&gt; (031) 0 (048) &lt;NUL&gt; (000) (254) Host --&gt; (255) * (042) (254) Device --&gt; (255) * (042) &lt;NUL&gt; (000) (254) Host --&gt; (255) &lt;SO&gt; (014) &lt;NUL&gt; (000) (254) (255) &lt;FF&gt; (012) &lt;DC1&gt; (017) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;SO&gt; (014) (254) (255) &lt;BS&gt; (008) &lt;FF&gt; (012) (254) Device --&gt; (255) &lt;DC2&gt; (018) (254) Host --&gt; (255) &lt;SO&gt; (014) &lt;NUL&gt; (000) (254) (255) &lt;FF&gt; (012) &lt;DC1&gt; (017) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;SO&gt; (014) (254) (255) &lt;BS&gt; (008) &lt;FF&gt; (012) (254) Device --&gt; (255) &lt;DC2&gt; (018) (254) Host --&gt; (255) &lt;LF&gt; (010) &lt;NUL&gt; (000) &lt;STX&gt; (002) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;LF&gt; (010) (254) Host --&gt; (255) &lt;SO&gt; (014) &lt;SOH&gt; (001) (254) (255) &lt;VT&gt; (011) &lt;US&gt; (031) (254) (255) &lt;VT&gt; (011) (132) (254) (255) &lt;VT&gt; (011) 0 (048) (254) (255) &lt;VT&gt; (011) &lt;NUL&gt; (000) (254) (255) &lt;FF&gt; (012) &lt;SOH&gt; (001) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;SO&gt; (014) (254) (255) &lt;BS&gt; (008) &lt;VT&gt; (011) (254) (255) &lt;BS&gt; (008) &lt;VT&gt; (011) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;VT&gt; (011) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;VT&gt; (011) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;FF&gt; (012) (254) Device --&gt; (255) &lt;DLE&gt; (016) (254) Host --&gt; (255) &lt;SO&gt; (014) &lt;NUL&gt; (000) (254) (255) &lt;VT&gt; (011) &lt;DLE&gt; (016) (254) (255) &lt;VT&gt; (011) G (071) (254) (255) &lt;VT&gt; (011) C (067) (254) (255) &lt;VT&gt; (011) E (069) (254) (255) &lt;VT&gt; (011) C (067) (254) (255) &lt;VT&gt; (011) T (084) (254) (255) &lt;VT&gt; (011) e (101) (254) Host --&gt; (255) &lt;VT&gt; (011) s (115) (254) (255) &lt;VT&gt; (011) t (116) (254) (255) &lt;VT&gt; (011) e (101) (254) (255) &lt;FF&gt; (012) r (114) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;SO&gt; (014) (254) (255) &lt;BS&gt; (008) &lt;VT&gt; (011) (254) Device --&gt; (255) &lt;ENQ&gt; (005) &lt;SOH&gt; (001) (254) Device --&gt; (255) (134) F (070) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;VT&gt; (011) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;VT&gt; (011) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;VT&gt; (011) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;VT&gt; (011) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;VT&gt; (011) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;VT&gt; (011) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;VT&gt; (011) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;VT&gt; (011) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;VT&gt; (011) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;FF&gt; (012) (254) Device --&gt; (255) &lt;DLE&gt; (016) (254) Host --&gt; (255) &lt;SO&gt; (014) &lt;NUL&gt; (000) (254) (255) &lt;VT&gt; (011) &lt;DLE&gt; (016) (254) (255) &lt;FF&gt; (012) (143) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;SO&gt; (014) (254) (255) &lt;BS&gt; (008) &lt;VT&gt; (011) (254) (255) &lt;BS&gt; (008) &lt;FF&gt; (012) (254) Device --&gt; (255) &lt;DLE&gt; (016) (254) Host --&gt; (255) &lt;SO&gt; (014) &lt;NUL&gt; (000) (254) (255) &lt;VT&gt; (011) &lt;DLE&gt; (016) (254) (255) &lt;VT&gt; (011) G (071) (254) (255) &lt;VT&gt; (011) C (067) (254) (255) &lt;VT&gt; (011) E (069) (254) (255) &lt;VT&gt; (011) C (067) (254) (255) &lt;VT&gt; (011) T (084) (254) (255) &lt;VT&gt; (011) e (101) (254) Host --&gt; (255) &lt;VT&gt; (011) s (115) (254) (255) &lt;VT&gt; (011) t (116) (254) (255) &lt;VT&gt; (011) e (101) (254) (255) &lt;FF&gt; (012) r (114) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;SO&gt; (014) (254) (255) &lt;BS&gt; (008) &lt;VT&gt; (011) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;VT&gt; (011) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;VT&gt; (011) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;VT&gt; (011) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;VT&gt; (011) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;VT&gt; (011) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;VT&gt; (011) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;VT&gt; (011) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;VT&gt; (011) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;VT&gt; (011) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;FF&gt; (012) (254) Device --&gt; (255) &lt;DLE&gt; (016) (254) Device --&gt; (255) &lt;ENQ&gt; (005) &lt;SOH&gt; (001) (254) Device --&gt; (255) (134) (140) (254) Host --&gt; (255) &lt;SO&gt; (014) &lt;SOH&gt; (001) (254) (255) &lt;VT&gt; (011) &lt;US&gt; (031) (254) (255) &lt;VT&gt; (011) (135) (254) (255) &lt;VT&gt; (011) &lt;NUL&gt; (000) (254) (255) &lt;VT&gt; (011) &lt;NAK&gt; (021) (254) (255) &lt;FF&gt; (012) (130) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;SO&gt; (014) (254) (255) &lt;BS&gt; (008) &lt;VT&gt; (011) (254) Device --&gt; (255) &lt;ENQ&gt; (005) &lt;SOH&gt; (001) (254) Device --&gt; (255) &lt;ACK&gt; (006) (144) (254) Device --&gt; (255) (134) &lt;NUL&gt; (000) (254) Host --&gt; (255) &lt;SO&gt; (014) &lt;NUL&gt; (000) (254) (255) &lt;VT&gt; (011) &lt;DLE&gt; (016) (254) (255) &lt;FF&gt; (012) &lt;EOT&gt; (004) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;VT&gt; (011) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;VT&gt; (011) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;VT&gt; (011) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;FF&gt; (012) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;SO&gt; (014) (254) (255) &lt;BS&gt; (008) &lt;VT&gt; (011) (254) Device --&gt; (255) &lt;DLE&gt; (016) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;FF&gt; (012) (254) Device --&gt; (255) &lt;DLE&gt; (016) (254) Host --&gt; (255) &lt;LF&gt; (010) &lt;NUL&gt; (000) &lt;NUL&gt; (000) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;LF&gt; (010) (254) Host --&gt; (255) &lt;CAN&gt; (024) &lt;NUL&gt; (000) (254) Device --&gt; (255) &lt;BS&gt; (008) &lt;CAN&gt; (024) (254) I was disappointed to see that it wasn&rsquo;t a text protocol but straight away I noticed some patterns. It seemed that messages start with a byte of value 255, end with 254 and that each payload consists of only a few bytes, often just one. In libCEC&rsquo;s source code I found that the first byte of a payload is the message code and the following bytes are the parameters. There was also a table of message codes there.
Table of message codes typedef enum cec_adapter_messagecode { MSGCODE_NOTHING = 0, MSGCODE_PING, MSGCODE_TIMEOUT_ERROR, MSGCODE_HIGH_ERROR, MSGCODE_LOW_ERROR, MSGCODE_FRAME_START, MSGCODE_FRAME_DATA, MSGCODE_RECEIVE_FAILED, MSGCODE_COMMAND_ACCEPTED, MSGCODE_COMMAND_REJECTED, MSGCODE_SET_ACK_MASK, MSGCODE_TRANSMIT, MSGCODE_TRANSMIT_EOM, MSGCODE_TRANSMIT_IDLETIME, MSGCODE_TRANSMIT_ACK_POLARITY, MSGCODE_TRANSMIT_LINE_TIMEOUT, MSGCODE_TRANSMIT_SUCCEEDED, MSGCODE_TRANSMIT_FAILED_LINE, MSGCODE_TRANSMIT_FAILED_ACK, MSGCODE_TRANSMIT_FAILED_TIMEOUT_DATA, MSGCODE_TRANSMIT_FAILED_TIMEOUT_LINE, MSGCODE_FIRMWARE_VERSION, MSGCODE_START_BOOTLOADER, MSGCODE_GET_BUILDDATE, MSGCODE_SET_CONTROLLED, MSGCODE_GET_AUTO_ENABLED, MSGCODE_SET_AUTO_ENABLED, MSGCODE_GET_DEFAULT_LOGICAL_ADDRESS, MSGCODE_SET_DEFAULT_LOGICAL_ADDRESS, MSGCODE_GET_LOGICAL_ADDRESS_MASK, MSGCODE_SET_LOGICAL_ADDRESS_MASK, MSGCODE_GET_PHYSICAL_ADDRESS, MSGCODE_SET_PHYSICAL_ADDRESS, MSGCODE_GET_DEVICE_TYPE, MSGCODE_SET_DEVICE_TYPE, MSGCODE_GET_HDMI_VERSION, MSGCODE_SET_HDMI_VERSION, MSGCODE_GET_OSD_NAME, MSGCODE_SET_OSD_NAME, MSGCODE_WRITE_EEPROM, MSGCODE_GET_ADAPTER_TYPE, MSGCODE_SET_ACTIVE_SOURCE, MSGCODE_GET_AUTO_POWER_ON, MSGCODE_SET_AUTO_POWER_ON, MSGCODE_FRAME_EOM = 0x80, MSGCODE_FRAME_ACK = 0x40, } cec_adapter_messagecode; I wrote a quick Python script to decode the slsnif log using this table. It&rsquo;s available here. And here&rsquo;s the decoded log.
Decoded log OUT PING (0x01) args IN COMMAND_ACCEPTED (0x08) args 0x01 OUT FIRMWARE_VERSION (0x15) args IN FIRMWARE_VERSION (0x15) args 0x00 0x0c OUT SET_CONTROLLED (0x18) args 0x01 IN COMMAND_ACCEPTED (0x08) args 0x18 OUT GET_BUILDDATE (0x17) args IN GET_BUILDDATE (0x17) args 0x5e 0xa8 0x3b 0xc1 OUT GET_ADAPTER_TYPE (0x28) args IN GET_ADAPTER_TYPE (0x28) args 0x01 OUT TRANSMIT_IDLETIME (0x0d) args 0x03 IN COMMAND_ACCEPTED (0x08) args 0x0d OUT TRANSMIT_ACK_POLARITY (0x0e) args 0x00 OUT TRANSMIT_EOM (0x0c) args 0xf0 IN COMMAND_ACCEPTED (0x08) args 0x0e IN COMMAND_ACCEPTED (0x08) args 0x0c IN TRANSMIT_SUCCEEDED (0x10) args OUT TRANSMIT_ACK_POLARITY (0x0e) args 0x00 OUT TRANSMIT (0x0b) args 0xf0 OUT TRANSMIT_EOM (0x0c) args 0x8c IN COMMAND_ACCEPTED (0x08) args 0x0e IN COMMAND_ACCEPTED (0x08) args 0x0b IN COMMAND_ACCEPTED (0x08) args 0x0c IN TRANSMIT_SUCCEEDED (0x10) args IN FRAME_START ACK (0x05) args 0x0f IN FRAME_DATA ACK (0x06) args 0x87 IN FRAME_DATA ACK (0x06) args 0x08 IN FRAME_DATA ACK (0x06) args 0x00 IN FRAME_DATA ACK EOM (0x06) args 0x46 OUT GET_AUTO_ENABLED (0x19) args IN GET_AUTO_ENABLED (0x19) args 0x01 OUT GET_DEFAULT_LOGICAL_ADDRESS (0x1b) args IN GET_DEFAULT_LOGICAL_ADDRESS (0x1b) args 0x01 OUT GET_DEVICE_TYPE (0x21) args IN GET_DEVICE_TYPE (0x21) args 0x01 OUT GET_LOGICAL_ADDRESS_MASK (0x1d) args IN GET_LOGICAL_ADDRESS_MASK (0x1d) args 0x02 0x06 OUT GET_OSD_NAME (0x25) args IN GET_OSD_NAME (0x25) args 0x43 0x45 0x43 0x54 0x65 0x73 0x74 0x65 0x72 OUT GET_PHYSICAL_ADDRESS (0x1f) args IN GET_PHYSICAL_ADDRESS (0x1f) args 0x30 0x00 OUT GET_AUTO_POWER_ON (0x2a) args IN GET_AUTO_POWER_ON (0x2a) args 0x00 OUT TRANSMIT_ACK_POLARITY (0x0e) args 0x00 OUT TRANSMIT_EOM (0x0c) args 0x11 IN COMMAND_ACCEPTED (0x08) args 0x0e IN COMMAND_ACCEPTED (0x08) args 0x0c IN TRANSMIT_FAILED_ACK (0x12) args OUT TRANSMIT_ACK_POLARITY (0x0e) args 0x00 OUT TRANSMIT_EOM (0x0c) args 0x11 IN COMMAND_ACCEPTED (0x08) args 0x0e IN COMMAND_ACCEPTED (0x08) args 0x0c IN TRANSMIT_FAILED_ACK (0x12) args OUT SET_ACK_MASK (0x0a) args 0x00 0x02 IN COMMAND_ACCEPTED (0x08) args 0x0a OUT TRANSMIT_ACK_POLARITY (0x0e) args 0x01 OUT TRANSMIT (0x0b) args 0x1f OUT TRANSMIT (0x0b) args 0x84 OUT TRANSMIT (0x0b) args 0x30 OUT TRANSMIT (0x0b) args 0x00 OUT TRANSMIT_EOM (0x0c) args 0x01 IN COMMAND_ACCEPTED (0x08) args 0x0e IN COMMAND_ACCEPTED (0x08) args 0x0b IN COMMAND_ACCEPTED (0x08) args 0x0b IN COMMAND_ACCEPTED (0x08) args 0x0b IN COMMAND_ACCEPTED (0x08) args 0x0b IN COMMAND_ACCEPTED (0x08) args 0x0c IN TRANSMIT_SUCCEEDED (0x10) args OUT TRANSMIT_ACK_POLARITY (0x0e) args 0x00 OUT TRANSMIT (0x0b) args 0x10 OUT TRANSMIT (0x0b) args 0x47 OUT TRANSMIT (0x0b) args 0x43 OUT TRANSMIT (0x0b) args 0x45 OUT TRANSMIT (0x0b) args 0x43 OUT TRANSMIT (0x0b) args 0x54 OUT TRANSMIT (0x0b) args 0x65 OUT TRANSMIT (0x0b) args 0x73 OUT TRANSMIT (0x0b) args 0x74 OUT TRANSMIT (0x0b) args 0x65 OUT TRANSMIT_EOM (0x0c) args 0x72 IN COMMAND_ACCEPTED (0x08) args 0x0e IN COMMAND_ACCEPTED (0x08) args 0x0b IN FRAME_START (0x05) args 0x01 IN FRAME_DATA EOM (0x06) args 0x46 IN COMMAND_ACCEPTED (0x08) args 0x0b IN COMMAND_ACCEPTED (0x08) args 0x0b IN COMMAND_ACCEPTED (0x08) args 0x0b IN COMMAND_ACCEPTED (0x08) args 0x0b IN COMMAND_ACCEPTED (0x08) args 0x0b IN COMMAND_ACCEPTED (0x08) args 0x0b IN COMMAND_ACCEPTED (0x08) args 0x0b IN COMMAND_ACCEPTED (0x08) args 0x0b IN COMMAND_ACCEPTED (0x08) args 0x0b IN COMMAND_ACCEPTED (0x08) args 0x0c IN TRANSMIT_SUCCEEDED (0x10) args OUT TRANSMIT_ACK_POLARITY (0x0e) args 0x00 OUT TRANSMIT (0x0b) args 0x10 OUT TRANSMIT_EOM (0x0c) args 0x8f IN COMMAND_ACCEPTED (0x08) args 0x0e IN COMMAND_ACCEPTED (0x08) args 0x0b IN COMMAND_ACCEPTED (0x08) args 0x0c IN TRANSMIT_SUCCEEDED (0x10) args OUT TRANSMIT_ACK_POLARITY (0x0e) args 0x00 OUT TRANSMIT (0x0b) args 0x10 OUT TRANSMIT (0x0b) args 0x47 OUT TRANSMIT (0x0b) args 0x43 OUT TRANSMIT (0x0b) args 0x45 OUT TRANSMIT (0x0b) args 0x43 OUT TRANSMIT (0x0b) args 0x54 OUT TRANSMIT (0x0b) args 0x65 OUT TRANSMIT (0x0b) args 0x73 OUT TRANSMIT (0x0b) args 0x74 OUT TRANSMIT (0x0b) args 0x65 OUT TRANSMIT_EOM (0x0c) args 0x72 IN COMMAND_ACCEPTED (0x08) args 0x0e IN COMMAND_ACCEPTED (0x08) args 0x0b IN COMMAND_ACCEPTED (0x08) args 0x0b IN COMMAND_ACCEPTED (0x08) args 0x0b IN COMMAND_ACCEPTED (0x08) args 0x0b IN COMMAND_ACCEPTED (0x08) args 0x0b IN COMMAND_ACCEPTED (0x08) args 0x0b IN COMMAND_ACCEPTED (0x08) args 0x0b IN COMMAND_ACCEPTED (0x08) args 0x0b IN COMMAND_ACCEPTED (0x08) args 0x0b IN COMMAND_ACCEPTED (0x08) args 0x0b IN COMMAND_ACCEPTED (0x08) args 0x0c IN TRANSMIT_SUCCEEDED (0x10) args IN FRAME_START (0x05) args 0x01 IN FRAME_DATA EOM (0x06) args 0x8c OUT TRANSMIT_ACK_POLARITY (0x0e) args 0x01 OUT TRANSMIT (0x0b) args 0x1f OUT TRANSMIT (0x0b) args 0x87 OUT TRANSMIT (0x0b) args 0x00 OUT TRANSMIT (0x0b) args 0x15 OUT TRANSMIT_EOM (0x0c) args 0x82 IN COMMAND_ACCEPTED (0x08) args 0x0e IN COMMAND_ACCEPTED (0x08) args 0x0b IN FRAME_START (0x05) args 0x01 IN FRAME_DATA (0x06) args 0x90 IN FRAME_DATA EOM (0x06) args 0x00 OUT TRANSMIT_ACK_POLARITY (0x0e) args 0x00 OUT TRANSMIT (0x0b) args 0x10 OUT TRANSMIT_EOM (0x0c) args 0x04 IN COMMAND_ACCEPTED (0x08) args 0x0b IN COMMAND_ACCEPTED (0x08) args 0x0b IN COMMAND_ACCEPTED (0x08) args 0x0b IN COMMAND_ACCEPTED (0x08) args 0x0c IN COMMAND_ACCEPTED (0x08) args 0x0e IN COMMAND_ACCEPTED (0x08) args 0x0b IN TRANSMIT_SUCCEEDED (0x10) args IN COMMAND_ACCEPTED (0x08) args 0x0c IN TRANSMIT_SUCCEEDED (0x10) args OUT SET_ACK_MASK (0x0a) args 0x00 0x00 IN COMMAND_ACCEPTED (0x08) args 0x0a OUT SET_CONTROLLED (0x18) args 0x00 IN COMMAND_ACCEPTED (0x08) args 0x18 I found out that the TRANSMIT messages cause actual CEC transmissions. Now I had to pinpoint the ones that turn the TV on. I compared the decoded log with the cec-client debug log and found the correct lines.
OUT TRANSMIT_ACK_POLARITY (0x0e) args 0x00 OUT TRANSMIT (0x0b) args 0x10 OUT TRANSMIT_EOM (0x0c) args 0x04 IN COMMAND_ACCEPTED (0x08) args 0x0e IN COMMAND_ACCEPTED (0x08) args 0x0b IN COMMAND_ACCEPTED (0x08) args 0x0c IN TRANSMIT_SUCCEEDED (0x10) args NOTICE: [ 2112]	&lt;&lt; powering on &#39;TV&#39; (0) TRAFFIC: [ 2112]	&lt;&lt; 10:04 It seemed that sending 10:04 to the CEC bus causes the TV to turn on. And indeed, I pasted this frame into CEC-O-MATIC CEC decoder and got:
source: Recording 1 destination: TV End-user features &gt; One Touch Play &gt; Image View On From the description it looked to me like a correct message that would turn on the TV. I sent these messages to the CEC adapter with Python and pySerial but after sending the first command, it was immediately rejected (9 = COMMAND_REJECTED).
$ python3 Python 3.11.4 (main, Jun 9 2023, 02:29:05) [GCC 12.2.1 20220924] on linux Type &#34;help&#34;, &#34;copyright&#34;, &#34;credits&#34; or &#34;license&#34; for more information. &gt;&gt;&gt; import serial &gt;&gt;&gt; ser = serial.Serial(&#34;/dev/ttyACM0&#34;, 38400, timeout=2) &gt;&gt;&gt; ser.write(b&#34;\xff\x0e\x00\xfe&#34;) 4 &gt;&gt;&gt; list(ser.read_until(b&#34;\xfe&#34;)) [255, 9, 14, 254] I figured I had to send more than just the transmission messages. After a quick glance at the decoded log, SET_CONTROLLED drew my attention. I sent a PING, then SET_CONTROLLED 1, then TRANSMIT_ACK_POLARITY, TRANSMIT, TRANSMIT_EOM and finally SET_CONTROLLED 0. That did the trick. My TV turned on. Now what was left to do was to find out the correct standby frame by repeating these steps for the cec-client&rsquo;s standby command (it turned out to be 10:36), write a tool I could use to control the power state of my TV, measure how much time it takes to do it and hope it&rsquo;s significantly faster than cec-client.
New tool I gathered all the information I had and wrote a Python script that could set the TV to standby mode or turn it on. I named it Fast CEC and you can find it here. Let&rsquo;s see how fast it is.
~ $ time fastcec /dev/ttyACM0 on real	0m 0.09s user	0m 0.01s sys	0m 0.00s ~ $ time fastcec /dev/ttyACM0 standby real	0m 0.09s user	0m 0.02s sys	0m 0.00s Fast CEC at 0.09 s is 37 times faster than cec-client at 3.31 s. I consider this endevour a success not only because of the significant speedup so that my TV turns on a bit faster. I also really enjoyed learning about CEC, analyzing libCEC and the adapter protocol and putting this all together into a useful tool.
I noticed that disconnecting two hard drives reduces PC resume time from 8 to 4.5 seconds. I might investigate this in the future.&#160;&#x21a9;&#xfe0e;
</description>
    </item>
    
    <item>
      <title>Simple home security video system</title>
      <link>https://krystianch.com/security/</link>
      <pubDate>Sun, 09 Apr 2023 19:36:58 +0200</pubDate>
      
      <guid>https://krystianch.com/security/</guid>
      <description>This page covers setting up a simple multicamera home security system with two features:
saving video material to disk, live streaming through a web browser. Live view.
Camera box rev. 1
Camera box rev. 2
The system consists of multiple Raspberry Pi devices connected to a common local network. It&rsquo;s recommended but not required to make a separate home network for this purpose. One device called the hub receives video streams and produces seekable HLS streams and at the same time saves video segments to disk. Other devices called camera devices transmit video streams from a camera to the hub device. The idea is that the hub device is installed inside and camera devices are mounted outside in electrical boxes with Ethernet and PoE.
I have a system like this deployed with 2 camera devices and 1 hub device. It&rsquo;s working well.
Camera devices Repeat the following steps for every camera device.
Hardware Parts:
a Raspberry Pi board, any generation, model B (with Ethernet) a microSD memory card mini radiators (optional) a Raspberry Pi Camera Module (possibly NoIR) with ribbon a RPi Camera mount (or standard ball mount and custom rig for the camera) M5 standoffs and screws a passive PoE injector/switch a 5V-3A-capable step-down converter board (e.g. LM2596) a bit of stranded copper wire with a 1x2 goldpin receptacle a fuse holder and a fuse (current rating depends on PoE voltage) an outdoor-rated RJ-45 receptacle a lot of network cable and plugs (cat. 5 unshielded is probably enough) an electrical box Tools:
drill small phillips head screwdriver small flathead screwdriver soldering iron and solder multimeter needle-nose pliers RJ-45 terminator tools for mounting the elecrical box in desired place Wiring: TODO
Software We will use the firmware interface for communication with the Raspberry Pi camera. It&rsquo;s because the new blob-free libcamera interface is still experimental and I wasn&rsquo;t able to get it to work on Alpine Linux.
Download files with firmware blobs that contain camera drivers and codecs. But first, to edit OS files, the partition needs to be remounted read-write.
mount /media/mmcblk0p1 -o rw,remount apk add curl curl &#39;https://raw.githubusercontent.com/raspberrypi/firmware/master/boot/start_x.elf&#39; &gt; /media/mmcblk0p1/start_x.elf curl &#39;https://raw.githubusercontent.com/raspberrypi/firmware/master/boot/fixup_x.dat&#39; &gt; /media/mmcblk0p1/fixup_x.dat Instruct to load firmware to the VideoCore GPU before boot.
echo &#34;start_file=start_x.elf&#34; &gt;&gt; /media/mmcblk0p1/config.txt echo &#34;fixup_file=fixup_x.dat&#34; &gt;&gt; /media/mmcblk0p1/config.txt If your camera is already connected, just reboot. Of not, poweroff, disconnect power, connect camera and connect power.
Verification After reboot, verify if you are able to obtain images from the camera.
apk add raspberrypi /opt/vc/bin/raspivid -t 0 You should be able to see a video feed via Pi&rsquo;s HDMI.
Raspivid service We will use raspivid&rsquo;s tcp output to send live video to the camera hub device.
Create /etc/init.d/raspivid with the following contents.
#!/sbin/openrc-run name=raspivid command=&#34;/opt/vc/bin/$name&#34; command_args=&#34;$raspivid_opts&#34; command_user=&#34;raspivid:raspivid&#34; supervisor=supervise-daemon respawn_delay=1 respawn_max=0 depend() { need net after firewall } start_pre() { [ -n &#34;$output_log&#34; ] &amp;&amp; checkpath -f &#34;$output_log&#34; \ -m 644 -o raspivid:raspivid [ -n &#34;$error_log&#34; ] &amp;&amp; checkpath -f &#34;$error_log&#34; \ -m 644 -o raspivid:raspivid } Make it executable and add this file to LBU.
chmod +x /etc/init.d/raspivid lbu add /etc/init.d/raspivid Create /etc/conf.d/raspivid with the following contents. These parameters can be tuned. Set the --output to the address of your hub device and adjust the --annotate option to reflect the name of the camera.
The H264 encoder that raspivid uses is limited to 1920x1080 px resolution. In order to use higher resolutions one needs to use MJPEG instead.
raspivid_opts=&#34; \ --nopreview \ --output tcp://192.168.1.10:8083 \ --timeout 0 \ --flush \ --codec H264 \ --rotation 180 \ --mode 4 \ --annotate $(( 1024 | 8 | 4 )) \ --annotate &#39;%Y-%m-%d %X ROAD&#39; \ --bitrate 0 \ --width 1640 \ --height 1232 \ --qp 25 \ --framerate 15 \ &#34; output_log=/var/log/raspivid.log error_log=/var/log/raspivid.log Add required raspivid user and add it to video group.
addgroup -S raspivid adduser -SDh/dev/null -s/sbin/nologin -Graspivid -graspivid raspivid raspivid adduser raspivid video Start the service, enable starting it on boot and save changes.
rc-service raspivid start rc-update add raspivid lbu commit -d Hub devices Firewall Allow traffic on the following ports in the firewall configuration:
TCP: 8081, 8082, 8083, &hellip; (for camera devices) TCP: 139, 445; UDP: 137, 138 (for samba) TCP: 80 (for http) FFmpeg service Create /etc/init.d/homesec with mode 755:
#!/sbin/openrc-run name=homesec command=&#34;/usr/bin/ffmpeg&#34; command_args=&#34;$ffmpeg_opts&#34; command_user=&#34;homesec:homesec&#34; supervisor=supervise-daemon respawn_delay=1 respawn_max=0 depend() { need net after firewall } start_pre() { [ -n &#34;$output_log&#34; ] &amp;&amp; checkpath -f &#34;$output_log&#34; \ -m 644 -o homesec:homesec [ -n &#34;$error_log&#34; ] &amp;&amp; checkpath -f &#34;$error_log&#34; \ -m 644 -o homesec:homesec } Create user homesec and group homesec.
For each camera create /etc/conf.d/homesec-cameraname:
ffmpeg_opts=&#34;-loglevel warning \ -r 15 \ -i tcp://192.168.1.10:8082?listen \ -c copy \ -f segment \ -segment_time 00:15:00 \ -segment_atclocktime 1 \ -strftime 1 \ /media/hdd/recordings/%Y-%m-%dT%H:%M:%S_cameraname.h264 \ -c copy \ -hls_time 1 \ -hls_list_size 900 \ /var/www/homesec/cameraname.m3u8 \ &#34; output_log=/var/log/homesec-cameraname.log error_log=/var/log/homesec-cameraname.log Web streaming apk add nginx rc-service nginx start rc-update add nginx server { listen 80 default_server; listen [::]:80 default_server; root /var/www/homesec/; location / {} } &lt;!DOCTYPE html&gt; &lt;link href=&#34;video-js.min.css&#34; rel=&#34;stylesheet&#34;&gt; &lt;script src=&#34;video.min.js&#34;&gt;&lt;/script&gt; &lt;style&gt; html, body { height: 100%; margin: 0; } video, .video-js { width: 100%; height: 50%; } &lt;/style&gt; &lt;video class=&#34;video-js&#34; controls autoplay muted data-setup=&#39;{&#34;liveui&#34;: true}&#39;&gt; &lt;source src=&#34;camera1name.m3u8&#34; type=&#34;application/x-mpegURL&#34;&gt; &lt;p class=&#34;vjs-no-js&#34;&gt;To view this video please enable JavaScript, and consider upgrading to a web browser that &lt;a href=&#34;https://videojs.com/html5-video-support/&#34; target=&#34;_blank&#34;&gt;supports HTML5 video&lt;/a&gt;&lt;/p&gt; &lt;/video&gt; &lt;video class=&#34;video-js&#34; controls autoplay muted data-setup=&#39;{&#34;liveui&#34;: true}&#39;&gt; &lt;source src=&#34;camera2name.m3u8&#34; type=&#34;application/x-mpegURL&#34;&gt; &lt;p class=&#34;vjs-no-js&#34;&gt;To view this video please enable JavaScript, and consider upgrading to a web browser that &lt;a href=&#34;https://videojs.com/html5-video-support/&#34; target=&#34;_blank&#34;&gt;supports HTML5 video&lt;/a&gt;&lt;/p&gt; &lt;/video&gt; Samba [homesec] path = /mnt/homesec/ apk add samba-server samba TODO
</description>
    </item>
    
    <item>
      <title>Power usage in my apartament</title>
      <link>https://krystianch.com/power/</link>
      <pubDate>Wed, 01 Feb 2023 20:15:42 +0100</pubDate>
      
      <guid>https://krystianch.com/power/</guid>
      <description>This year, new regulations regarding home power usage were introduced in Poland. I&rsquo;ve done some power and energy measurements in my apartament to help me comply with the 2000 kWh yearly energy limit. All values were measured by Orno OR-WAT-435 power meter.
PC: 93 W idle, 2 W suspended, 1 W off TV1: 152 W ES off, 106 W ES low, 66 W ES high, 0 W standby Air Conditioner: 741 W avg. (12.71 kWh / 17 h 9 min runtime) Energy measurements:
Electric kettle: 58 Wh per 0.5 l water Bathroom towel drier 300 W: 94 Wh (one wet towel, lowest temp. setting, shortest time setting) Displaying PC desktop.&#160;&#x21a9;&#xfe0e;
</description>
    </item>
    
    <item>
      <title>Better Mazovian Railways ticket service</title>
      <link>https://krystianch.com/km/</link>
      <pubDate>Fri, 11 Nov 2022 00:00:00 +0000</pubDate>
      
      <guid>https://krystianch.com/km/</guid>
      <description>During my studies at Warsaw University of Technology I used Mazovian Railways to go home and back to Warsaw. For a long time I&rsquo;ve used the ticket machines to buy train tickets. The problem was that, due to the frequent departures, there was often a queue to use the machine. Turthermore, the ticket machine was often slow, sometimes even to the point of timing out. Then I tried buying tickets olnine through the Mazovian Railways website. It was horrible. You have to enter your personal details twice and check multiple checkboxes after scrolling through nested popups. It takes an unacceptably long time to buy tickets this way.
That&rsquo;s why I made mazowieckie, live at km.krystianch.com. It&rsquo;s simpler than the official store and it loads way faster in slow network conditions (like in a train!) and on lower-end devices. It&rsquo;s not spying on you unlike the official one, which uses Piwik/Matomo to gather information like the size of your viewport and track your actions. It persists the connection you choose and your personal details in your browser&rsquo;s cookies so that you don&rsquo;t have to enter them manually again. There are also simplified descriptions of discounts because the original ones are difficult to understand. My inspiration for this project was this blogpost.
Connection search form
Personal details form
This service uses the internal HTTP API to purchase tickets. I use it with my partner every time we travel by Mazovian Railways. Tickets bought through this service were verified many times by ticket controllers.
</description>
    </item>
    
    <item>
      <title>Taking 404 spherical photos in 2 hours</title>
      <link>https://krystianch.com/pgrid/</link>
      <pubDate>Sun, 13 Jun 2021 00:00:00 +0000</pubDate>
      
      <guid>https://krystianch.com/pgrid/</guid>
      <description> Download the video Demonstration of a photorealistic camera simulation using pgrid.
For my project, pgrid, I had to take a lot of photos with a spherical camera. Pictures had to be taken on a 20 cm &ldquo;floor-grid&rdquo; as precisely as possible. Excluding a whole-day preparation and figuring out how to do this, the process took 2 hours 17 minutes, and the result was a set of 404 spherical photos, which you can download here (1.6 GB, CC BY-SA 4.0). The images are equirectangular projections with 5376 x 2688 px resolution. They depict a section of lab 012 in the Faculty of Electronics and Information Technology at Warsaw University of Technology.
The first order of business was to design and assemble the camera rig. Maciej Stefańczyk, who came up with the original idea for this project, suggested using a steel rail with a pole attached to a cart. This approach seemed better than using a tripod because we could position the rail precisely and take multiple photos by moving just the cart. This sped up the process by quite a bit. I marked 6 points dividing the rail into 20 cm segments to create discrete cart positions.
Camera rig. Rail, cart, pole and a camera mount.
Scale drawn on the rail.
The next step was to painstakingly mark a 20 cm x 1 m grid on the floor. Points of this grid determined where to place the rail. I chose a reference point and used a laser level and a tape measure to draw more points. I helped myself along the way by assuming some parts of the room were parallel (like tile grouts and the wall), which ended up being a mistake. I had to redo parts of the grid because errors accumulated and grew to multiple centimeters.
Points grid on the floor.
In the picture below, you can see a lot of sloppy circles on the floor. They are there because those small grid points were not well visible, so I drew a circle around each of them. I also annotated some points with their coordinates so that in case I got lost while taking the photos, I would be able to align quickly.
Now let&rsquo;s go back a few days. I was looking for a way to automate taking photos and annotating them with coordinates. The spherical camera I used was Ricoh Theta V, which Maciej kindly lent to me. This camera has a Wi-Fi interface and runs an HTTP server, to which you can make API calls. It uses the Theta Web API, which is compatible with the Open Open Spherical Camera API.
I created an app that would take pictures and keep track of filenames and coordinates. I don&rsquo;t have much experience in UI/UX, so the best I could come up with is a 2D scatterplot, which shows pending points (blue) and points where a photo was already taken (green). The user had to click a point and then press the &ldquo;Take picture&rdquo; button to execute the camera.takePicture command. Then the app polled the command status endpoint to wait until the camera has finished processing. After the image was ready, it saved a line containing the filename (retrieved from command status) and coordinates to a text file like this:
R0020155.JPG 3.6_-5.6_0.0.jpg R0020154.JPG 3.6_-5.4_0.0.jpg R0020153.JPG 3.6_-5.2_0.0.jpg Screenshot of the app after taking all 404 photos.
Fast forward to the picture-taking day. Maciej offered his help, so we split responsibilities. He sat behind a desk, hidden from the camera, with a laptop and operated the app. I, however, was repositioning the camera and hiding while he was taking pictures. Watch this short timelapse to see me in action.
After the work was done, I downloaded the images from the camera and devised a simple script to visualize our timing based on Exif metadata, which contains timestamps of images. Below you can see the plot with time on the horizontal axis and the number of pictures taken on the vertical axis.
Timing plot.
Expand to see the script. #!/bin/sh script=$(cat &lt;&lt;EOF set terminal svg background &#39;white&#39;; set datafile separator &#34;,&#34;; set ylabel &#34;total photos taken&#34;; set xlabel &#34;time [h]&#34;; plot &#39;&lt; cat -&#39; with dots notitle; EOF ) timestamps=$(exiv2 *.jpg \ | awk &#39;BEGIN {FS=&#34; : &#34;;} /Image timestamp/ { print $2; }&#39; \ | sed &#39;s/^\([0-9]*\):\([0-9]*\):\([0-9]*\)/\1-\2-\3/g&#39; \ | xargs -I {} date +%s -d &#34;{}&#34; \ | sort) first=$(echo &#34;$timestamps&#34; | head -n 1) relative=$(echo &#34;$timestamps&#34; | xargs -n 1 expr -$first +) total=$(echo &#34;$relative&#34; | tail -n 1) seconds=$(expr $total % 60) minutes=$(expr $total / 60 % 60) hours=$(expr $total / 60 / 60) &gt;&amp;2 echo &#34;Total: $total s = $hours h $minutes min $seconds s&#34; echo &#34;$relative&#34; \ | xargs -I {} echo &#34;scale=2; {} / 60 / 60&#34; \ | bc \ | awk &#39;{print $0&#34;,&#34;,NR}&#39; \ | gnuplot -e &#34;$script&#34; We took two long breaks, about an hour after the first photo and then half an hour after that. There were also a few short breaks. The main reason for them was the need to move something out of our way that I missed while preparing the space the previous day. All in all, our average picture-taking speed was 2.9 pictures per minute.
Now it&rsquo;s time for reflections about the whole process. Let&rsquo;s start with the camera rig. The pole was poorly secured to the cart because the screw holes were not compatible with each other. We could have drilled into the base of the pole to make it more stable. Due to a bit of wobbliness, images are more or less rotated around the mounting point of the pole. The effect is not easily noticeable but is definitely present. This problem can be corrected by performing a transform on the images, but this is material for another blog post.
The grid I drew on the floor was not that precise. It&rsquo;s because I unnecessarily rushed this task and didn&rsquo;t give it enough thought. Also, a tool like a long, rigid straight angle would help to make it better. I also thought of using a sheet of some kind with a grid drawn on it. It could have been placed on the floor, and the rail would be positioned according to it. The only issue would be moving the sheet without changing its orientation, but again, a long straight angle would help.
The last issue was the software. I mean, it wasn&rsquo;t that bad, but I noticed some room for improvement. The app indicated when the picture was done by showing it in the GUI. After Maciej saw this, he verbally communicated that I could leave the hiding spot and move the camera to the next position. This communication path could be shortened if the app made a sound when the picture was taken. Then the person operating it could focus on keeping track of coordinates and not on communicating with the other person.
The app was also too mouse-focused. Clicking points and then clicking the &ldquo;shutter button&rdquo; was repetitive and could have been less of a hassle by making it more keyboard-friendly. Points could have been selected by arrow keys (or hjkl) and pictures taken with the spacebar. I think that we could vastly increase (if not double) our average pictures-per-minute by fixing just the software issues.
All in all, this part of the pgrid project was a lot of fun. It required skills from a few domains to execute, but I learned a lot, which is very rewarding. This set of spherical photos can be used to photo-realistically simulate a room, which has its uses in service robot simulation.
</description>
    </item>
    
    <item>
      <title>NGINX Prometheus exporter, but it&#39;s awk</title>
      <link>https://krystianch.com/nginx-prometheus/</link>
      <pubDate>Tue, 01 Jun 2021 00:00:00 +0000</pubDate>
      
      <guid>https://krystianch.com/nginx-prometheus/</guid>
      <description>NGINX exposes a few interesting metrics with its stub status module. However, they are exported in a non-standard format which looks like this
Active connections: 4 server accepts handled requests 6756 6756 16204 Reading: 0 Writing: 1 Waiting: 3 If you wanted Prometheus to scrape these you are out of luck, but there are two solutions to this problem. One is knyar/nginx-lua-prometheus and the other is nginxinc/nginx-prometheus-exporter. Both are probably fine, but I don&rsquo;t really like the idea of running an additional deamon, which&rsquo;s sole purpose is to serve metrics from other daemon.
If you happen to already have fcgiwrap running, we can just use awk. If you don&rsquo;t have fcgiwrap, install and run it. You can use a simple setup like this for exporting many more metrics. Introducing: &ldquo;NGINX exporter, but it&rsquo;s awk&rdquo;!
Inspired by this method to transform the output of unbound-control stats into Prometheus metrics style output I decided to do the same for NGINX.
Here is the result:
# Read nginx stub_status # and output Prometheus metrics style output. # Use it like this: curl -s &#34;localhost:8080/nginx_status&#34; | awk -f &#34;metrics.awk&#34; BEGIN { FS=&#34; &#34;; } /^Active connections: [0-9].*$/ { m[&#34;active&#34;]=$3 } /^ [0-9].* [0-9].* [0-9].*$/ { m[&#34;accepted&#34;]=$1 m[&#34;handled&#34;]=$2 m[&#34;requests&#34;]=$3 } /^Reading: [0-9].* Writing: [0-9].* Waiting: [0-9].*$/ { m[&#34;reading&#34;]=$2 m[&#34;writing&#34;]=$4 m[&#34;waiting&#34;]=$6 } END { print &#34;# HELP nginx_connections_accepted Accepted client connections&#34; print &#34;# TYPE nginx_connections_accepted counter&#34; print &#34;nginx_connections_accepted &#34; m[&#34;accepted&#34;] print &#34;&#34; print &#34;# HELP nginx_connections_active Active client connections&#34; print &#34;# TYPE nginx_connections_active gauge&#34; print &#34;nginx_connections_active &#34; m[&#34;active&#34;] print &#34;&#34; print &#34;# HELP nginx_connections_handled Active client connections&#34; print &#34;# TYPE nginx_connections_handled counter&#34; print &#34;nginx_connections_handled &#34; m[&#34;handled&#34;] print &#34;&#34; print &#34;# HELP nginx_connections_reading Connections where NGINX is reading the request header&#34; print &#34;# TYPE nginx_connections_reading gauge&#34; print &#34;nginx_connections_reading &#34; m[&#34;reading&#34;] print &#34;&#34; print &#34;# HELP nginx_connections_waiting Idle client connections&#34; print &#34;# TYPE nginx_connections_waiting gauge&#34; print &#34;nginx_connections_waiting &#34; m[&#34;waiting&#34;] print &#34;&#34; print &#34;# HELP nginx_connections_writing Connections where NGINX is writing the response back to the client&#34; print &#34;# TYPE nginx_connections_writing gauge&#34; print &#34;nginx_connections_writing &#34; m[&#34;writing&#34;] print &#34;&#34; print &#34;# HELP nginx_http_requests_total Total http requests&#34; print &#34;# TYPE nginx_http_requests_total counter&#34; print &#34;nginx_http_requests_total &#34; m[&#34;requests&#34;] print &#34;&#34; } Short and straight to the point. Now, this is just the awk program. We need to run it against the output of NGINX stub status module, but first let&rsquo;s activate the module itself. Add the following inside your http directive in nginx.conf.
server { listen 8080 default_server; listen [::]:8080 default_server; location /nginx_status { stub_status; } } Reload NGINX configuration. You will most likely need to use sudo or doas for this.
nginx -s reload And check if you can access the status page.
curl &#34;localhost:8080/nginx_status&#34; You should see something resembling what you saw at the beginning of this post.
Next save the awk program presented above somewhere convenient. I&rsquo;ll assume it is placed in /usr/local/share/nginx/metrics.awk
Now let us test the program. Run the following command. (The -s flag prevents curl from printing the progress bar to stderr.)
curl -s &#34;localhost:8080/nginx_status&#34; | awk -f &#34;/usr/local/share/nginx/metrics.awk&#34; You should see something that starts like this:
# HELP nginx_connections_accepted Accepted client connections # TYPE nginx_connections_accepted counter nginx_connections_accepted 7135 If there is no number after nginx_connections_accepted something did not work properly.
Next add a shell script that runs this whole shebang (pun intended) and save somewhere convenient. I recommend `/usr/local/bin/nginx-metrics'.
#!/bin/sh curl -s &#34;localhost:8080/nginx_status&#34; | awk -f &#34;/usr/local/share/nginx/metrics.awk&#34; Do not forget to make it executable. (You&rsquo;ll need sudo or doas.)
chmod +x /usr/local/share/nginx/metrics.awk Last step. Add the following inside our server directive in nginx.conf, adjusting the path to fcgiwrap.sock if needed.
location /nginx/metrics { gzip off; fastcgi_pass unix:/run/fcgiwrap/fcgiwrap.sock; include /etc/nginx/fastcgi_params; fastcgi_param SCRIPT_FILENAME /usr/local/bin/nginx-metrics; } That&rsquo;s it. Now you have NGINX metrics, Prometheus-style at localhost:8080/nginx/metrics. Add a suitable scrape config to prometheus.yml, paying extra attention to add metrics_path: '/nginx/metrics' if you used the same path as me.
Done, minimalistic NGINX exporter using only awk and existing NGINX + fcgiwrap setup.
</description>
    </item>
    
    <item>
      <title>Streaming OpenCV frames</title>
      <link>https://krystianch.com/cvstream/</link>
      <pubDate>Mon, 15 Oct 2018 00:00:00 +0000</pubDate>
      
      <guid>https://krystianch.com/cvstream/</guid>
      <description>If you&rsquo;ve ever played with robotics and OpenCV you&rsquo;d probably wondered how to display frames, which are processed on your robot, on your computer. Well, the solution may be to stream your video over the network.
One of the advantages of this approach is that you can annotate the video on your robot before sending it. That means you can, for example, mark targets that you are tracking, display FPS count and much more.
My choice was to send the video using a UDP socket because it is faster and more convenient than TCP as it&rsquo;s connectionless. Also, it is the preferred choice for streaming real-time video. Below there is a simple example program, which shows how to send data through an UDP soket using Boost&rsquo;s Asio library.
// This program sends a message // to udp://127.0.0.1:5001 // and exits. #include &lt;boost/asio.hpp&gt; #include &lt;iostream&gt; namespace ip = boost::asio::ip; int main(int argc, char *argv[]) { // try changing these parameters and see what happens std::string addr = &#34;127.0.0.1&#34;; std::string port = &#34;5001&#34;; std::string message = &#34;this message will be sent\n&#34;; // set up the socket boost::asio::io_context io_context; ip::udp::socket socket(io_context, ip::udp::endpoint(ip::udp::v4(), 0)); ip::udp::resolver resolver(io_context); ip::udp::resolver::results_type endpoints = resolver.resolve(ip::udp::v4(), addr, port); // send the message socket.send_to(boost::asio::buffer(std::string(message)), (const ip::basic_endpoint&lt;ip::udp&gt; &amp;) *endpoints.begin()); return 0; } When I launched netcat to listen on port 5001 before running the program above I received the message.
❯ nc -u -l -p 5001 127.0.0.1 this message will be sent Now that we have learned how to send data through the network, we are ready to stream video frames, right? Not so fast. Sending raw frames would be extremally inefficient. One frame of a BGR24 640x480 video takes up 3 B * 640 * 480 = 921600 B = 900 KB. However, there is a method to send lighter frames. We can encode our images as JPG and stream them, so that we are actually streaming a MJPEG video. Looking at the size of several JPG encoded frames i found that they may take up as little as 93 KB (the size varies). Thats an order of magnitude smaller than a raw one.
We can use the imencode() function from OpenCV to encode our frames.
There is one more problem. On my machine whenever i wanted to send an encoded frame I got the Message too long error. I solved this problem by splitting the data to chunks and then sending those chunks separatly. Below is the final, working code.
// TODO: annotate the video or do an algorithm #include &lt;boost/asio.hpp&gt; #include &lt;iostream&gt; #include &lt;opencv2/opencv.hpp&gt; namespace ip = boost::asio::ip; int main(int argc, char *argv[]) { const int chunk_size = 64000; std::string addr = &#34;127.0.0.1&#34;; std::string port = &#34;5001&#34;; boost::asio::io_context io_context; ip::udp::socket socket(io_context, ip::udp::endpoint(ip::udp::v4(), 0)); ip::udp::resolver resolver(io_context); ip::udp::resolver::results_type endpoints = resolver.resolve(ip::udp::v4(), addr, port); cv::Mat frame; cv::VideoCapture capture(0); while (true) { capture &gt;&gt; frame; std::vector&lt;uchar&gt; buf; cv::imencode(&#34;.jpg&#34;, frame, buf, std::vector&lt;int&gt;()); for (auto it = buf.begin(); it &lt; buf.end(); it += chunk_size) { auto end = it + chunk_size; if (end &gt;= buf.end()) { end = buf.end(); } socket.send_to( boost::asio::buffer(std::string(it, end)), (const ip::basic_endpoint&lt;ip::udp&gt; &amp;) *endpoints.begin() ); } } } There is still room for improvement though. The video could be compressed using interframes. H.264 is probably the most popular choice.
</description>
    </item>
    
    <item>
      <title>An interactive kiosk for a local cinema</title>
      <link>https://krystianch.com/pckisz/</link>
      <pubDate>Sun, 30 Sep 2018 00:00:00 +0000</pubDate>
      
      <guid>https://krystianch.com/pckisz/</guid>
      <description>Source code: [api] [web] [scraper]
This kiosk is situated in Powiatowe Centrum Kultury i Sztuki im. Marii Konopnickiej which is a local culture &amp; art center that includes a cinema. The primary role of the kiosk is to provide visitors with information about upcoming shows. All of the information about shows and movies is scraped from the center&rsquo;s webpage. It also enables users to browse the center&rsquo;s homepage.
Gallery Home screen Upcoming shows view Searching for specific shows Searching for a specific movie A view of the Center&#39;s webpage Technologies Back-end (serverless API):
AWS Lambda Amazon API Gateway Amazon DynamoDB Python 3 Beautiful Soup Python library for web scraping Front-end (Single Page Application):
Vue.js </description>
    </item>
    
    <item>
      <title>Historical multimedia database</title>
      <link>https://krystianch.com/historydb/</link>
      <pubDate>Tue, 21 Jun 2016 00:00:00 +0000</pubDate>
      
      <guid>https://krystianch.com/historydb/</guid>
      <description>Source code: https://github.com/krystiancha/django-smmc
This application provides a catalog of historical multimedia, accessible via an interactive kiosk. Photographs, videos, documents and audio files can be browsed and viewed using a touchscreen. The media is managed via an extensive administrator panel.
Background This application was created for I Liceum Ogólnokształcące im. Zygmunta Krasińskiego w Ciechanowie. The opportunity for our cooperation was created during the construction of The Tradition Room in this high school. The school&rsquo;s principal needed a way to display a large number of media connected to the school, accumulated over many years.
Gallery Landing page Searching media Media type filter menu Narrowing down the results by inputting more tags Technologies Back-end:
Django REST Framework Django Python 3 Front-end (Single Page Application):
AngularJS </description>
    </item>
    
    <item>
      <title>Projects</title>
      <link>https://krystianch.com/projects/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      
      <guid>https://krystianch.com/projects/</guid>
      <description> This list contains all projects I’ve created or contributed to, excluding negligible contributions. The muted ones are deprecated.
1st contrib. Status Project 2025-03-09 author journalman 2025-02-23 contributor sogogi 2025-02-09 contributor MediaMTX 2024-12-16 contributor llama.cpp 2024-11-03 author SDP eXchange protocol (sdpx-raspi) 2024-09-24 contributor libdatachannel 2024-09-07 author raspiwhip, e t c . 2024-08-08 author Konferencja ,,Wolny Soft&rsquo;&rsquo; [PL] 2024-08-04 contributor kimchi 2024-07-17 author fcast-mpv 2024-07-07 author webdetect 2024-06-27 contributor soju 2024-04-03 author photobrowser 2024-02-19 author keditb64 2024-02-18 author AVR resistive touchscreen driver 2024-02-01 employee Comtegra GPU Cloud 2024-01-31 author HEXONET script for OpenWRT ddns-scripts 2024-01-30 author wgpeer 2023-11-26 contributor sourcehut 2023-10-07 author tiny-timer 2023-10-02 author PadMouse 2023-09-11 author buyfriend (source) 2023-09-06 author Time and date source for OBS Studio 2023-07-19 author fastcec (blog post) 2023-06-13 author tiny-hvsp 2023-06-11 author es51922-serial 2023-05-08 author Custom HSV night light 2023-03-17 co-author SafeDNN 2022-12-15 contributor tokidoki 2022-09-15 contributor go-webdav 2022-07-27 contributor Himitsu 2022-06-04 author asana-tui 2022-05-09 author uploadme 2022-04-20 author openstack-exporter-pci 2022-02-18 author mazowieckie 2022-01-02 author Pekao24 CLI 2021-12-10 contributor chartsrv 2021-11-07 author FFmpeg-based video surveillance solution (web, OpenRC scripts) 2021-11-04 author apk-nodeps 2021-10-20 author biju 2021-08-10 author Prezentacja o błędach w układach scalonych [PL] 2021-08-09 contributor Disk encryption in Alpine Linux config scripts 2021-05-28 contributor aports 2021-03-04 author network-epaper, eth. driver, e-paper lib. 2021-02-17 contributor Hauk 2021-01-16 author Chat Stats (source, started as donations) 2021-01-11 author PAMAE Python 2020-12-26 author route53-ddns 2020-12-25 contributor Bosch Sensortec Prometheus exporter 2020-11-30 co-author Data fusion in wastewater networks (presentation, presentation 2, article, article) 2020-11-30 author Open Spherical Camera API Python Client 2020-11-15 author originlodge 2020-11-06 author manga-browser 2020-09-01 author transcriptions 2020-07-01 author wotstats 2020-06-20 author tiny-weather (started as thermnet, then pico-weather) 2019-09-24 author xdg-open w/ terminal support 2019-09-17 contributor Waybar 2019-09-14 co-author Train game (HackYeah 2019) (src1, src2) 2019-06-10 contributor dex 2019-05-30 author Dockerfiles for BioWeb 2019-05-11 co-author Local Positioning System (BEST Hacking League 2019, video reel, source) 2019-03-01 author pgrid (presentation 1 [PL], presentation 2 [PL], article 1 [PL], article 2 [EN], started as camera_mock, then panoramagrid) 2018-11-23 author Image median filter for MARS 2018-09-05 author Kiosk for local cinema 2018-07-15 author Harmony Playground (source) 2018-06-18 author rqt_histogram 2018-05-02 author trackclip 2018-04-21 co-author Marker-following robot (BEST Hacking League 2018, video) 2018-03-11 author Self balancing robot (old attempt) 2018-01-25 co-author Line-following robot (video) 2017-12-27 author Prezentacja o przetwarzaniu zdalnym [PL] (źródło) 2017-10-27 author Warsztaty z podstaw Arduino [PL] (źródło) 2017-10-17 author synthgen 2017-08-07 author trello-autoassign 2017-07-30 author Instagram follower browser (video) 2017-07-22 author Protodos 2017-07-19 author Failed AVR RF transmitter based on wrong assumptions 2017-06-18 author Ondum 2017-04-15 author Short article about Adruino&rsquo;s digitalWrite speed 2017-03-24 author libroboarm 2016-12-24 author Red-black Tree library 2016-07-28 author Historical multimedia database 2016 (?) author Coal furnace controller </description>
    </item>
    
  </channel>
</rss>
