Béton brut

Kanał subskrybcji Napisz do mnie…

Raspberry z papierka

Jeśli definicją szaleństwa jest „robieni tego samego, spodziewanie się innego rezultatu” to należy powiedzieć, że programowanie i administracja muszą być przeciwieństwem szaleństwa. Oba zawody polegają na rozkładaniu problemu na części pierwsze, ekstrakcję sensu i zamykanie całości w powtarzalny element, który raz wykonany oszczędzi nam technologicznego jąkania.

Szaleństwo w obu zawodach wypływa z innego źródła: nasze rozwiązania często są kruche i produkcja ich zajmuje dłużej niż suma powtórzeń. Nikt nie powiedział, że mamy wszystkie odpowiedzi, zwykle nie znamy nawet pytań.

Po raz kolejny zgrzytałem zębami, gdy przegrywałem czysty obraz dystrybucji na kartę SD abym mógł zaprząc Raspberry do męczenia się nad moim nowym pomysłem. Mimo tego, że mam dostęp do dziesiątek serwerów znajduję przyjemność w zerkaniu przez ramię na lodówkę i szydercze uśmiechanie się w kierunku zwisających z za krótkich kabli ethernetowych mikro-komputerów.

Do tej pory widzieliście już jak Jeżyna mrugała LED-ami czy też pieczołowicie zbierała klatki z kamer. Ostatnio zacząłem zostawiać laptopa w pracy i zbudowałem sobie „maszynę do pisania” w nadziei, że to natchnie mnie twórczo. Głównym rezultatem było szukanie fontów, które dają się czytać na 3.2 calowym ekranie i które mają polskie znaki.

Raspberry na biurku

Dziś znów coś wymyśliłem i od razu rozbolała mnie głowa. Znów ustawianie WiFi, znów parowanie urządzeń BT, przegranie tego czy owego. Zawsze to samo. Ale przecież nie trzeba tak żyć. Postanowiłem zobaczyć, czy uda się łatwo zmodyfikować obraz tak, abym zawsze mógł startować z wygodnego mi miejsca. Rozważałem Ansible, ale to zakłada obecność sieci.

Rozpocząłem od pobrania czystej wersji systemu i rozpakowania jej. Dostajemy wtedy plik YYYY-MM-DD-raspios-ver-distro.img. Jest to obraz zawierający dwie partycje: /boot i rootfs. Teoretycznie można je zamontować przy użyciu -o loop, ale trzeba znać ich przesunięcia, a my tu gadamy o automatyzacji. Na szczęście istnieje kpartx zajmujący się czytaniem tych informacji.

% sudo kpartx -v -a 2020-05-27-raspios-buster-lite-armhf.img
add map loop8p1 (254:0): 0 524288 linear 7:8 8192
add map loop8p2 (254:1): 0 3088384 linear 7:8 532480

Otrzymaliśmy dwa urządzenia reprezentujące wyżej wspomniane partycje. Teraz możemy je normalnie podmontować.

% mkdir /tmp/{1,2}
% sudo mount /dev/mapper/loop8p1 /tmp/1
% sudo mount /dev/mapper/loop8p2 /tmp/2

Mając dostęp do zamontowanych partycji możemy podciągnąć nasz system do stanu, który nam bardziej odpowiada. Przykładowo sudo touch /tmp/1/ssh, zgodnie z dokumentacją odpali nam SSHd przy piewszym uruchomieniu. Oto kilka modyfikacji, które dodałem dla własnej wygody:

  1. Edycja hostname, bo komputer musi się jakoś fajnie nazywać.
  2. Wgranie własnego klucza żebym się nie musiał bawić we wpisywanie haseł. Wystarczy stworzyć katalog .ssh w home/pi w ścieżce drugiej partycji i wrzucić co trzeba do authorized_keys
  3. Dostaję wścieklizny, gdy za każdym razem muszę usuwać odcisk palca hostu. Utworzyłem więc i przegrałem do etc/ssh/ wcześniej wygenerowane pliki ssh_host_* upewniając się, że nigdy nie zobaczę spanikowanego komunikatu o potencjalnym zagrożeniu. Jedna uwaga do tego, unit systemd odpowiedzialny za tworzenie kluczy ignoruje obecność już istniejących. „Rozwiązałem” ten problem usuwając go z lib/systemd/system. Pewnie jest mądrzejszy sposób, np. usunięcie symlinka.
  4. Podobnie jak z aktywacją SSHd możemy wrzucić do podmontowanego /boot wpa_supplicant.conf z definicją domowego/pracowego WiFi.
  5. Dograłem trochę oprogramowania, którego i tak zawsze potrzebuję, a nie znajduje się w pakietach.

Jest jeszcze jedna sztuczka, której nie będę opisywał, bo już zrobili to za mnie. Używając qemu możemy dokonać chroota w obcy architektonicznie system i poinstalować/poodpalać rzeczy bez potrzeby wkładania karty do Raspberry.

Kiedy skończymy modyfikacje kluczowe jest odmontowanie partycji i usunięcie mappera stworzonego przez kpartx. Podczas pierwszych eksperymentów udało mi się dokonać sztuki, w której co innego widziała karta, a co innego obraz.

% sudo umount /tmp/1 && sudo umount /tmp/2
% sudo kpartx -d 2020-05-27-raspios-buster-lite-armhf.img
loop deleted : /dev/loop8

Po tej operacji nasz zmodyfikowany obraz możemy odłożyć gdzieś na bok i w przyszłości generować systemy typu „zupka chińska”, tylko zalać wodą.

Pomiędzy dwoma rozwiązaniami, które rozważałem: uruchamianie z TFTP lub modyfikacja obrazu źródłowego ten drugi okazał się być dużo mniej wymagający. Może w przyszłości zautomatyzuję tę automatyzację (zwłaszcza tę część z qemu, ale wpierw naciszę się tym małym, środowym, sukcesem.


Dobre rzeczy, gorzej: Jajeczka w 302 liniach Lua

W mojej przygodzie z programowaniem jest wiele rzeczy, których nie próbowałem. Umościłem się wygodnie na dnie stosu i ciężko jest mi się wygrzebać z tego barłogu, gdzie ciepło, a wszystkie kąty są znane. Cierpię też na przypadłość, która trapi wielu programistów: mój zmysł estetyczny jest przytłumiony. Jeśli coś wygląda paskudnie, a działa doskonale to znaczy, że jest piękne. Dlatego też podchodziłem do programowania gier z wielką nieufnością. Trzeba się piekielnie napracować żeby coś dobrze wyglądało. Żeby grający wiedział, co dzieje się na ekranie.

Do tego trzeba mieć pomysł. Najlepiej dobry pomysł.

Dlatego latami czytałem sobie rozmaite „jak to się robi”, ale nigdy nawet nie wyszedłem poza kompilację załączonych przykładów do tej czy innej biblioteki, mądre potrząsanie głową i mówienie „Acha!”.

Kilka dni temu zamknąłem dzień w pracy straszną awanturą. Jedna z tych, która jak śniegowa kula zaczyna się gdzieś od płatka, a kończy się groźbą rozpłaszczenia nas wszystkich. Potrzebowałem zająć się czymś do końca dnia żeby nie usiąść i nie zacząć komponować e-maila z kategorii „jak ja to widzę”.

Potrzebowałem czegoś absorbującego uwagę, więc najlepiej czegoś, czego nigdy nie robiłem. Wiem, napiszę grę. Problem braku pomysłu i jakości rozwiązałem w iście holiudzkim stylu. Zrobię to, co już było, ale gorzej!

Mój wybór padł na kultową grę elektroniczną zwaną kolokwialnie „Jajeczkami”. Zasady są proste, grafika jest prosta. Powinienem sobie dać radę. W temacie narzędzi padło na Luę i Love2d. Nie znałem jednego ani drugiego, więc idealnie wpasowywały się w mój plan zajęcia głowy nowymi problemami.

Po godzinie udało mi się uzyskać kontrolę nad środowiskiem na tyle, że patrząc na biały prostokąt rozpierała mnie duma i wiara we własne możliwości. Postanowiłem udokumentować ten cały proces i podzielić się nim z Wami. Na początku wymyśliłem, że będzie to doskonały tekst dla początkujących, ale im bardziej piętrzyły się przede mną problemy tym bardziej traciłem wiarę, że dam radę.

Powstały więc zapiski chaotyczne, które ani nie uczą, ani nie tłumaczą. Przynajmniej powstrzyma mnie to od napisania „Re: jak ja to widzę”.

„Jajeczka”

Wpierw o samej grzej. „Jajeczka” to klon Mickey Mouse, gry elektronicznej wydanej w 1984 przez Nintendo. Postacie zostały wymienione na mniej reakcjonalne maskotki z kreskówki „Ну, погоди!”. Wcielamy się w postać Wilka, który w kreskówce przedstawiany jest jako bumelant i opryszek. Tym razem zajmuje się pracą na kurzej fermie, najwidoczniej w wyniku resocjalizacji.

Na czterech grzędach siedzą cztery kury, które znoszą jajka, a te pod wpływem grawitacji toczą się rynnami w dół. Naszym zadaniem jest złapanie ich do koszyka. Trzeba też nadmienić, że kury te są niesamowicie produktywne, Trofim byłby dumny.

Wyświetlacz to segmentowane LCD. Znaczy to, że wszystkie możliwe elementy są predefiniowane, a ruch symulowany jest przez zapalanie odpowiednich kształtów. Znacząco ułatwia mi to kradzież, gdyż nie muszę się przejmować skomplikowanymi fazami ruchu.

Wyświetlacz LCD ze wszystkimi komponentami

Postanowiłem wyciąć wszystkie elementy, które pozostają w ruchu i potem poukładać je samemu w grze.

Mamy więc:

  1. Wilka patrzącego w lewo i prawo
  2. Ręce Wilka sięgające do dolnej i górej grzędy dla lewego i prawego kierunku
  3. Pięć faz animacji toczącego się jajka dla każdej rynienki
  4. Cztery fazy animacji „stłuczonego jajka”
  5. Wskaźnik liczba „stłuczonych jajek”

To powinno wystarczyć za część graficzną. Teraz pozostaje zebrać całość do kupy. Ale zanim zaczniemy, kilka słów dla czytelników, którzy nigdy nie interesowali się jak to się dzieje, że rzeczy się ruszają i są w odpowiednich miejscach. Choćby amatorsko doświadczeni w sztuce będą wiedzieli, że te definicje nie są idealne, ale to neofita tłumaczy ludziom krok za nim.

Ekran

Możecie myśleć o ekranie komputera jak o kartce papieru w kratkę, gdzie każdy segment to pojedynczy piksel, który można zaświecić na dowolny kolor z dostępnej dla komputera palety. Pozycję na ekranie wyrażamy jako koordynaty X i Y.

W przypadku Love2d, lewy górny róg ekranu znajduje się pod pozycją (0, 0). Więc (10, 0) znaczy jedenaście pikseli (liczymy od zera) w prawo od brzegu w pierwszej linii. Idąc dalej tym tropem (4, 5) znajdzie się w szóstej linii od góry, pięć pikseli od lewego brzegu ekranu.

W większości przypadków nie będziemy rysować grafiki piksel po pikselu. Zwykle wczytujemy gotowy element graficzny, który umieszczamy na naszej płaszczyźnie. Sam wczytany obiekt ma własną, wewnętrzną siatkę koordynat, ale umieszczamy go wskazując miejsce, gdzie znajdzie się jego (0, 0).

Więc jeśli chcemy wczytać niezmiernie estetyczną żółtą twarz i umieścić ją gdzieś na ekranie, powiemy, że pod (9, 5) to wyląduje tam lewy górny bok, reprezentowany wewnętrznie przez (0, 0)

Animacja pokazująca siatkę

Myślę, że to wszystko czego potrzebujemy na tę chwilę.

Stan

Stan to wiedza o wszystkich właściwościach świata stworzonego w grze. Co jest stanem w Jajeczkach? W którą stronę obrócony jest wilk i jak ma ułożone ręce. Ile razy przegraliśmy. W jakiej pozycji znajdują się jajka i ile ich jest na ekranie. Punktacja. Wszystkie elementy, które tworzą grę są odzwierciedlone przez zmienną, która jest potem interpretowana przez nas.

Zmienna score w consts.lua trzyma naszą punktację, miss liczbę skuch, w main.lua hands i face reprezentują pozycję wilka.

W Love2d stan jest globalny, tj. wszystkie komponenty mają bezpośredni dostęp do zmiennych. Jest to uważane za bardzo zły pomysł™, gdyż niepowiązane komponenty mogą sobie stawać na palcach i doprowadzać do nieokreślonego stanu, ale myślę, że obniża to też znacząco wysokość pierwszego stopnia, który potencjalny twórca musi pokonać.

Pętla

Istnienie czasu to problem, który spędzał sens z powiek wielu filozofom. Czy to rzeczywiście taka prosta linia z przeszłości w przyszłość? Czy może cały czas istnieje na raz, a nasza świadomość odbiera tylko echa takiej projekcji. Być może zmiany są w ogóle niemożliwe?

Jakakolwiek jest rzeczywistość skryta przed naszymi oczami, zmechanizowaliśmy czas, pocięliśmy go na plasterki sekundową wskazówką zegarka i empirycznie jest nam z tym dobrze.

W grze zmiana i czas muszą być przez nas odtworzone, nikt nie znajdzie przyjemności patrząc w statyczny obraz na ekranie kiedy miał zamiar grać.

Love2d posiada dwie funkcje, które są wywoływane na okrągło, w nieskończonej pętli. To w nich implementujemy zmianę (co się rysuje) i czas (zmiany stanu gry). Są to odpowiednio love.draw() i love.update(dt).

Może zauważyliście, że w przypadku update mamy parametr dt — jest to zmienna, która mówi ile czasu minęło od ostatniego jej wywołania (Δ czasu). Pozwala nam to kontrolować czas w którym rzeczy się dzieją. Przykładowo chciałem aby można było poruszać Wilkiem w każdej chwili, ale ruch spadających jajek musi zachować jednolite tempo, niepowiązane z ruchem postaci (inaczej gra nie była by uczciwa: gdyby Wilk poruszał się w tempie spadających jajek, sięgnięcie trzech będących na granicy rynny byłoby niemożliwe).

Popatrzmy w kod:

function love.update(dt)

    require('wolf_movement') 
    wolf_movement()

    time_since_movement = time_since_movement + dt
    if(time_since_movement >= ms_to_roll) then
        time_since_movement = 0
        -- a jednak się rusza!
    end
end

Mamy zmienną time_since_movement w której składujemy wszystkie mikrosekundy, które do nas przyszły w wyniku wykonywania funkcji. Jeśli ta suma jest większa niż zdefiniowana w ms_to_roll ruszamy spadającymi jajkami. Funkcja odpowiedzialna za ruch wilka znajduje się w wolf_movement i jest poza blokiem, pozwalając mu ruszać się w każdej chwili.

Po tym jak Love2d skończy wykonywać ciało funkcji love.update(dt) wywoływane jest love.draw(), gdzie nasza grafika jest rysowana zgodnie ze stanem ustawionym wcześniej.

Elementy graficzne

Każda klatka animacji znajduje się w osobnym pliku i jest wczytywana podczas inicjalizacji programu. Wymyśliłem sobie, że będę je wszystkie trzymał w tabeli (nomenklatura Lua znacząca listę/hashmapę), dzięki czemu animowanie będzie polegało tylko na zwiększaniu indeksu o jeden.

W przypadku Wilka musiałem zastosować nieco inną metodę. Ponieważ rusza się on niezależnie i jest sterowany przez nas, możemy jego fragmenty pochować pod ładnymi nazwami

-- pozycje wilka
wolf_left = {
    171, 134, -- koordynaty
    love.graphics.newImage('assets/wolf_left.png'), -- obrazek
    'left' -- nazwa
}
wolf_right = {
    250, 134,
    love.graphics.newImage('assets/wolf_right.png'),
    'right'
}
left_up = {
    124, 134,
    love.graphics.newImage('assets/left_up.png'),
    'up'
}
left_down = {
    116, 193,
    love.graphics.newImage('assets/left_down.png'),
    'down'
}
right_up = {
    309, 138,
    love.graphics.newImage('assets/right_up.png'),
    'up'
}
right_down = {
    300, 198,
    love.graphics.newImage('assets/right_down.png'),
    'down'
}

Każdy fragment Wilka składa się z czteroelementowej tablicy, która zawiera: koordynaty, reprezentację wczytanego obrazka, a następnie string. Ten ostatni dodałem nie mogąc wymyślić jak zaimplementować sytuację, gdy Wilk patrzy w jedną stronę i zmieniamy go na drugą. Logika mówi, że jeśli trzymał ręce w górze w lewo, po zmianie na prawo ta pozycja będzie zachowana. Niestety, nieznajomość Lua spowodowała, że nie mogłem wymyślić lepszego pomysłu niż wulgarne nazwanie kierunku.

Ruch jajek miał dużo mniej problemów merytorycznych.

-- ruch jajek
eggs_pos = {
    {
        {37, 86, love.graphics.newImage('assets/left_up_egg_1.png')},
        {54, 99, love.graphics.newImage('assets/left_up_egg_2.png')},
        {75, 104, love.graphics.newImage('assets/left_up_egg_3.png')},
        {92, 117, love.graphics.newImage('assets/left_up_egg_4.png')},
        {107, 137, love.graphics.newImage('assets/left_up_egg_5.png')}
    },
    {
        {39, 156, love.graphics.newImage('assets/left_down_egg_1.png')},
        {55, 164, love.graphics.newImage('assets/left_down_egg_2.png')},
        {71, 176, love.graphics.newImage('assets/left_down_egg_3.png')},
        {92, 183, love.graphics.newImage('assets/left_down_egg_4.png')},
        {105, 204, love.graphics.newImage('assets/left_down_egg_5.png')},
    },
    {
        {416, 87, love.graphics.newImage('assets/right_up_egg_1.png')},
        {399, 95, love.graphics.newImage('assets/right_up_egg_2.png')},
        {381, 106, love.graphics.newImage('assets/right_up_egg_3.png')},
        {359, 117, love.graphics.newImage('assets/right_up_egg_4.png')},
        {346, 128, love.graphics.newImage('assets/right_up_egg_5.png')},
    },
    {
        {418, 159, love.graphics.newImage('assets/right_down_egg_1.png')},
        {399, 164, love.graphics.newImage('assets/right_down_egg_2.png')},
        {380, 174, love.graphics.newImage('assets/right_down_egg_3.png')},
        {364, 184, love.graphics.newImage('assets/right_down_egg_4.png')},
        {343, 200, love.graphics.newImage('assets/right_down_egg_5.png')},
    }

}

Mamy więc tabelę z czterema elementami, które odwzorowują cztery rynny. A w nich pięć faz animacji spadającego jajka: koordynaty i plik graficzny. Czyli eggs_pos[1][1] to pierwsza rynna, pierwsza pozycja jajka. I tak, zanim zapytacie: tabele w Lua zaczynają indeks od 1. Miałem przynajmniej trzy błędy związane z moim — najwidoczniej optymistycznym — założeniem, że liczymy od zera!

Podobnie wygląda definicja animacji skuchy.

I to byłoby na tyle w temacie przygotowania elementów graficznych.

Wilk lewy, Wilk prawy

Miałem wielką ambicję zrobienia jakiejś dobrej abstrakcji w temacie ruchu naszym protagonistą. Nauczyłem się jednak, że zła abstrakcja boli całe życie. Zwłaszcza, gdy robi się ją kompletnie bez znajomości idiomów języka. Postanowiłem, że prosty if() nikogo nie zabił, nawet jeśli powtarzam się powtarzam się przy tym przy tym.

Czytanie klawiatury w Love2d jest trywialne, nie trzeba znać nawet kodów klawiszy.

function wolf_movement()
    if love.keyboard.isDown("up") then 
        if face[4] == 'left' then
            hands = left_up 
        else
            hands = right_up
        end
    end
    if love.keyboard.isDown("down") then 
        if face[4] == 'left' then
            hands = left_down 
        else
            hands = right_down
        end
    end
    if love.keyboard.isDown("right") then
        face = wolf_right
        if hands[4] == 'up' then
            hands = right_up
        else
            hands = right_down
        end
    end
    if love.keyboard.isDown("left") then
        face = wolf_left
        if hands[4] == 'up' then
            hands = left_up
        else
            hands = left_down
        end
    end
end

Zmienne face i hands przechowują obecną pozycję Wilka. Widzimy tu moje tchórzliwe użycie czwartego elementu do zachowania stanu rąk. Teraz trzeba wrócić do love.draw i powiedzieć mu, co rysować.

love.graphics.draw(face[3], face[1], face[2])
love.graphics.draw(hands[3], hands[1], hands[2])

I to wszystko! Już można hasać po ekranie. Programowanie gier to czysta przyjemność, gdy nie przejmujesz się detalami!

Demonstracja ruchu

Jajka się toczą

Nabuzowany tym sukcesem postanowiłem powoli zabrać się za następny element, ruch spadających jajek. Pierwsza wersja przyszła szybko.

Piersze jajka za płoty

Mina mi jednak szybko zrzedła. Okazało się, że zapomniałem o grze w grze. Moja pierwsza implementacja nie mogła działać. Muszę mieć możliwość losowego spuszczania jajek dowolną rynną oraz dodawania nowych. Dodatkowo jest też zwiększający się poziom trudności, zaczynamy od jednego jajka, a po uzyskaniu jakiejś liczby punktów muszą być dwa. I nie mogą zaczynać w tym samym momencie. Dobrą chwilę patrzyłem w kod nie potrafiąc się wyrazić wystarczająco jasno w Lua.

Na przykład zmienną trzymającą stan rynien normalnie ująłbym używając binarnej reprezentacji. 0x1010 i wiemy, że jajko toczy się po rynnie 1 i 3. Ale Lua nie ma w standardowej bibliotece odpowiednich narzędzi, nie chciałem utknąć w trybie „jak się instaluje biblioteki”. Zrobiłem więc następującą rzecz, wiedząc już, że moje zapiski będą bezużyteczne dla absolutnie początkujących: dodałem zmienne eggs = {0, 0, 0, 0} i roll_on = {0, 0, 0, 0} reprezentujące odpowiednio klatkę animacji na danej rynience i to, czy coś się po niej toczy. Następnie napisałem okropne sum(), które przyjmuje tabelę i sumuje elementy. Aplikując to na roll_on wiem ile rynienek jest zajętych. Teraz tylko sprawdzić, czy chcę mieć więcej jajek niż jest widoczne, zapisać indeksy tych, które są puste (==0) i losowo wcisnąć jajko.

A co z opóźnieniami? Okazało się, że Lua jest cierpliwe i zaakceptowało mój szalony pomysł: ustawię pozycję na ujemną!

I jakimś cudem zadziałało, uff…

function gutters_logic()
    if sum(roll_on) == 0 then -- nic się nie toczy
        -- znieś jajko byle gdzie
        roll_on[math.random(1,4)] = 1
    end

    if sum(roll_on) < eggs_on_screen then
        -- jest mniej jajek niż chcemy
        print("Sum(roll_on) ", sum(roll_on))
        print("eggs_on_screen ", eggs_on_screen)
        pick = {}
        how_many_we_need = eggs_on_screen - sum(roll_on)
        print('how_many_we_need ', how_many_we_need)
        for i=1, table.getn(roll_on) do
            -- wybieramy puste rynienki
            if roll_on[i] == 0 then
                table.insert(pick, i)
            end
        end

        for i=1, how_many_we_need do
            -- losowa rynna z dostępnych
            choice = math.random(1, table.getn(pick)) 
            if roll_on[pick[choice]] == 1 then
            else
                roll_on[pick[choice]] = 1
                -- ileś „tyknięć nim się pojawi”
                eggs[pick[choice]] = math.random(1, 4) * -1
            end
        end
    end
end

Jak Doktor Frankenstein byłem zafascynowany i przerażony, że to żyje!

Rysowanie poszło dużo łatwiej.

for i=1, table.getn(roll_on) do
    if eggs[i] > 0 then
        current_egg = eggs_pos[i][eggs[i]]
        love.graphics.draw(current_egg[3], current_egg[1], current_egg[2])
    end
end

Goń i łap

Nie pozostało nic innego jak zacząć łapać te jajka. Po raz kolejny zawiesiłem wszystko na choince if()ów.

function success_or_failure(i)
    if i == 1 and face == wolf_left and hands == left_up then return true end
    if i == 2 and face == wolf_left and hands == left_down then return true end
    if i == 3 and face == wolf_right and hands == right_up then return true end
    if i == 4 and face == wolf_right and hands == right_down then return true end
    return false
end

Należało to jeszcze wpiąć w love.update(dt)

for i=1, table.getn(roll_on) do
    -- jeśli jajko się tu toczy
    if roll_on[i] == 1 then
        -- daj następną klatkę animacji
        eggs[i] = eggs[i] + 1
        if eggs[i] == fail_at then
            -- jajko jest na brzegu? A gdzie patrzy wilk?
            if(success_or_failure(i)) then
                score = score + 1
                -- agresywnie zwiększam ilość jaj dla debugu
                if score == 2 then
                    eggs_on_screen = 2
                end
                if score == 5 then
                    eggs_on_screen = 3
                end
            else
                miss = miss + 1
                if i == 1 or i == 2 then
                    failure_left = 1
                else
                    failure_right = 1
                end
                print("Bams", i, failure_left, failure_right)
            end
            -- jajko znika, komora losowania jest pusta
            eggs[i] = 0
            roll_on[i] = 0
        end
    end
end

I tak, krok po kroku, całość zaczęła przypominać pierwowzór. Osiągnąłem swój cel główny: zmarnowałem prawie 5h1. Cel poboczny, tj. zrobić coś, czego nigdy nie robiłem, uznaję też za osiągnięty.

Teraz myślę nad zmianą kariery, napędzany zadufaniem początkującego.

Kod dostępny jest w repozytorium wraz z półproduktami.

PS. jeśli na serio chcecie się czegoś dowiedzieć o projektowaniu gier polecam Wam książki wydane przez moich przyjaciół z „Inżynierii Wszechświetności”, choćby Level design.


  1. I 2h pisząc to, a za 7h muszę przejechać na rowerze 60Km, proszę o współczucie. 


Oczy wielkie jak spodki, a na nich rzęsy jak łyżki

Siedzieliśmy na betonowych schodach z nogami spuszczonymi przez balustradę i rozmawialiśmy o przyszłości. To były czasy kiedy rozmawianie o przyszłości było zajęciem przyjemnym, taśma na nienagranej szpuli życia była zdecydowanie dłuższa. Karolina chciała zostać pielęgniarką lub weterynarzem. Ja planowałem zostać lumpem. Ona została samotną matką, a mnie się udało.

Karolina wyszła na chwilę do mieszkania, by wrócić z herbatą i pudełkiem pełnym papierów. Wyciągnęła z niej jedną kartkę i powiedziała: znalazłam ostatnio list miłosny od Radka. I choć to faux pas czytać cudze listy, dwakroć miłosne, a po trzykroć powtarzać ich treść w Internecie, jedna linijka utkwiła mi w pamięci do teraz.

Listy miłosne w podstawówce mają dwie cechy: próbują naśladować język dorosłego romansu zagapiony z popularnej kultury i kart książek do języka polskiego, ale są jeszcze wystarczająco pozbawione elementu cielesności by nie być „w sedno”, mają też okropną gramatykę i interpunkcję. Gdy przeczytała mi więc zdanie „[…] Twoje czerwone usta oczy” nie mogłem przestać wymyślać zabawnych anegdot o tym, co powoduje, że Karolina ma czerwone oczy. Płacze nad przecinkiem, nie śpi, a może ma nieustanny katar? Debatowaliśmy to aż do wystygnięcia herbaty.

Mijają lata, a ja za każdym razem, gdy trafiam na porównanie w książce odpalam w głowie proces, który modyfikuje ckliwości w głupoty. Nie mogę się tego pozbyć, co pewnie tłumaczy, czemu nie czytam romansów. Za dużo „śmierci autora” na centymetr kwadratowy. A mówię Wam to żeby Was zakazić, może gdy wszyscy będziemy porażeni tą samą przypadłością środowisko medyczne weźmie nas pod uwagę i wynajdą proszki na cynizm. Byle nie w formie czopka.

Miała usta jak jagody: czarne, łatwo było je rozgnieść, pożerały je niedźwiedzie.

Miała oczy jak diamenty: ślepe, rysował jej szkła kontaktowe, sprowadzały nieszczęście na afrykańskie wioski.

Jej piersi jak arbuzy, dawały wiele rozkoszy, gdyby nie te pestki.

Jej łabędza szyja utrzymywał małą głowę, a twarz zakończona dziobem utrudniała picie z filiżanek.

Jej porcelanowa skóra była twarda w dotyku, uniemożliwiała ruch, a kolor nabrała dopiero wypalona w piecu.

Włosy koloru siana: jesienią sino-brunatne, zmatowiałe, martwe.

Była mądra jak sowa: wiedziała wszystko o jedzeniu myszy w locie.

Miała wielkie serce, co martwiło kardiologów.

Śpiewała jak skowronek, monotonnie, bez słów, budziła cię świtem.

Była potulna, jak baranek. Nocą rodzice spuszczali na nią psy by zapędzić ją do łóżka.

Rzuciła mu perłowy uśmiech. Małe, okrągłe zęby świeciły się tęczowo gdy padało na nie światło pod dobrym kątem.

EDIT: ojej, wymsknęło się z drafts. No trudno.


Rezygnacja: automatyzacja

„Zautomatyzowałem zadanie, które zajmowało mi 2 minuty raz na miesiąc. Poświęciłem 8 godzin pracy i już mogę je robić w minutę. Póki nie zmienią się parametry.”

Na dworze administracja prowadzi wiosenną wojnę z okalającym nas betonem. Piechota uzbrojona w młoty pneumatyczne kruszy wszelki opór. Zwycięstwo jest blisko, może jeszcze tydzień. Cywilne ofiary wojny, czyli ja, są wliczone w koszta tej bitwy.

Nie mogę tak pracować, co znaczy, że muszę poczekać na wieczór, aż świat pójdzie spać. Muszę jednak jakoś wypełnić te godziny. Co mogę zrobić? Tylko rzeczy głupie. Głupie rzeczy nie wymagają myślenia, a zajmują tyle samo, co rzeczy dobre!

Kilka dni temu wysłałem moim kolegom-programistom komediowe wideo o tragediach czających się w świecie mikroserwisów. Nigdy nie robiłem mikroserwisów, ale mam bardzo mocne opinie na ich temat dlatego staram się uzyskać społeczny dowód na moje racje od ludzi, którzy dłubią je każdego dnia. Podczas krótkiej wymiany opinii powiedziałem, że DevOops powinni nauczyć się od projektantów i DTP-owców i nazywać serwisy tak, jak oni nazywają pliki. Losowe liczby, daty, wulgaryzmy.

Zawsze lubiłem patrzeć na ich metodologię. Historia wprost wycieka z takiej nazwy. Czy jest data? Może to już któryś dzień walki? Czy jest _final, i czy jest coś po _final? Nieodpowiedzialny hurra-optymizm został ukarany. Czy są „ozdobniki”, które powodują, że sortowanie katalogu wyciąga je na górę?

Wymyśliłem, że jedynym dobrym zajęciem jest przygotowanie kodu, który mógłby zostać zamieniony w mikroserwis ułatwiający utworzenie takich nazw. Użyłem najlepszej technologii AI (tak na serio to ML (tak na serio to if(random() >= .5))) i utworzyłem następujące rozwiązanie™.

params = {
    'crazyness': .5, 
    'optimism': .6, 
    'vulgarity': .7, 
    'timekeeper': .6, 
    'decorations': .6
}

from despyration import despyration

files = [
    'podanie_o_nienapierdalanie_mlotem.docx',
    'reklama-plynu-do-mycia.psd', 
    'umowa_o_dzialo.pdf'
]

[despyration(f, **params) for f in files]

A oto rezultaty:

2020-05-19-PODaNIe_O_NiENapIerDalanIE_mloTem!!!.docx

rEKLAMa-plYNu-DO-mYciA__fuck-2020-05-19@.psd

umowa_o_dzialo_final_final_final+++.pdf

Myślę, że udało mi się oddać ducha, choć pozostaje wiele pracy. Trudno jest zasymulować prawdziwy proces, sztukę, intuuicję oraz frustrację, która rodzi te nazwy. Stoję na ramionach gigantów, a oni mi mówią żebym się odwalił, co to w ogóle znaczy wspinać się po ludziach, zwłaszcza teraz, w epoce zarazy.


To się nie nadaje do druku

Każda przygoda ma swój punkt zapalny. U mnie były dwa. Pożyczyłem okropnego tableta z Windowsem przyjacielowi, którego laptop nagle wykitował. To okropny tableto-laptop. Okropna klawiatura, 2GiB pamięci, 32GiB wlutowanego dysku i 64GiB karta, którą dodałem żeby się coś dało zrobić pomiędzy aktualizacjami. To dlaczego go mam? Waży mniej niż kilogram, więc nadaje się do plecaka, gdy idę w świat (wszystkie systemy czekają z kręceniem powrozu na swoje karki do momentu w którym moja stawiam nogę na dworcu), dodatkowo ładuje się z USB.

Ma też niezły ekran, więc używam go do czytania tych średnio-długich tekstów. Pożyczając go pozbawiłem się tej opcji.

Kupiłem też drukarkę. Wyrzucenie z biur klientów przez zarazę odebrało mi możliwość bezczelnego podsyłania ludziom e-maili o tytule „wydrukuje mi Pani?”. Do tego moja lokalna poczta jest bardzo popularna, więc każdego dnia o każdej godzinie wystaje z niej ludzka gąsienica petentów. Dowiedziałem się, że można drukować własne znaczki, więc zakup wydał się jeszcze bardziej usprawiedliwiony.

Bo kto ma teraz lepsze znaczki niż ja?

Zdjęcie znaczka

Pomyślałem, że połączę utratę urządzenia do czytania i nabycie urządzenie do drukowania w nowe rozwiązanie. Wydrukuję sobie zaległe teksty i będę sobie w spokoju czytał do kawy. Cóż może być trudniejszego niż wydrukowanie dokumentu? Z pewnością nic.

Zbadajmy jak to wygląda!

Wybrałem do testów serwisy, których artykuły gniją w moim Pocket’cie. Nie testowałem prywatnych stron, bo wydaje mi się to nie fair. Nie tylko teksty są zwykle krótsze, ale też nie mogę oczekiwać, że mają budżet/czas na picowanie swojej oferty. Dodatkowo, teksty „prywatne” są głównie o programowaniu, więc mają wartość dodaną na ekranie komputera, głównie naukę metodą Kopiego-Pasty.

the Bellows: Tekst wygląda dobrze. Nagłówek, wraz z elementami nawigacji powtarza się co stronę. Takoż guzik udostępniania. Stopka dodaje całą stronę A4 niczego ważnego.

Places Journal: Tekst sformatowany jak na stronie, mała czcionka, elementy takie jak odgrywaczka audio pozostały. Stopka dodaje bezużyteczną stronę. Nieczytelne, bezużyteczne.

Damage: Układ praktycznie doskonały. Niestety, stopka znów dodaje bzdety, których nikt nie potrzebuje na kartce.

Claremont Review of Books: <div> z reklamą ciągnie się całą długość dokumentu, powodując, że tekst zajmuje 70% strony i jest przesunięty do prawego brzegu. Dwie strony stopki. Koszmar.

The Point: Idealnie. Pierwszy przypadek, gdy wszystko jest gotowe. Tekst, który próbowałem dodał pustą stronę A4, ale ponieważ ostatni paragraf kończył się idealnie na poprzedniej zakładałem, że to przykry zbieg okoliczności. Zbadanie innego tekstu potwierdziło. Pierwszy sukces!

Harper’s: Nagłówek zawiera logo i pustą stronę. Potem jest strona tekstu, która się urywa, gdyż jest „zabezpieczone JavaScriptem”. Siadaj, dwója. Jutro przyjdź z matką.

Public Books: Font à la „Retina, ale na kartce”, dwie strony stopki. Znacie tę piosenkę.

Times Literary Supplement: Szatan, piekło, antychryst. Czyli Single Page Application. Jedna strona nagłówków, wraz z banerem polecającym żebym dał im dolara, jedna strona tekstu, ucięta, a na niej baner proszący o dolara. Stopka. I baner proszący o dolara.

Ars Technica: Sztywna nawigacja, ślepe galerie, paginacja. Desperacja.

Increment: „drukuje się” jedna strona.

Jeden dobrze. Jeden OK. Trzy powodują, że drzewa ronią łzę. Cztery nawet nie próbują udawać, że ktoś kiedyś myślał o papierze.

Umówmy się: gówno wiem o projektowaniu wyglądu, potrzebach „spieniężania” treści i innych problemach z którymi mogą się borykać magazyny internetowe. Jestem tylko kolesiem, który chciał sobie wydrukować trochę tekstu do czytania. Te rezultaty podsycają jednak moją nienawiść do tumiwisizmu współczesnej sieci.

Jeśli zboczysz z wyznaczonej ścieżki choćby o krok nagle znajdujesz się na brzegu klifu, a za Tobą materializuje się dobrze ubrany konsultant, który uprzejmymi gestami zachęca cię do skoku. Najlepiej bańką do przodu.


Chromecast dla Linuksiarza

Przyznaje się, przegrałem. Jest sobota, a ja miałem dobry pomysł na zabawny tekst. Tekst, który wymagał może zbyt wiele kompetencji, których nie posiadam. Od siódmej rano walczyłem z oprogramowaniem do edycji wideo, zbyt starym cmake, nagrywaniem akcelerowanego wideo, faktem, że pliki instalacyjne wyparowały mi z archiwum i tym, że firma, od której kupiłem oprogramowanie mentalnie jest w 1990 i strzeże swoich fantów jak oczka w głowie.

Rzuciłem się z motyką na słońce, a na dodatek słońce było uzbrojone i mnie obstrzelało ostrą amunicją. Wybiła trzynasta, a ja mam jedno źle skadrowane wideo — testowe wideo, nawet nie to, co chciałem — które się chybocze, dyga i ma zły aspekt. Czas powiedzieć sobie: OK, zjedz coś, usiądź. Po co się denerwować.

To prowadzi nas do miejsca, w którym, jak mawiają Amerykanie jestem all dressed up with nowhere to go. Pójdę więc po linii najmniejszego oporu i po prostu opiszę, co mam na biurku.

Chromecast

Chromecast to odbiornik strumieni medialnych, produkowany przez Google. Małe pudełeczko, które wpina się do telewizora/monitora przez HDMI i pozwala na przesyłanie audio/wideo z rozmaitych serwisów. Mimo mojej niechęci do dawania Google wiazdu do mojego życia, podobnie jak w przypadku porannego projektu, trzeba wiedzieć kiedy się poddać. Mimo niemiłej mi natury produktu nie udało mi się zbudować niczego samemu, bądź też z półproduktów, co byłoby choćby w ułamku tak funkcjonalne.

Normalni ludzie mogą używać aplikacji, które wspierają Chromecast. Praktycznie każda popularniejsza aplikacja mobilna (YouTube, Spotify, Netflix, etc) potrafi rzucić swoje bity na ekran. Działa to bardzo dobrze. Dla normalnych ludzi z komputerami biurkowymi jest Chrome, który będąc IE6 dla nowożytności1, potrafi przesyłać zawartość zakładki.

Co pozostaje dla ludzi z pryszczami, Firefoksem i terminalem? Bardzo dużo bardzo dobrych opcji, o dziwo!

catt

catt to program po którego opisaniu mógłbym skończyć tę notkę. Robi praktycznie wszystko, czego mogę chcieć. Potrafi odpalać strumienie serwisów, które nie wymagają autoryzacji (YouTube, Twitch, Vimeo, etc), oddaje kontrolę nad głośnością, wspiera przewijanie, listy odtwarzania, kolejkowanie, potrafi użyć wbudowanej w Chromecast przeglądarki, by wyświetlić stronę WWW, a ostatecznie odtworzyć plik z dysku lokalnego tworząc serwer HTTP i serwując klatki.

Kilka przykładowych zaklęć:

  1. catt cast https://www.youtube.com/watch?v=0cQA6d3adPs — odtworzy nam wideo znajdujące się pod tym adresem

  2. catt seek 1:00 — przesunie nas na osi czasu

  3. catt add https://www.youtube.com/watch?v=h0rSYEoBMYM — doda następny plik do kolejki (tymczasowo wspierany jest chyba tylko YouTube)

  4. catt cast_site https://fuse.pl/beton/chromecast-linux.html — wyświetli stronę

  5. catt cast Pobrane/HoliłudzkiFilm.Grupa.Nick.x264.mkv — wyświetli lokalny plik

Dostępny jest przez PyPI oraz jako źródło na GitHubie.

Na boku: znalazłem ciekawe zastosowanie dla catt cast_site. Opisywane wcześniej ttyd może zostać „rzucone” na ekran, co wyciąga nam sesję terminala. Oczywiście, nie ma jak z nią działać, ale wymyśliłem obejście tego problemu przy użyciu screen. Na początku tworzymy i odczepiamy sesję przez screen -d -m -S shared, następnie odpalamy ttyd screen -x shared, to startuje nam usługę. Używamy catt cast_site aby wyświetlić stronę ttyd. Teraz wystarczy odpalić lokalnie screen -x shared i już możemy pisać. A tam już co Wam się podoba, htop czy tail -f. Działa znośnie.

VLC

VLC potrafi odtwarzać do Chromecasta, podobnie jak catt. Z tą jednak różnicą, że umie przekodowywać pliki w locie (kosztem procesora). Jest to szczególnie przydatne w sytuacji, gdy mamy pliki „z Internetu”. AVI, mkv, mp4 z x265, rzeczy do których Chromecast nie ma kodeków. Dodałem sobie alias vlcast dla własnej wygody:

cvlc --sout "#chromecast" --sout-chromecast-ip=IP --demux-filter=demux_chromecast

mkchromecast i pulseaudio-dlna

Oba programy potrafią utworzyć sink (jak to będzie po naszemu? Przecież nie „zlew”?) PulseAudio dzięki czemu dźwięk odtwarzany lokalnie może zostać przesłany do urządzenia będącego pod kontrolą Chromecasta. pulseaudio-dlna jest dużo bardziej monotematyczny i robi tylko to, mkchromeast ma więcej funkcji, ale żadnej nie spełnia tak dobrze jak catt. Oba znajdują się w oldoldstable2, zakładam więc, że Wasza dystrybucja też je posiada.

pychromecast

Nic nie stoi na przeszkodzie żebyście sami spróbowali porobić własne zabawki. Odpowiednia biblioteka, która implementuje API Chromecasta pozwoli Wam przejąć kontrolę nad urządzeniem. Gdybym miał kiedyś zrobić info-kiosk to pewnie rozważyłbym takie rozwiązanie. Mam nadzieję, że nie będę nigdy robił info-kiosku.

pychromecast jest oczywiście na PyPI i pod postacią źródła na GitHubie.

Jak widzicie, zebranie wystarczająco dużej ilości narzędzi odpalanych z terminala powoduje, że osiągnąłem stan wyjściowy normalnych ludzi, którzy klikają w Chrome „prześlij na Chromecast”.

Chromecast

Największą wadą Chromecasta jest to, że gdy nie odtwarza treści to przechodzi w tryb „elektronicznej ramki ze zdjęciami” pokazując mi najlepsze momenty moich wypraw, zachody słońca, jelenie, lasy, reklamówki z piwami. W obecnej sytuacji zmienia to znak zapytania w zdaniu „pamiętasz, jak było fajnie” na wykrzyknik.


  1. Best viewed on IE6 in 1024x768 

  2. Nie mam serca żeby się zebrać do aktualizacji mojego głównego laptopa. Działa? Działa. Chyba, że chcę cmake z tego tysiąclecia. 


UserLAnd: chroot w kieszeni

Gdybym zeskanował teraz domową sieć trafiłbym pewnie na nie mniej niż cztery komputery z Linuksem. A jeśli starczyłoby mi gniazdek, to mogę dobić i do dziewięciu. To nie mówi zbyt wiele o samym systemie, a więcej o mojej nieumiejętności używania innego środowiska. Może to Syndrom sztokholmski, ale czuję się z tym doskonale! Dla niektórych „rok Linuksa na biurku” zaczął się w 1997.

Niedawno zapytano mnie „czy możesz nauczyć mnie Linuksa”. Trochę się tym strapiłem, bo mogę oczywiście, tylko nie wiem, co rozumieć przez „Linuksa”? Czy uczenie kogoś powłoki, potoków i pisania pomocniczych skryptów to nauka Linuksa, czy na przykład Basha? Czy stawianie serwera HTTP to nauka Linuksa? Embedded? Rzuciłem okiem na spisy treści już napisanych książek typu „Wstęp do…” i wyszło mi, że zwykle jest to książka w której autor opisuje jak okiełznuje się jego ulubioną dystrybucję.

Dla mnie pytanie o „naukę Linuksa” jest jak pytanie o „naukę młotka”. Do stolarki, budowlanki, morderstwa powodowanego pasją? Nie wiem, jak do tego podejść. Żeby nie utykać w filozofii tego pytania przeskoczyłem na grunt twardszy, jak uczyć kogoś, kto może nie ma wystarczająco zasobów i intuicji żeby mógł wybrać jedną z miliona opcji. Zainstalować na komputerze jako główny lub drugi system? Ciężko. Wirtualna maszyna? Trochę pokracznie. Raspberry Pi? Ból tyłka. WSL? Obiecujące, ale mam zero doświadczenia i jak coś się wywali to nie będę umiał odkręcić.

I wreszcie wpadłem na rozwiązanie, które mi się spodobało.

Każdy telefon z Androidem chodzi pod kontrolą kernela Linuksa i jakiś śmieci na górze, które służą do wyświetlania reklam. Teoretycznie nic nie stoi na przeszkodzie żeby „dorobić” telefonowi przedrostek GNU/. Przez lata było kilka takich projektów, nigdy nie przykładałem do nich jednak uwagi, gdyż jak wspomniałem na górze, mam dziewięć innych komputerów. Przebiegłem się po Biedronce Aplikacji i znalazłem jeden, który mi wybitnie odpowiada: UserLAnd.

Po pobraniu aplikacji i jej odpaleniu zostaniemy powitani przygotowanymi dystrybucjami, które możemy odpalić na swoim telefonie. Są to po prostu drzewa plików spakowane i gotowe do pobrania. Osobiście testowałem tylko Debiana, ale zakładam, że inne działają tak samo. Poza surowymi dystrybucjami można porać pojedyncze aplikacje, takie jak IDE Pythona, Firefoks, GIMP czy zork.

Wybór dystrybucji

Po kliknięciu rozpocznie się proces pobierania i ekstrakcji. Trzeba zwrócić uwagę na to, że pliki te trafią na miejsce z którego mogą być czytane przez inne aplikacje (albo nie: nie jestem na bieżąco z Androidem) więc składanie ważnych danych takich jak hasła, klucze SSH i dane osobiste nie jest polecane.

Konfiguracja użytkownika

Następnie zostaniemy poproszeni o podanie nazwy użytkownika i haseł do SSH i VNC. Nic prostszego.

Tworzenie połączenia

Potem deklarujemy jak się chcemy łączyć z naszą maszyną. Osobiście, co nie jest dziwne, preferuję SSH. Jak będę potrzebował okienek to zawsze mogę dodać -X.

I to wszystko. System podniesie się po chwili i możemy na niego wskoczyć. Ponieważ aplikacja nie może przypisać sobie niskich portów domyślnym jest 2022. Podczas uruchamiania sesji aplikacja będzie chciała użyć jednego z dostępnych klientów aby ustanowić połączenie na telefonie, ale możemy to zignorować. Ekran dotykowy plus magiczne | zaklęcia | z --toną --parametrów to nie jest świat w którym chcę żyć i którego i Wam nie życzę.

λ ~/ ssh emil@192.168.1.204 -p 2022
emil@192.168.1.204's password:
Welcome to Debian in UserLAnd!
emil@localhost:~$ uname -a
Linux localhost 4.9.117+ #2 SMP PREEMPT Thu Feb 13 01:10:05 CST 2020 aarch64 GNU/Linux

I już mamy „prawdziwego Linuksa” chodzącego pod kontrolą kernela dostępnego w Androidzie. Czy to metoda „lepsza” niż te, które wspomniałem wyżej? Wątpliwe. Ale wydaje się najmniej skomplikowana i inwazyjna.


Światy wytworzone

Kiedy usłyszałem jak dziecko koleżanki z pracy relacjonuje fantastycznego gola strzelonego przez Christiano Ronaldo ucieszyłem się w duchu. Było lato z turniejem piłkarskim i myślałem, że młody człowiek siedzi i ogląda zmagania na boisku, jak i ja. Nie mając specjalnego talentu do rozmawiania z dziećmi (choć kiedyś byłem dzieckiem jeśli wierzyć zdjęciom, pewnie zapomniałem co one robią w wolnym czasie) zagadałem do niego o inną spektakularną bramkę. Popatrzył na mnie podejrzliwie, coś burknął i wycofał się w kierunku matczynego biurka.

OK, nie wiem czego się spodziewałem, dyskusji o funkcji raumdeutera we współczesnym, ofensywnym futbolu? Nic to. Po pewnym czasie, łowiąc jednym uchem strzępki monologów odkryłem powód naszego braku porozumienia.

Ja mówiłem o Ronaldo z krwi i kości, a on mówił o jego podobiźnie zbudowanej z poligonów, którą kieruje grając w FIFĘ na konsoli. I to on jest tym prawdziwszym, choć w rzeczywistości to tylko faksymile Portugalczyka.

Trochę się zeźliłem, a w brzuchu zrobił mi się koktajl z ogórków konserwowych i jogurtu. Raz, że nie kumam bazy. Dwa, że jak tak można żyć z fałszywym obrazem kogoś! Kiedy wyszedłem z biura nie mogłem przestać myśleć o tym, jak dziwny to świat w którym można żyć kompletnie w oparach cyfrowych fatamorgan. Winiłem oczywiście współczesność, rozpasany konsumeryzm i głupotę dzieci, które grają w gry, których nawet nie trzeba wczytać z taśmy.

Im dłużej jednak nad tym myślałem tym bardziej dobijała się do mojej świadomości myśl w tle. I szeptała cicho Newcastle United… Newcastle United… — ale co, Newcastle? Ach.

Odkryłem nagle, czemu tak bardzo mnie ta myśl gnębiła od rana.

Przez kilka lat moim życiem zawładnęła gra komputerowa: Championship Manager (teraz: Football Manager). Był to symulator ekonomiczno-sportowy, który pozwalał się wcielić w rolę managera jednego z klubów angielskiej ligi. Gra z daleka wyglądała jak arkusz kalkulacyjny i nikt w temacie nie mógł zrozumieć czemu biały pasek wypełniający się po prawo powoduje, że wpadam z szału w ekstazę by zaraz wsiąść w pociąg powrotny do szału. Jednej wiosny postanowiliśmy z moim boiskowym kolegą że już się wystarczająco nauczyliśmy w tym roku szkolnym i możemy wziąć dwa miesiące wolnego i poświęcić się rzeczy, która jest na serio ważna: graniu od rana w Championship Managera.

Od razu był problem. Obydwaj chcieliśmy prowadzić Manchester United, który był tytanem piłkarskim tamtych lat. Kto by nie chciał być Fergusonem podwórka? Rzuciliśmy monetą i wyszło, że przegrałem. Musiałem wybrać inną drużynę. Mając nikłe pojęcie o angielskiej piłce postanowiłem przyjąć następującą heurystykę: Machester. United. Manchester City jest dziadowskie. Więc to nie pierwszy człon decyduje o sukcesie w piłce. W takim razie jasne jest, że to United jest sygnałem „to dobra drużyna”. Wybrałem więc Newcastle United.

Championship Manager

I zaczęliśmy grać. Po dwóch miesiącach okazało się, że nasze szkoły nie rozumieją potrzeby walki o ligowe tytuły i obaj zostaliśmy na tym samym roku. Co nam wybitnie pasowało, bo to znaczyło, że mamy dwa miesiące wakacji i dodatkowo mamy już wszystkie książki i zeszyty, więc nie musimy się nawet przygotowywać! Lekko 4 miesiące luzu, świetnie.

Jak pisałem wcześniej granie w Championship Managera wymagało wyobraźni. Cała gra to tylko kolumny cyfr i trochę tekstu. Ludzka natura lubi historie, uczłowieczaliśmy więc te ciągi znaków, dorobiliśmy się własnego zestawu przesądów, mieliśmy drużyny z którymi nie lubiliśmy grać. Wspominaliśmy ciekawe momenty w naszych karierach. Żyliśmy w alternatywnej rzeczywistości, gdzie nie pojedynczy piłkarz, a cała drużyna, była ofiarą naszej nieskrępowanej fantazji.

Statyski Matta Le-Tissiera

Ponieważ byłem bramkarzem szczególnie urzeczywistniałem w mojej głowie wyczyny mojego bramkarza Srok1. Któregoś dnia wyznaczyłem go do strzelania rzutów karnych. Strzelił. Dla żartu zostawiłem go jako egzekutora. I strzelił następnego. I następnego. A jego legenda rosła. Kiedy chciał zakończyć karierę próbowałem przerobić stan gry tak żeby został. Co się nie udało.

Przez lata myślałem o nim, o Hopperze, moim strzelającym karne bramkarzu Newcastle z rozrzewnieniem. Tyle wspaniałych wspomnień. I gdy odbiłem się od katalizującego incydentu z Ronaldo i synem koleżanki zdałem sobie sprawę, że… ja nie wiem nawet jak Hopper wygląda. Doszedłem do domu, usiadłem do komputera. OK, jest, widzę. Strasznie mało coś o nim. I w trzecim linku na Google czytam, co następuje.

Hooper started with a clean sheet against West Ham. But things soon declined as he was beaten by a long-range last- minute free-kick by Matt Le Tissier in a 2-1 loss.

Newcastle United’s Worst XI: Worst goalkeepers

Okazał się, że Hopper nigdy nie był bohaterem, nigdzie, poza moją głową. Był kiepskawym bramkarzem, który wyleciał z startowej jedenastki i praktycznie nigdy niczego nie dokonał. Ja się oburzyłem na kogoś, kto strzela bramki w FIFA, a sam mam 12 sezonów fałszywych wspomnień, które sobie opowiedziałem patrząc w kilka rubryczek. I kiedy on symuluje człowieka sukcesu odnosząc sukces, ja wymyśliłem człowieka sukcesu z piłkarskiego nikogo.

Kto jest bez winy niech pierwszy rozpocznie grę długim wyrzutem.


  1. „Sroki” to przydomek Newcastle United 


Szybkie życie

W Internecie bulgocze mem o tym, jak to w czasach zarazy mądrzy ludzie dokonali wielkich czynów i jakie ważne dzieła wyprodukowali. Oczywiście, jak w każdym inspirational porn pomijane są pewne fakty. Na ten przykład twórcy wspaniałych rzeczy nie wdzierali się na intelektualną scenę, a byli na niej już od dawna. Albo to, że nie musieli się w większości przejmować czynszem, zamkniętą szkołą, ani też doświadczać pierwszy raz zdalnej pracy.

Ja celuję dużo niżej. Każdemu jego dzieło w miarę możliwości.

Znudzony i niechętny pracy kręciłem się tu i tam, szukając ciekawego zajęcia. Wyciągnąłem stare numery „Młodego Technika”. Przeczytałem kawałek tekstu o tym jak działają świece zapłonowe. Nie było ognia w moim sercu. Wyciągnąłem Tołstoja, ale na pierwszej stronie było od razu pięć długich, dwuczłonowych rosyjskich nazwisk i wiedziałem, że nie ma szans żebym się odnalazł w tym bez tablicy korkowej i czerwonych sznurków do utworzenia relacji pomiędzy bohaterami historii.

Zastanawiałem się jak wygląda miasto. Od tygodnia wyszedłem z domu trzy razy. Na 5 minut. Dokupić alkoholu i jajek. Jajka jem na śniadanie, alkohol na kolację. Te dumania przypomniały mi o zapomnianej dziś instytucji „publicznych webcamów”. Kiedyś były bardzo popularne, ludzie oglądali obce miasta, kurze fermy, małpy, psy i co jeszcze komu wpadło do głowy. Wpisałem więc frazę w wyszukiwarkę i po chwili widziałem swoją okolicę. Popatrzyłem chwilę jak pan chodzi z miejsca w miejsce, zaparkował samochód, a później odjechał. Pan, śledzony nieustępliwie przez śmieciarkę na światłach, opróżniał kubły. Świat istniał, choć nie wyglądał ciekawie.

Pomyślałem, że mógłbym sobie rzucić to wideo na mały, dziesięciocalowy telewizorek, który stoi w kącie mojego biurka i zabawia mnie wyświetlając anime z lat 80 kiedy pracuję. Stworzyłbym sobie wirtualne okno na ulicę.

Biurko

Ostatecznie, gdzieś tam pod spodem, jest pewnie jakiś strumień wideo, który można zawinąć.

Poszedłem sprawdzić i oczywiście, jest jakiś. Po pierwsze lista M3U, po drugie powtarzający się cykl fragmentów wideo. Dla testu skopiowałem jeden z linków i podałem do wget. Coś się ściągnęło. mplayer odtworzył 2 sekund wideo z kamery. OK, czyli to tak działa. Wideo jest serwowane w tej serii plików i po osiągnięciu ostatniego, 92 fragmentu, lista zawraca do pierwszego, oznaczonego 42.

Dobra, a czy da się je jakoś seryjnie ściągnąć? Znaczy, na pewno się da, ale tak żebym potem mógł coś z tym zrobić? Otworzyłem nową powłokę terminala i wydałem klasyczne zaklęcie mkdir, git init, vi.

Wpierw wymyśliłem bardzo skomplikowany sposób pobierania tego, z wątkami, sprawdzaniem ETag i kto wie, co jeszcze. A potem uznałem, że głupia metoda jest równie dobra, a doprowadzi mnie do sukcesu szybciej.

Czyli tak:

  1. Wezmę wszystkie kawałki klipów w momencie odpalenia
  2. Zapiszę je sobie na boku
  3. Zobaczę ile czasu mi to zajęło i odejmę od maksymalnej długości wszystkich klipów
  4. go to 1
import requests # klient HTTP
from time import sleep # chcemy pauzować

# wpisany na twardo adres z którego będę pobierał informacje
URL = 'https://cdn-3-go.toya.net.pl:8081/kamery/lodz_piotrkowskaschillera_0{}.ts'

# formad do zapisywania, index poprzedzony dopełnieniem 12 zer
OUTPUT = 'output/{:012}_frame.mpg'

""" Żeby się było łatwiej kręcić, wartości [42…91] w liście """
frames = range(42, 92)

idx = 0
while True:
    # Cała lista otwarzania ma (92-42)*2 sekund 
    rotation_time = len(frames) * 2

    #Kiedy zaczynamy proces
    start_time = datetime.now()

    for frame in frames:
        print("Grabing {}".format(frame))
        # Klasyczny HTTP GET / i zapisanie pliku docelowego
        response = requests.get(URL.format(frame))
        if response.status_code == 200:
            with open(OUTPUT.format(idx), 'wb') as f:
                f.write(response.content)
            idx = idx + 1
    # Jak przelecieliśmy wszystkie ramki to czekamy resztę sekund, aż 
    # się wszystkie podmienią
    after_downloading = datetime.now()
    remaining_seconds = rotation_time - int((after_downloading - start_time).total_seconds())
    print("Clips fetched, sleeping for {}".format(remaining_seconds))
    # …i do łóżka
    sleep(remaining_seconds)

Udało się za pierwszym razem. Pokombinowałem jeszcze z nazwami i takimi tam, ale wyglądało na to, że całość ściągnęła się bez problemu. No dobra, ale co z tym teraz zrobić? Chwilowo nie miałem pomysłu, więc sklonowałem repozytorium na lodówkową RaspberryPi i pozwoliłem się jej kręcić, naciągnąć mi nieco danych.

Minął czas, a ja wymyśliłem. Zrobię tzw. timelapse czyli przyśpieszone wideo z źródłowych fragmentów. Mam w vimwiki całą stronę poświęconą rozmaitym zaklęciom, którymi można przymuszać ffmpeg do robienia fantastycznych rzeczy.

Kiedyś spędziłem wieczór na brzegu zalewu żeby zamienić 6 minutowy filmik z zachodu słońca w taki, który byłby ustabilizowany, krótszy i jeszcze miał muzykę. Wyzwanie: tylko przy użyciu połączenia SSH do domu. Wyszło OK, ale nie spektakularnie.

Nie było sensu zaprzęgać do tego Pythona, wsadziłem całość w prosty skrypt Basha. 1

#!/bin/bash

# Wczytaj z katalogu output wszystkie pliki *.mpg
for file in output/*.mpg ; do
    # Weź obecną nazwę, utnij ją przy kropce, dodaj png
    new_file=`echo $file | cut -d. -f1`.png
    echo $new_file
    # Weź pierwszą klatkę animacji i zapisz pod nową nazwą
    ffmpeg -i $file -vf "select=eq(n\,0)" -vframes 1 $new_file
done

# Zakoduj nowopowstałe klatki w mp4/h264 żeby się przeglądarki nie czepiały
ffmpeg -pattern_type glob -i 'output/*.png'  -an -vcodec libx264 -pix_fmt yuv420p -profile:v baseline -level 3 out.mp4

Po kilku testach postanowiłem, że osiągnąłem znośny efekt (mimo kilku momentów w których uzyskałem duplikaty serii klatek. Mogłem usunąć to post factum ale jak mówił Bob Ross, “There are no mistakes, just happy accidents“), wysłałem wszystko na RaspberryPi i postanowiłem dać się całości kręcić przez noc.

Wysłałem jeszcze e-mail do mojego przyjaciela Łukasza żebrząc o jakiś utwór muzyczny z mojej ulubionej płyty jego projektu, Coffee Sounds. Na moje szczęście zgodził się, za co chcę mu jeszcze raz podziękować.

Wstałem o piątej, zebrałem 17200 klatek i oto przed Wami efekt bezcelowej, nocnej dłubaniny. Łódź, Piotrkowska, Pasaż Schillera. Od 22:30 do 8:00.

  • :15 zaczyna padać deszcz, ulice się świecą odbitym światłem latarni
  • :37 „Do Poloneza? Ale Panie Władzo, ja nie tańczę”
  • 3:15 wschód słońca

Te, zdecydowanie zbyt rzadkie, momemty w moim życiu gdy zamieniam „czy da się?” w „da się” przy pomocy śliny, mchu i oglądania problemu przez palce sprawiają mi zawsze najwięcej radości.

Polecam Wam zaprzęgać komputery do bezużytecznej, kreatywnej pracy. Komputer jako odpalarka do przeglądarki to takie marnotrastwo nieomal nieograniczonej mocy.

Czas szybciej płynie, gdy dłubiesz. Lokatorzy więzień próbujący wykonać podkop też Wam to powiedzą.


  1. Potem trochę pozmieniałem rzeczy, całość dostępna jest w repozytorium git 


ttyd

W dzieciństwie jednym z moich ulubionych formatów kawałów był ten o magicznej istocie, która spełnia życzenia w sposób dosłowny, obracający pragnienie petenta w jego nieszczęście.

Żołnierz poszedł łowić do pobliskiej rzeki i zdziwił się, gdy dyndająca na końcu żyłki ryba przemówiła: „Oszczędź mnie, a spełnię Twoje życzenie”. Długo się nie zastanawiał i powiedział rybce: „Chcę być odznaczony ważnym orderem”. Puf! Gdy dym — jasny sygnał spełnionego zaklęcia — opadł, żołnierz zauważył, że stoi w okopie. Po lewej flance przedzierają się nieprzyjaciele, po prawej to samo. A frontem czołgi i karabiny maszynowe. Słyszy hasło „Naprzód!” i ściana kamratów popycha go do walki. Biegnąc myśli, „No kurwa, pośmiertny dała”.

Należy pamiętać żeby uważać czego się pragnie. Spełnienie może być gorsze niż pragnienie. Bo weźmy na przykład jedno z pragnień w sferze technologii: żeby napisać aplikację raz i odpalać ją na wielu różnych systemach. Przez dekady podejmowane były różne próby. A to systemy, które są kompatybilne na poziomie źródeł w stopniu wystarczającym by proces portowania był niezbyt bolesny (C/PM, POSIX), albo platformę sprzętową (MSX, czy potem x86), albo maszyny wirtualne i bytecode (Z-Machine czy JVM).

Jak się to wszystko powiodło? Trudno powiedzieć, to sytuacja jak z za krótką kołdrą. Jednym marzł nos, innym stopy, ale ogólnie zgadzano się, że kołdra jest dobrym pomysłem i któregoś dnia uda nam się uszyć taką o zadowalającej długości.

I wtedy ktoś poszedł wyłowić tę złotą rybkę siecią. I rybka dała nam uniwersalną platformę aplikacji: przeglądarkę internetową.

Ludzie często błędnie rozumieją ewolucję, myślą, że obecny stan wynika z jakiegoś planu, a nie tylko wypadkowych czynników, które promują jakąś właściwość nad inną. Tak samo w przypadku przeglądarki jako platformy aplikacji rzeczy nie rozwijały się pod okiem bogów, a były wypadkową dostępności, pewnej wolności, ceny i kilkuset sprytnych gości, którzy ciągnęli kołdrę w kierunku własnym, czyli słusznym. Tak jak w przypadku ewolucji organizmów prowadziło to czasem do dziwnych rezultatów: nasze oczy widzą świat do góry nogami i potem kradną cykle mózgu na obracanie obrazu, jest też JavaScript.

Nie da się ukryć, że jestem internetowym sceptykiem. To naturalna droga, którą przebywa każdy entuzjasta, gdy okazuje się, że „jego dziecko” pali za szkołą, przynosi same jedynki i nie chce zostać filozofem etyki medycznej. Głębokie rozczarowanie równe pokładanych nadziei.

Większość dnia, a już z pewnością jego produktywną część, spędzam ślepiąc w terminal tekstowy. Znajduję spokój w środowisku, które pokornie czeka mrugając na mnie zalotnie kursorem, aż powiem mu czego chcę. Proste aplikacje, które komunikują się tekstem. Kontrola, automatyzacja i surowa estetyka. I prawie ezoteryka, którą ciężko jest wytłumaczyć ot, tak.

A ten przydługi wstęp ma służyć wyłącznie temu żebym Wam mógł powiedzieć o moim odkryciu, które cieszy me serce i być może ucieszy Wasze. Jest to ttyd, serwer podający terminal przez przeglądarkę.

ttyd to serwer napisany w C, który serwuje jednostronicową aplikację opartą o xterm.js i WebSockety. Jest trywialna w użyciu i działa spektakularnie dobrze. Próbowałem praktycznie wszystkiego, czego używam na terminalu, nawet najbardziej fikuśnych aplikacji. Wystarczy wpisać na terminalu:

ttyd -p 3000 aplikacja

…i już na http://localhost:3000 mamy uruchomiony wirtualny terminal z którego możemy nią operować. Wspiera SSL, BASIC AUTH, autoryzację certyfikatami (bardzo przydatne w pewnych okolicznościach), tryb „tylko do odczytu”, pojedyncze i wielokrotne sesje.

Po co?! O dziwo znalazłem wiele zastosowań!

Po pierwsze używam tego do moich różnych eksperymentów z RaspberryPi. Nie wszyscy mają pod ręką klienta SSH i wystarczająco samozaparcia by zmieniać konfigurację, ale wystarczy, że powiem im „kliknijcie na ten adres i wpiszcie informacje”, podłączę skrypt w powłoce pod ttyd i voilà. I tak, mógłbym napisać webaplikację, ale zapewniam Was, że nawet używając mikroframeworku zejdzie mi dłużej dłubiąc HTML niż kilka razy wywołać input().

Po drugie, sytuacja podobna, często czytam logi używając tail -f, w połączeniu z trybem „tylko do odczytu” mogę go udostępnić ludziom, którzy mają może potrzebę zerknięcia, ale nie muszą mieć dostępu do systemu.

Po trzecie, to bardzo dobrze zbudowane narzędzie i strasznie mi się podoba. To będzie mój finalny i rozstrzygający argument.

A teraz czas na część wizualną dla tych, którzy przebrnęli przez te ściany tekstu. Uruchomię ttyd, które uruchomi browsh czyli terminalową przeglądarkę, która używa Firefoksa jako silnika, w Firefoksie. Pamiętajcie, „…bo mogę” to nie powiedzonko, to styl życia.


Klik, klik. Dziadek znikł.

Zdjęcie gór

Robiła się późna niedziela, a to znaczyło, że muszę powoli wyrwać się z ciszy otaczających mnie gór i wrócić na łono ogarniętego świątecznym amokiem miasta. Ciągnąłem więc nogę za nogą i oddawałem się mojej pasji oglądania lokalnego życia. Natrafiłem na przykład na szyld, który nie tylko oferował wideofilmowanie, ale także usługę przegrywania kaset VHS na płyty DVD. Jak przepisywanie kamiennych tablic na papirus. To zawsze napełnia mnie radością, że mogę uciec w miejsca, gdzie technologiczny postęp jest na chorobowym.

Jak większość górskich dróg ta też była pełna dzikich zakrętów, a przy jednym z nich zauważyłem kolejny szyld: „zakład pogrzebowy”. Uśmiechnąłem się do siebie: taki ostry zakręt i zakład pogrzebowy to doskonałe miejsce na skecz o tym jak klient dosłownie sam stuka do drzwi biznesu.

Pokonałem zakręt i co widzę na jego końcu. Drugi zakład pogrzebowy, po przeciwnej stronie ulicy. Jak stacje benzynowe obsługujące wielopasmową autostradę. To spowodowało, że rozbudowałem moje gdybania: jak dwa zakłady pogrzebowe znajdują wystarczająco biznesu dla osady, która ma cztery tysiące dusz?1

Wykluczając scenariusz taniego horroru w którym oba zakłady pogrzebowe wynajmują morderców, którzy są kołem zamachowym biznesu, zacząłem gdybać jak funkcjonuje tak specyficzny biznes w obliczu konkurencji. Czy drzwi do gabinetu lokalnego dyrektora szpitala zamykają się non-stop za właścicielami tych zakładów, trzymających pod pachą coraz to wykwintniejsze butelki alkoholi? A może kosze owoców.

I jak ja, adept współczesnego świata, mógłbym im pomóc w polepszeniu „lejka konwersji” trupa w przelew.

Na początek należy zacząć od rzeczy prostych. Reklama na Facebooku dla targetu, zgromadzić marketingowe dane o umieralności, posegregować w grupy docelowe: ludzie, którzy umierają normalnie, ze starości. Ludzie, którzy konsumują alkohol aby zapomnieć. Operatorzy maszyn rolniczych zakończonych ostrzami. Zaoferować zniżki dla tych, którzy wybiorą usługi zanim utracą zdolność decydowania.

Potem trzeba przejść do argumentów cięższych. Zakupienie danych o zdrowiu i nawykach od firm, które inwigilują obywateli w ramach „darmowych usług”. Od producentów elektronicznych krokomierzy przez producentów urządzeń podsłuchowych, które potrafią powiedzieć jaka jest pogoda, co pozwala inwigilowanym na zaprzestanie wyglądania przez okno. Tak uzyskane informacje, wielka-mała data, nadaje się idealna do wsadzenia do paszczy modeli sztucznej inteligencji. Wyłuskane dane trzeba skorelować z już istniejącymi informacjami o denatach i wejść w biznesowy układ z okolicznymi sklepami aby promować zachowania, które są denatogenne.

Hasła jak „Na co ci fura, pij denaturat”, „Siała baba mak i zrobiła kompot dla dzieci” oraz „Bądź eko, usuwaj odpady, pal oponami” powinny pomóc we wzroście.

Nie można też zapominać o klientach drugiego stopnia, tj. rodzinie i bliskich, którzy pozostają po zmarłym. Mobilna aplikacja pozwalająca na zatrudnienie aktorów w rolach „płaczek”, wybierania im strojów i ról, zatrudnienie blogera do napisania przemowy kaznodziei, nagrobek z NFC pozwalających odhaczyć się rodzinie wraz z tabelą wyników. Mikropłatności za duszę przez BLIK. Streaming dla tych, którzy wyjechali za chlebem do San Francisco.

To wszystko zrodziło się w mojej głowie jako cyniczna obserwacja bezdusznej maszyny, która pożera ludzkie interakcje i nas samych, dlatego też jestem pewien, że ktoś właśnie siada do pisania biznesplanu i wysyła e-maile do znajomych kapitalistów wysokiego ryzyka. Do tej osoby: proszę mi odpalić z 5% żebym mógł dłużej siedzieć w lesie i snuć okropne wizje. Gwarantuje, że mam jeszcze paskudniejsze pomysły, które da się zrealizować.


  1. Ponieważ wróciłem znów w to samo miejsce udało mi się znaleźć trzeci zakład pogrzebowy. Skala przemysłowa. 


Mrygaj, mrygaj mała gwiazdko

Człowiek potrafi się nabawić kompleksów. Bez względu na wiek, dokonania czy stancję w życiu w głębi duszy wiemy, że dookoła są ludzie mądrzejsi, ładniejsi i lepiej poukładani. W mojej prywatnej księdze przewin i uchybień dodałem kilka lat temu paragraf pt. „nawet dzieci teraz robią rzeczy elektroniczne podłączone do Raspberry, a Ty może przeczytałeś kiedyś na Wikipedii opis SPI i tyle”.

Czułem się z tym po dwójnasób źle, że od elektroniki zaczęła się moja przygoda z byciem dziwakiem. Jak zapomnienie pierwszej miłości tak bardzo, że nie możesz przywołać jak miała na imię. I ta nękająca wątpliwość: może jej nigdy nie kochałem? A może nawet nie istniała?

Podczas grzebania na Allegro trafiłem na segment diod LED. Zagooglałem za symbolem i widzę, że ktoś już odwalił za mnie całą robotę i jestem jedno PIP-nięcie od wejścia w posiadanie działającego sterownika. Dziewięć nowych polskich złotych zmieniło ręce. Przyszła paczka. Otwieram. Jedyną dokumentacją załączoną do płytki był paragon fiskalny, a one zwykle niewiele pomagają w podłączaniu elektroniki. No trudno, zalogowałem się na Raspberry, która leży pod moim łóżkiem i powiedziałem poweroff.

Na szczęście dokumentacja biblioteki zawierała instrukcje podłączenia. Pomyliłem się tylko raz, co było spowodowane w większości przez moje grube pazury. Zainstalowałem biblitekę odpowiedzialną za gadanie do MAX7219 i sklonowałem repozytorium żeby mieć katalogów przykładów pod ręką.

I ruszyło! Lecą napisy, pięknie. Polałem sobie drinka żeby dołączyć do setek tysięcy równie uzdolnionych dzieci ze szkół podstawowych. Po krótkiej zabawie stwierdziłem, że przesuwanie tekstów niezbyt mnie cieszy, a nawet wywołuje przykre skojarzenia z <marquee>1 i szyldami reklamowymi. Przy matrycy 8x8 jest to trochę komiczne. Oczywiście będę indywidualnie zapalał piksele. Tylko jak? Wyniki wyszukiwań prowadziły do przykładów, które nie działają już w obecnej wersji. Ale przecież musi być jakiś sposób.

Zerknąłem do kodu źródłowego i oczywiście! Jest metoda display, która przyjmuje monochromatyczny obraz reprezentowany przez PIL.Image. A skoro tak, to mogę przecież od ręki zrobić obrazek i go wstawić.

x = randint(0, 7)
y = randint(0, 7)
image = Image.new("1", (8, 8))
image.putpixel((x, y), 255)

Wcisnąłem to do mojego skrawka kodu i zaświeciły mi się losowo diody. Czułem jak pnę się po drabinie kompetencji. Już widzę jak ze szczytu macha mi Wielki Elektronik. No to teraz trzeba zaprogramować jakieś obrazki. I tu stał się problem. Mam gust, ale nie mam talentu. Bez wątpienia należy ten problem zwalić na kogoś innego.

Napisałem więc „interpretator”, który potrafi zamienić tekst w obrazek i oddałem sprawę mojemu demoscenowemu przyjacielowi, thungowi. On akurat nie miał czasu, ale „narysował” mi ludka.

...**...
..*..*..
...**...
.******.
...**...
..*..*..
..*..*..
........

Prawdziwa sztuka!

Następnego dnia przybyłem na stadion siatkarek ełkaesu godzinę za wcześnie.2 To był dobry czas żeby zrobić z wcześniejszego kodu „format animacji”, tak żebym mógł wyprodukować serię obrazów z jednego pliku. Jak postanowiłem, tak zrobiłem i chwilę przed gwizdkiem sędziny miałem już gotowy prymitywny generator serii obrazków.

Wlazłem znów pod łóżko żeby odpiąć Jeżynę i zniosłem ją na biurko. Stosując metodologię Sir Copy’ego i Lorda Paste nawtykałem jeden kod w drugi.

I stało się. Eppur si muove, parabole tańczą, ludek skacze.

To prawie krępujące chwalić się takimi bzdurami, ale na serio byłem z siebie zadowolony. Nie tylko zabrałem się za coś od razu, ale nawet osiągnąłem wszystko co chciałem i nikt nie musiał nade mną stać i obijać kijem. Pomyślałem: zaskoczę wszystkich jeszcze bardziej jak napiszę notkę. Prawdziwy hat-trick zdziwienia.

Gdy żyjesz życie pełne Logiki Biznesowej, Formatów Wymiany Danych, Garbage In / Kłopoty Out, każda dioda LED świeci jak prywatne słońce, co budzi do życia florę i faunę, która żyje w mojej głowie.


  1. kiedy zagooglacie za <marquee> bo nie jesteście pewni pisowni zwróćcie uwagę, że górny status wyników wyszukiwania przesuwa się w dobrze znany sposób. 

  2. po ekscytującym meczu wygraliśmy 3:1 


Rodzina

Coraz częściej zauważam pełzający wolno, ale nieustępliwie, rozpad podstawowej komórki społecznej, którą jak wiemy jest rodzina. Ale i dalej, wygasającą potrzebę bliskości i czułości. Chińskie z dowozem, zdalna praca, anonimowe ruchaj-randki, a najbliższy kontakt to karta/terminal płatniczy.

I to wszystko jest prawda, ale czy trzeba z tego powodu załamywać ręce i wpadać głową do przodu w okopy konserwatyzmu?

Nie trzeba!

Wśród tysięcy głosów, szepczących wam do ucha słowa pełne lęku, ja niosę wam ukojenie. Narodziła się bowiem nowa forma rodziny, która uniknęła socjologicznej analizy i pierwszej strony prasy kolorowej. Forma rodziny, która może zaspokoić potrzeby wielu.

Zbudowania na zaufaniu i wolnorynkowa, ale też gardząca strukturą i płciami. Nowoczesna, niezbędna, sycąca.

Patrzę z dumą jak w biurach rodzą się nowe rodziny. Tu trzy, tam cztery osoby biorą się pod ramiona i mówią sobie szczerze: weźmiemy pakiety rodziny platformy streamującej seriale. Tam znów, jawnie i z podniesionym czołem, powstaje homoseksualny związek, gdzie chłopak może mieszkać z innym chłopakiem pod jednym dachem Spotify.

Wolna miłość (za niewielką opłatą) wreszcie przybyła.


Demonów okiełznanie

Internet jest jak ogrodzone płotem osiedle. Nowe, błyszczące, praktycznie wszyscy żyją tu na cudzym, a sąsiedzi są anonimowymi wałami do których w najlepszym wypadku czujemy zimną obojętność.

Ktoś non stop donosi też dzielnicowemu o wszystkich małych przewinieniach i szeptanych słowach, a on, zdaje się, zawsze ma kary nieprzystające do ich wagi. Mówią, że tylko dlatego nikt nie szcza na klatkach, co jest ceną wartą płacenia. Zwłaszcza, gdy płacą inni.

Ja? Ja jestem ostatnim rdzennym mieszkańcem tych regionów. A przynajmniej lubię tak o sobie myśleć, bo daje mi to poczucie godności. Jestem pasożytem tego ekosystemu, jedząc z jego śmietników i gdy nikt nie widzi, wygrażając pięścią w kierunku sączącego się z okien światła.

Tyle metafora.

Nienawidzę, gdy strony nie mają kanału wiadomości. Jeszcze bardziej, gdy mają, ale jest zepsuty przez miesiące i nikt nie reaguje na moje ponaglające e-maile donoszące o tej sprawie. Równie niedobrze jest, gdy strona, której zmuszony jestem używać, zakłada, że mój komputer jest identyczny jak ten suto opłacanych rozwijaczy. „U mnie nie tylko działa, u mnie nawet wygląda”, zdają się mi szeptać do ucha.

Ostatnim przykładem jest strona z ramówką w serwisie Eleven Sports. Dwa paski do przewijania, które zwykle wymagają indywidualnego masażu myszką aby ujawnić jakie drużyny kopiące piłkę mogę zobaczyć na żywo tego wieczoru. Każdego dnia czułem się z tym coraz gorzej, aż wreszcie postanowiłem, że zadaniem komputerów jest przetwarzanie informacji.

Napisałem więc mały serwis, który kradnie informacje, przetwarza je i zamienia w format wymiany informacji o kalendarzach.

Klik, klik, klik: Eleven Sports w ICS

Plik kalendarzowy wyświetlony w calcurse

Teraz mogę wreszcie żyć jak człowiek, nie muszę naciskać guzików na domofonie licząc, że ktoś mieszkający na osiedlu mi powie, kto gra.


Trzy kolory: seledynowy

Bywacie czasem nieszczęśliwi? Co wtedy robicie? Pijecie? Palicie? Snujecie się po zaułkach? Programujecie?

Ja robię wszystko powyżej, naraz. W ramach nieustającej akcji „im więcej myślisz, tym bardziej nie chcesz” znalazłem się w momencie, gdzie potrzebowałem dnia ucieczki od zgiełku zwykłego dnia. Zamiast iść do biura poszedłem zwiedzać, a podczas zwiedzania wpadł mi do głowy pomysł na zajęcie umysłu. Na Twitterze jest sobie taki bot, który produkuje — zabawnie nazywane — palety kolorów. Nazywa się toto colorschemer.

Oto przykład:

schemer

Myślę sobie: a gdyby napisał bota, który potrafi wyekstrahować kolory z załączanych na Twitterze obrazków tym samym doprowadzając do SKYNET-u? Mógłbym generować z nich arkusze stylów z deklaracjami, palety do GIMP-a, czy to tam.

Brzmi jak marnowanie czasu!

Konsumujemy

Zanim zaczniemy naszą podróż po obrazku musimy zamienić plik znajdujący się na dysku w kupkę informacji. Dla Pythona dostępna jest biblioteka Pillow zawierająca w sobie wszystkie potrzebne funkcje. Użyjemy jej aby uzyskać piksele, kolory oraz rozmiar obrazka.

Na boku Bardziej zaawansowani czytelnicy mogą się skrzywić, że piszę funkcje zawierające tylko return — normalnie używam takich funkcji aby ułatwić sobie odpluskwianie i obsługę błędów. W tym przykładzie nie chcę dodatkowo zaciemniać, więc pozostawiłem tak, jak jest, no i funkcje mają bardziej zrozumiałe nazwy niż oryginalne metody.

def load_image(filename: str):
    return Image.open(filename)

def get_pixels(image: Image):
    return image.load()

def get_bounds(image: Image):
    return image.getbbox()

I tak, w kolejności: load_image wczytuje obrazek z dysku, get_pixels zwraca piksele na których możemy dokonywać analizy, get_bounds zwraca cztery wartości, które określają obszar zajmowany przez obraz.

Kolory na ekranie

Ekran komputera możemy reprezentować jako dwuwymiarową płaszczyznę której każdy punkt reprezentuje para wartości X i Y. X odpowiada za oś lewo-prawo, a Y, odpowiednia góra-dół. Pozycje zaczynamy liczyć od lewa w prawo i od góry w dół. To znaczy, że miara koordynat (0, 0) to piksel znajdujący się w górnym, lewym rogu.

Jeśli obraz ma rozdzielczość 1980x1080 to jego prawy dolny róg znajduje się na (1979, 1079) gdyż liczymy od zera.

Pod każdym z tych punktów kryje się święta trójca, Zielony, Czerwony i Niebieski, składowe kolorów w systemie RBG.

Kolory w RBG są reprezentowane przez bajt, który to bajt może przechowywać wartości numeryczne od 0 do 255. Czyli jeden piksel to trzy bajty informacji przybite do jakiejś pozycji na ekranie. Gdybyśmy chcieli sobie wyobrazić bardzo mały kwadrat, który ma bok składający się z dwóch pikseli, to moglibyśmy zapisać te dane w następujący sposób:

square = (
             (255, 0, 0), (0, 255, 0),
             (0, 0, 255), (128, 128, 128)
     )

Kolory RBG

Skoro wiemy w jaki sposób otrzymamy dane, zobaczmy, z czym nam przyjdzie pracować.

Pierwszy krok: w którą stronę ciąć

Przykładowy obrazków

Wszystkie obrazki zamieszczane na koncie przychodzą w dwóch formatach. Kolory są ułożone góra/dół lub lewo/prawo. Wykrycie typu obrazka będzie naszym pierwszym zadaniem. Utworzymy sobie typ wyliczeniowy (enum) żeby było nam łatwiej czytać kod.

Nazwałem mój enum Gravity, bo wydawało mi się, że to dobra, wiele mówiąca nazwa. „W którą stronę «spadają» kolory. Kiedy usiadłem do tego tekstu stało się oczywiste, że dużo lepszą nazwą byłoby Orientation. Udowodniłem tym samym teorię, że nazywanie rzeczy w programowaniu jest sztuką trudną.

class Gravity(Enum):
    VERTICAL = 0
    HORIZONTAL = 1

Zastanówmy się, jak wykryć, czy powinniśmy badać kolory idąc z góry w dół, czy też idąc z lewa w prawo.

Jeśli weźmiemy piksel znajdujący się pod koordynatami (0, 0), a potem weźmiemy piksel znajdujący się w przeciwległym rogu (0, szerokość obrazka) to jeśli są one takie same, znaczy, że układ obrazka jest góra/dół, bo cała pierwsza linia jest jednego koloru! Proste! W przeciwnym przypadku obrazek zorientowany jest lewo/prawo, gdyż wiemy, że kolor w lewym rogu jest inny niż ten w prawym.

Jak wykryć kierunek

Zrobimy z tego funkcję, która zwróci nam typ „grawitacji”.

def detect_gravity(pixels, bounds):
    max_width  = bounds[2]
    max_height = bounds[3]

    top_left_corner = pixels[0, 0]

    top_right_corner = pixels[0, max_width - 1]
    bottom_left_corner = pixels[0, max_height - 1]

    if top_left_corner == top_right_corner:
        return Gravity.HORIZONTAL
    return Gravity.VERTICAL

Wszystkie te kolory

OK, wiemy już jakiego typu obrazek wczytaliśmy. Teraz naszym zadaniem jest wybranie unikalnych kolorów. Zrobimy to idąc po linii i zapisując znalezione kolory. Powinno być całkiem prosto, wystarczy wybrać kierunek, zwiększać licznik o jeden i zapisywać do tablicy rezultatów unikalne wartości RGB.

Zerknijmy na kod.

def extract_distinct_colors(pixels, gravity, bounds):

    idx = 0

    if gravity == Gravity.VERTICAL:
        max_travel = bounds[3]
    else:
        max_travel  = bounds[2]

    distinct_colors = []

    while idx < max_travel:
        if gravity == Gravity.VERTICAL:
            pixel = pixels[0, idx]
        else:
            pixel = pixels[idx, 0]
        idx += 1
        if pixel not in distinct_colors:
            distinct_colors.append(pixel)
    return distinct_colors

Ustawiamy indeks idx na zero, ustawiamy rozmiar boku, którym będziemy podróżowali zależnie od «grawitacji», tworzymy pustą tablicę na rezultaty. Następnie kręcimy się aż do wyczerpania boku i sprawdzamy, czy właśnie odnaleziony piksel (dokładniej, trójca jego kolorów) jest już w distinct_colors, jeśli nie ma, dodajemy. Po zakończeniu operacji zwracamy zawartość tablicy.

Odpaliłem kod i wszystko działało, może nawet za dobrze, bo otrzymałem piętnaście kolorów, gdy spodziewałem się trzech. Dałem się nabrać swoim starym oczom, ale też udało mi się zapomnieć o faktach dotyczących obrazków w Internecie: kompresji. Jeśli popatrzymy na ofiarę naszej inspecji będziemy jasno widzieć (dosłownie) trudne do wyodrębnienia wahania w kolorze. Wahania, które ten naiwny kod, który szuka tylko unikalności, dokłada do listy. I dobrze robi, bo są to unikalne kolory, ale nie to chcieliśmy uzyskać.

Wiele kolorów w jednym pasku

Co teraz? Podglądanie obrazka pod lupą upewniło mnie, że te przekłamania w kolorze występują w niewielu miejscach, głównie tam, gdzie spotykają się dwa różne kolory. Rozsądnym sposobem byłoby liczenie ilości wystąpień. To pozwoli nam odrzucić sieroty po kompresji. Zmodyfikowałem więc kod w następujący sposób: dodałem occurance_counter, który zawiera liczniki wystąpień danego koloru. W bloku try…except sprawdzam w jakim miejscu w liście znajduje się obecnie odkryty kolor, a jeśli się nie znajduje (co podniesie wyjątek ValueError na metodzie .index()) znaczy, że widzimy go pierwszy raz. Tym razem z funkcji zwracamy dwie wartości: listę kolorów oraz ich liczniki.

def extract_distinct_colors(pixels, gravity, bounds):

    idx = 0

    if gravity == Gravity.VERTICAL:
        max_travel = bounds[3]
    else:
        max_travel  = bounds[2]

    distinct_colors = []
    occurance_counter = {}

    while idx < max_travel:
        if gravity == Gravity.VERTICAL:
            pixel = pixels[0, idx]
        else:
            pixel = pixels[idx, 0]
        idx += 1

        try:
            found_at_index = distinct_colors.index(pixel)
            occurance_counter[ found_at_index ] = occurance_counter[ found_at_index ] + 1
        except ValueError as e:
            distinct_colors.append(pixel)
            occurance_counter [ distinct_colors.index(pixel) ] = 1
    return (distinct_colors, occurance_counter)

Teraz muszę dodać tylko funkcję, która użyje obu tych informacji i zwróci mi n najczęściej występujących kolorów. Muszę przyznać, że byłem tu już trochę zmęczony, siedziałem na ławce w ciemnym parku, offline, paląc papierosy. Dlatego funkcja filter_top_entries nie jest może najbardziej przyjazna dla oczu początkujących, dlatego rozłożę ją może na czynniki pierwsze.

def filter_top_entries(count, distinct_colors, occurance_counter):
    filtered = sorted(occurance_counter.items(), key=lambda x: x[1], reverse=True)[0:count]
    return [distinct_colors[ x[0] ] for x in filtered]

Pierwszym paraemetrem jest liczba elementów, które chcemy uzyskać, dwa pozostałe to produkt extract_distinct_colors.

occurance_counter zawiera dane w następującym formacie:

    {
      '0': 12,
      '4': 1,
      '2': 34,
      []
    }

Indeksem w tym słowniku jest pozycja koloru w distinct_colors, a jego wartością jest liczba wystąpień. W Pythonie słownik posiada metodę .items(), która zwraca pary elementów, czyli gdybyśmy użyli jej na tym przykładowym kodzie, otrzymalibyśmy ( ('0', 12), ('4', 1), ('2', 34)). To właśnie przekazujemy do sortowania. Ale sorted() nie wie jak posortować dwuelementową listę. Dlatego też w parametrze key podajemy kawałek kodu, który wskaże jaki element z tej listy jest parametrem, który nalezy posortować. Pod indeksem 0 znajduje się pozycja koloru, pod indeksem 1 znajduje się liczba wystąpień. x: x[1] znaczy „do sortowania użyj ilości wystąpień, które znajdziesz w liście na pozycji pierwszej.

Ponieważ sorted() domyślnie sortuje od najmniejszej do największej musimy też poprosić o odwrócenie tego zachowania, gdyż szukamy najczęściej występujących kolorów. Do tego właśnie służy parametr reversed=True.

Produktem sorted() jest lista, więc możemy na niej użyć pythonowej składni do przycinania list, w tym wypadku [0:count] zwróci nam tylko fragment posortowanej listy do ilości podanej w pierwszym parametrze.

I to jest pierwsza linia. ;-)

Python posiada wygodną składnię do „kompresowania” wyrażeń normalnie zamykanych w for, to właśnie robi druga linia. Można ją przeczytać „dla każdego elementu w filtered, które uzyskaliśmy z sorted() weź element pod indeksem zero, który jest indeksem koloru na liście «unikalności» i zwróć to wszystko jako tablicę”

Alternatywnie możnaby to zapisać jako:

results = []
for x in filtered:
    results.append(distinct_colors[ x[0] ])

I już! Teraz powinniśmy mieć możliwość uzyskania trzech najważniejszych kolorów!

Składanie tego wszystkiego do kupy

Teraz możemy przetestować wszystko razem. Normalnie nie pisze się kodu testującego bezpośrednio w bibliotece, ale… wiecie, jak to jest. Zrobiłem sobie listę obrazków skradzionych z Twittera, wsadziłem je do listy i dla każdej z nich wywołałem wszystkie, dyskutowane wcześniej, funkcje.

Dopisałem jeszcze to_hex_expression, które zamienia RGB w znany nam dobrze format heksadecymalny używany w programach graficznych oraz CSS.

test_cases = [
    'test-cases/Dk82gZuVsAAcGho.jpg',
    'test-cases/Dk666IVUUAALxRx.jpg',
    'test-cases/Dk7kG4qU0AAJ5RL.jpg',
    'test-cases/Dk8oxdUVAAY6-JY.jpg',
    'test-cases/Dk9EPPVV4AA0obD.jpg',

]

from writers import to_hex_expression

for test in test_cases:
    print()
    image = load_image(test)

    bounds = get_bounds(image)
    pixels = get_pixels(image)

    gravity = detect_gravity(pixels, bounds)

    distinct_colors, occurance_counter = extract_distinct_colors(pixels, gravity, bounds)
    colors = filter_top_entries(3, distinct_colors, occurance_counter)
    for color in colors:
        print(to_hex_expression(color))

A oto rezultat:

test-cases/Dk82gZuVsAAcGho.jpg Gravity.VERTICAL
#446bae
#a10499
#35530b

test-cases/Dk666IVUUAALxRx.jpg Gravity.HORIZONTAL
#fcf779
#77ab56
#1fa874

test-cases/Dk7kG4qU0AAJ5RL.jpg Gravity.VERTICAL
#9eff00
#ff5c01
#cc406f

test-cases/Dk8oxdUVAAY6-JY.jpg Gravity.HORIZONTAL
#7f8f4e
#aca588
#ff6bb5

test-cases/Dk9EPPVV4AA0obD.jpg Gravity.VERTICAL
#24ff29
#1fb47a
#4a5d98

Trzy kolory „wyssane” z obrazka. Pełen sukces, dzień zmarnowany, wszyscy są szczęśliwi. Niestety, nie dotarłem do momentu w którym nauczyłbym kod łażenia „osobiście” na Twitter i podkradania obrazków, gdyż skończył się mi dzień.

Jeśli ktoś z czytelników uważa, że jest sens napisać drugą cześć, gdzie wpadlibyśmy grabić via API to można mi zostawić e-mail.

Przepraszam też jeśli ten tekst był zbyt chaotyczny i/lub bezużyteczny. Próbuję pobić prokrastynację przez brute force. Repozytorium z kodem jest dostepne na bitbucket, bronikowski/kaleidoscope.


Palec w oko

Homo erectus, homo sapiens, homo vatis1. Człowiek, co powstał z kolan, pomyślał po to, by opowiadać historie z dużego i małego „ha”. Człowiek z natury chce się wygadać, a czasem nawet chce być wysłuchany, ale głównie gadać.

Opowiadanie historii demokratyzowało się przez cały nasz czas na tej planecie. Od garstki liderów religii tłumaczących początek świata i bogów, aż po miliony użytkowników sieci społecznościowych z których wielu tłumaczy, jak świat się skończy i kto (zwykle „Oni”) się do tego przyczyni.

Gdzieś pomiędzy teraz a wtedy istniał świat w którym dostęp do technologii był ograniczony, ale ludzie nadal chcieli gadać. Był to świat w którym komputery osobiste były nadal zabawkami, a obliczeniowych mięśni dostarczały systemy UNIX-owe, popularne głównie w instytucjach naukowych, militarnych i uniwersytetach.

Każdy pracownik i student posiadał swoje konto, które było centrum cyfrowego życia. To tu przychodziły e-maile, tu kompilowało się oprogramowanie, tu odpalało aplikacje. Nic dziwnego, że katalogi użytkowników znajdowały się na partycji nazywanej tradycyjnie /home, był to prawdziwy dom dla e-bytów.

Ktoś w 1977 wpadł na pomysł protokołu, który pozwalałby „zapytać” systemu o fakty dotyczące danego użytkownika. Kiedy ostatnio był zalogowany, a jeśli jest, jak dawno dotknął klawiszy, jaki jest najnowszy e-mail czekający nań w skrzynce, kiedy ostatnio czytał pocztę, jak się nazywa i kilka innych, zbytecznych rzeczy.

Protokół ten został nazwany finger.

Informacje uzyskane tą drogą wydają się być trochę dziwne, ale jeśli weźmiemy pod uwagę, że „wsadzało się palec” w konto kolegi lub koleżanki z pracy/studiów, wiedza o tym, czy są przy terminalu i czy czytali już pocztę od czasu, gdy wysłaliście e-maila jest całkiem użyteczna.

Finger działał też w Internecie, gdy ten stał się faktem, człowiek mógł dla własnej frajdy dziabać paluchem znanych ludzi. No, znanych ludzi, którzy mieli konta na maszynach UNIX-owych, choć nie wiem, kto chciałby znać innych.

Co to ma jednak wspólnego z moim wstępem o potrzebie wygadania się? Finger czytał też dwa pliki z katalogu domowego użytkownika: .project i .plan.

W zamyśle miało to pomóc w organizacji pracy, gdzie .project opisywałby nasze długoterminowe działania, a .plan byłby zdaniem odpowiadającym na pytanie „co dziś robisz?”.

Niektórzy ludzie używali tych dwóch plików zgodnie z przeznaczeniem, związani obowiązkiem służbowym lub kompletnym brakiem poczucia humoru. Inni wsadzali do .planu tyrady o jedzeniu na stołówce, żarty, cytaty z książek, matematyczne dowody o wyższości Star Treka nad Star Wars i wszystko, co przyszło im do głowy.

Homo vatis1 nie zrodziło się wraz z erą Facebookiem, MySpace czy Twittera. Jeśli dacie ludziom możliwość napisania zdania, to znajdą się natychmiast ludzie piszący zdania ważne, zabawne i irytujące. Oraz ich czytelnicy.

W latach dziewięćdziesiątych finger został uznany przez większość administratorów za daemon niebezpieczny: program uruchamiany zewnętrznie, przez Internet, który czyta prywatne pliki z katalogu domowego użytkownika, czasem z podwyższonym dostępem to źródło potencjalnego bólu głowy.

Jeden z pierwszych robali używających Internetu do rozprzestrzeniania się, Morris, używał dziury znajdującej się w implementacji protokołu.

Tym samym popularność .plan spadła niemal natychmiast do zera. Ludzie jednak nie przestali opowiadać historii, zmieniły się tylko platformy i media, od rylca do glinianych tablic po pióra wieczne, od .plan do mediów społecznościowych. Nikt nie zamknie nam ust póki jest miejsce gdzie można napisać zdanie, szalet miejski jest na to dowodem ostatecznym.

* * *

W Internecie ciężko znaleźć jakieś archiwa wpisów do .plan, jego natura była bardzo efemeryczna. Jedyne popularne archiwum to .plan Johna Carmacka, autora wolf3d.exe i quake.exe.


  1. człowiek bard w kłamczo-łacinie 


Wolniej

Wiek średni to taki okres w życiu człowieka, gdy mimowolnie staje się „kołczem rozwoju osobistego” zaskarbiając sobie nienawiść wśród ludzi bliskich i przygodnych. Wszystkie błędy popełnione w życiu przepasuje czerwoną wstążką i obnosi je jak relikwie nie potrafiąc rozpoznać, że nie ma w nich nic wyjątkowego ani odkrywczego.

Uprawomocniony wchodzeniem w ostatni zakręt — już mocno przedłużonej — młodości, chciałbym Wam powiedzieć coś o uczeniu się. Jest to podwójnie skandaliczne, gdyż mury szkoły widziałem tylko od strony szarego tynku, gdy odbijałem odeń piłkę.

Struktura społeczeństwa zmieniła się już nie raz. Tak jak kiedyś chudy i spalony słońcem był z pewnością chłopem, którego jedynym dorobkiem jest garb tak bladaporcelanowa i pulchna była Hrabina. I gdy nawiniemy taśmę czasu w nasze okolice, sygnały statusu odwróciły się.

To samo stało się z „wolnym czasem”, choć w ogóle rzecz taka jak „wolny czas” w naszym rozumieniu jest produktem zupełnie nowym na rynku kultury. Kiedyś człowiek, co mógł się wyciągnąć na tapczanie i tak po prostu leżeć i wypierdywać dziury w materiale, był królem życia. Dziś kto nie uprawia wspinaczki i garncarstwa, najlepiej za jednym zamachem, jest prostakiem po stokroć. Wolny czas należy wypełnić zajęciami tak szczelnie żeby narastało w człowieku marzenie o rozwinięciu talentu do wypróżniania się w biegu, jak to czynią konie, między galopem od jednej aktywności do drugiej.

Są tylko dwie ważne rzeczy w uczeniu się nowych zdolności. Jak drogi jest sprzęt zakupiony do uprawiania hobby i jakiej klasy ludzie widzą Cię podczas go uprawiania. Kpię oczywiście.

Pierwszą rzeczą, o dziwno często pomijaną podczas skoku w nowe zajęcie, jest fakt, że trzeba te hobby na serio uprawiać. Mam pełen strój bramkarski, ale boisko widziałem pewnie 10 lat temu, nie wypada mi się nazywać „bramkarzem”, nawet „były bramkarz” brzmi głupio, jak „byłe dziecko”.

Jestem zwolennikiem pozytywistyczno-praktycznej definicji. Piszesz? Jesteś pisarzem. Nie piszesz? Nie jesteś. Biegasz? Jesteś biegaczem. Nie biegasz, nie jesteś. I może nie jesteś najszybszym biegaczem albo najbłyskotliwszym pisarzem. To jednak ocena wartości, a świat nosi wielu twórców, którzy są lepsi ode mnie, a nie naplułbym im na łeb, gdyby się palili. To wszystko są truizmy, ale bardzo łatwo się je zapomina, czasem nawet z premedytacją, gdy podczas wiosennego sprzątania przekładasz notatnik Moleskine z jedną zapisaną stroną (zakupy), buty do biegania i gitarę z zerwaną struną.

To prawda pierwsza: jeśli chcesz mieć jakąś przyjemność z uczenia się nowej rzeczy musisz ją uprawiać. Bardzo łatwo odstawiać teatr i grać przed sobą komedię. Ale nikt się nie śmieje, bo nikt tego nie ogląda.

Prawda druga: czym wolniej się uczysz tym większa szansa, że się nie poddasz.

W świecie zoptymalizowanym pod natychmiastową gratyfikację ciężko jest się przestawić na żmudną drogę. Jakby Jezusowi powiedzieć: „będziesz cierpiał, ale jaka to historia z tego wyjdzie”. Dlatego współczesnych synów bożych separuje się od zdrowego społeczeństwa w zakładach psychiatrycznych, komu by pasowała taka opcja!

Od miesięcy próbowałem nauczyć się Rusta. I moja droga zawsze przebiegała tak samo. Pobieżnie skonsumowany wstęp w dokumentacji. Kompilacja println!("Twoja stara");. Próba napisania czegoś. Frustracja. Porzucenie nauki na miesiące. GO TO 10.

Kilka dni temu postanowiłem, że przestanę skakać z jednego miejsca dokumentacji w drugie, próbując zmusić kompilator do respektowania mojego autorytetu wieloletniego programisty.

Godzina po godzinie, tworzyłem sobie katalog z nazwą jakiegoś elementu języka np. 03.array, 04.struct, 05.enum i mozolnie, pokornie, przepisywałem przykłady, modyfikowałem je, wymyślałem inne, testowałem hipotezy. Czasami aż mnie szczypało w dupę, że muszę znów pisać kod, który „wypisuje wszystkie elementy z tablicy wraz z ich indeksem”. I kiedy wreszcie zesztywniał mi kark okazało się, że podczas tych zupełnie głupich zajęć nie tylko załapałem idee, których opisy czytałem wcześniej na wyrywki, byłem też wewnętrznie zadowolony mimo że nie stworzyłem niczego oczywistego.

Teraz myślę do siebie, że „wolno” to może złe słowo, lepszym byłoby „metodycznie”, ale dychotomia „wolno” ↔ „szybko” brzmi mi lepiej niż „metodycznie” ↔ „chaotycznie”.

„Szybkość” to to, co marketingowcy umieszczają w dużym czerwonym kółku w nadziei, że odmieni Ci się to w głowie na „wygodę”. I w pewnym sensie jest to prawda. Ludzie spędzili wiele lat budując świat w którym 20% dostępnej wiedzy pozwala wykonać 80% zadań z zadowalającym skutkiem.

Dlatego dobicie do 21% spowoduje, że pobijesz większość. Dlaczego? Bo wolno.


Nazwa pliku jako UX

Ze wszystkich mądrych rzeczy, które powiedziałem i napisałem przez lata, tak o życiu, jak i o programowaniu, szczególne miejsce w moim sercu ma ta obserwacja:

Bardzo łatwo rozpoznać interface projektowany przez programistę: „godzina i piętnaście minut” jest wyrażone jako 1.25

Ta obserwacja była formą samokrytyki, gdyż podczas prowadzenia dema zauważyłem, że sam wykonałem takiego „międzymordziowego bazyliszka”. Od tego czasu, aż do teraz, oddałem wszystkie prace związane z tworzeniem interakcji ludziom, którzy się na tym znają. A przynajmniej powinni.

Zaszyłem się więc w ciemnej norze backendu, gdzie użytkownicy istnieją tylko jako miraże, tworzone i niszczone na potrzeby automatycznych testów. Bez historii, bez potrzeb, nie wysyłają świątecznych kartek. Perfekcyjna harmonia.

Ostatni projekt zmusił mnie jednak do opuszczenia mojej kryjówki, gdyż przypadła mi kluczowa rola, a że i projekt przypadł mi do gustu, byłem całkiem zadowolony i dałem nura.

Jednym z produktów systemu, który pisałem, były dokumenty do druku, gdyż część z naszych użytkowników jest cyfrowo wykluczona i nie chcemy aby potrzeba posiadania komputera z dostępem do Internetu była barierą. Ponieważ jedna osoba może mieć maksymalnie sto takich dokumentów, wymyśliłem i przeforsowałem zmianę. Dokumenty można scalać w pojedynczą grupę dokument, będący dla systemu równoważnikiem. Każdy dokument zawiera numer seryjny oraz sygnaturę kryptograficzną, dlatego zbudowanie takiej paczki jest całkiem proste i pozwala na łatwą weryfikację spójności.

Przepchnąłem ten pomysł zaczynając od współpracowników, przez kierownika projektu ze strony klienta, kończąc na akceptacji urzędu marszałkowskiego. Kiedy dostałem glejt z pieczęcią „no dobra, rób” usiadłem i zaprogramowałem.

Wszyscy żyli szczęśliwie kilka miesięcy, aż do momentu w którym Panie z rozliczeń zaczęły zwracać uwagę na rosnącą liczbę pomyłek przy zdawaniu paczek dokumentów. Bardzo dziwne, bardzo dziwne. Zalogowałem się jako użytkownik i poszedłem oglądać.

I wtedy zrozumiałem, że znów mam 1.25 jak w cytacie wyżej.

Dla własnej wygody trzymam paczkę jako SHA512 robiący za sumę kontrolną. To oczywiste, że tak robię. Kiedy piszę automatyczny test mogę od razu stwierdzić, czy dokument zapisany na dysku zgadza się z tym, czego się spodziewałem.

Dla mnie SHA512 jest oczywiste. Dla użytkownika natomiast otrzymanie pliku 4143aaf85e1a825463a8a202b9ee6ffa486[…]9a5edc944e3e473f89a889c85e093abe7adce5164dfefef0f3eded1e.pdf niesie bardzo niewiele informacji. W sytuacji, gdy mamy jedną paczkę problem rozwiązuje się naturalnie. Gdy użytkownik ma tych plików 12, heksadecymalny jazgot jest nie do przetrawienia.

Pracownik za kwadrans do fajrantu nie ma już mocy na sprawdzanie każdego dokumentu. A to rodzi błędy.

Dodałem więc nagłówek Content-Disposition: attachment; filename="%s_%s_%d" i sformatowałem nazwę pliku używając imienia, nazwiska i liczby dokumentów w paczce. Jan_Kowalski_15.pdf jest trudniej źle zinterpretować.

Od tego czasu liczba błędów spadła do zera.

Jedna linijka, a taka różnica. Nie wiem, czy być dumny z tej, szybkiej, reakcji czy też zawstydzony, że nadal mentalnie siedzę w backendowej jaskini.


Za kwadrans wiosna

Otworzyłem okno. Mimo ostrzeżeń o zanieczyszczeniu powietrza podjąłem decyzję, że lepszy jest dym płonących opon unoszący się z kominów łódzkich slumsów niż moja własna tytoniowa wędzarnia. Przez szparę wlał się chłód będący idealnym towarem zastępczym świeżości.

Usiadłem na fotelu i patrzyłem się tak w okno. Laptopa zostawiłem w biurze żeby się odtruć, widocznie nawyku patrzenia się w okna z tępym wyrazem na twarzy nie da się wyzbyć w jeden dzień odwyku. Kontemplowałem wszechświat, ale bardzo krótko, bojąc się że przez przypadek coś odkryję i będę musiał temu stawić czoło.

Zabrałem się za odpisywanie na listy i okłamywanie się, że nie jest mi zimno z powodu tego okna. Jak ludy prymitywne wierzyłem, że moje przekonanie o komforcie udzieli się, zwykle nie zważającym na byt ludzki, sferom niebieskim. I że będzie wiosna.

Po trzech godzinach, gdy wiosna się nie zjawiła, a w jej miejsce przyszła noc, musiałem uznać przewagę materii nad umysłem. Zamknąłem okno. Zapaliłem papierosa żeby nie dać się tej nowo nabytej świeżości zbyt łatwo.

I wtedy usłyszałem to leniwe, monotoniczne bzyczenie.

Tłusta mucha, pobudzona jak i ja, tą pierwszą obietnicą wiosny, podniosła swój zielony brzuch i zaczęła latać mi nad łbem.

Ja pisałem do paryżanki o chińskim atramencie, a ona przerywała mi co zdanie.

Zdałem sobie sprawę, że sam doprowadziłem do tej sytuacji. Nie przez otwieranie okna, nie. Widzicie, to wszystko przez technologię. 15 lat temu zwinąłbym w rulon gazetę i postawił się w roli sędziego, oskarżyciela i kata w jednej osobie. Po krótkiej gonitwie zgniótłbym ją na jakiejś płaszczyźnie i wydał z siebie pomruk zadowolenia; prymat homo sapiens zostałby zachowany.

Po upadku prasy papierowej mógłbym jej najwyżej przyjebać tabletem.

W sumie i tak nic na nim nie czytam.


Umówiłem się z nią na @833

Internet od zawsze był przestrzenią przyjazną eksperymentom i wynalazczości. I zgodnie z nazwą mojej ulubionej kolumny w „Młodym Techniku” — „Pomysły genialne, zwariowane i takie sobie”1 niektóre przetrwały nie tylko próbę czasu, ale stały się budulcem współczesnego świata. Inne wypadły z rąk twórców i z hukiem roztrzaskały się na betonowej posadzce rzeczywistości.

Pomysł, o którym chciałem Wam dziś opowiedzieć, tańczy na granicy tych trzech przymiotników. No, chyba, że będziemy patrzeć z punktu widzenia przyjęcia rozwiązania na rynku, wtedy nawet „takie sobie” to raczej laurka niż opis stanu faktycznego. Popatrzmy na niego oczami cyber-futurystów roku pańskiego 1998, kiedy Internet miał już pierwszy zarost.

Najważniejszym zadaniem sieci było połączyć nas wszystkich i stworzyć Globalną Wioskę, gdzie wektory wiedzy, różnorodności i zaradności dodadzą się rodząc sytuację, w której co drugi obywatel będzie Królem Filozofem. Dziś z perspektywy czasu widać, że idea wioski się spełniła: kumoterstwo, plotkarstwo i disko polo ma się dobrze.

Zostawmy jednak cynizm.

Kiedy zaczęliśmy pracować w globalnej wiosce okazało się, że wszyscy tyrają w systemie trójzmianowym. Idziesz do piekarza, a on śpi, gdyż jest na innym kontynencie, choć metaforycznie, na wyciągnięcie e-ręki. Strefy czasowe podstawiły nogę rozpędzającej się, globalnej, ekonomii. Próba umówienia się z kimś wymagała powtórki z geografii, ustalenia stref czasowych wszystkich uczestników, oraz tego, czy używają obecnie czasu letniego czy zimowego.

Swatch wpadł na pomysł jak rozwiązać ten nowy — na tę skalę — problem, sprzedając przy okazji niezłą górę zegarków. Swatch Internet Time!

Francja, po rewolucji, wprowadziła czas decymalny, oparty na 10 godzinach, które składały się z 100 minut po 100 sekund. Jako że ciężko się patentuje rozwiązania, które są jasno opisane w podręcznikach do historii, Swatch dokonał lekkiej modyfikacji: doba składała się z 1000 .beat, nie posiadały one strefy czasowej, więc były uniwersalne dla świata. Wystarczało, że umówisz się z nią na @833, weźmiesz od szefa akonto i wszyscy będą wiedzieć, kiedy to jest! Proste!

Oczywiście to nie zmieniło niczego w fakcie, że podróżujemy dookoła kuli plazmy i ludzie nadal śpią gdzieś kiedy zegarek Swatcha „wybija” @833.

Ja jednak byłem urzeczony pomysłem i choć nie miałem kontaktów z nikim, kto potrzebowałby się ze mną umówić w różnych strefach czasowych, dumnie włączyłem renderowanie ilości .beat na belce systemowej mojej Amigi. Próbowałem nawet nieśmiało podawać tak sformatowaną godzinę różnym znerdziałym znajomym, ale oni patrzyli tylko na mnie jak na dziwaka. Dwadzieścia lat później sytuacja nie uległa zmianie w tej kwestii.

Swatch nie był zadowolony z tego, że ich wynalazek jest tak beztrosko implementowany przez różnych obszarpańców, co pewnie nie mają zegarka ni garnituru i zrobił to, co każde lajfstajlowe korpo, zaczął ścigać „nielegalne dzielenie”, aż do momentu w którym nikt nie chciał się w to bawić. Trudno wymagać żeby każdy, z kim się umawiasz, miał zegarek tego samego producenta.

Dodatkowo Swatch, chcąc zapewne podbić wartość swojego wynalazku, wprowadził swoją własną strefę czasową, Biel/Switzerland, od której liczony był czas bazowy dla Internet Time.

Ktoś mógłby zapytać, czy jest jakaś merytoryczna różnica między umawianiem się na godzinę wg. UTC a tym oto wynalazkiem? Jest jedna, oczywista. Godzina z prefiksem @ jest bardziej e-cyber.

I tak pomysł genialny, bo stymulował fantazję o Jednym Świecie, zwariowany, bo ciężko wyobrazić sobie coś tak radykalnie dobrego żeby cały świat entuzjastycznie przytaknął, ale też taki sobie, bo w przeciwieństwie do innych zdobyczy epoki internetowej nie dane mu było oderwać się od cyca korpowładców.

A piszę o tym dlatego, że wracając wczoraj, lekko intelektualnie zmęczony imprezą, przypomniałem sobie o tym moim zegarku na belce Amigi i wymyśliłem, że dopiszę sobie to samo, tylko jako element belki tmuxa. Sama procedura jest trywialna.

  1. Weź obecną godzinę
  2. Policz ile sekund minęło od północy
  3. Ponieważ w 24h jest 86400 sekund, a chcemy 1000 jednostek, dzielimy to przez 86.4 i odrzucamy resztę
  4. Dzwoniący telefon to prawnicy szwajcarskich zegarmistrzów
  5. No division is illegal, hack the planet

Natychmiast po napisaniu kodu zdałem sobie sprawę, że jestem o godzinę za daleko. Jasne, muszę przekręcić datę na ich strefę czasową. Programowanie z uwzględnieniem stref czasowych przechodzi atrofię za każdym razem, gdy przestaję o nim myśleć. Poszedłem więc czytać dokumentację, ale za nic na świecie nie mogłem się dobrać do, nieoficjalnej przecież, strefy czasowej Swatcha.

Rzutem na taśmę odkryłem, że jest on równoważny UTC+1, dodałem więc brzydką arytmetykę na datach. Jestem pewien, że zrobiłem coś źle. Apple dwa lata wydawało iOS z błędem, który przestawiał budziki użytkownikom, a Microsoft miał firmware w Zune, które spowodowało, że jeden dzień nie dało się włączyć odtwarzacza. Skoro oni nie wiedzą, co robią, to ja też sobie pozwolę.

Efekt działania na terminalu:

Zrzut z ekranu pokazujący program w działaniu

#!/usr/bin/env python3
from datetime import datetime, timedelta
import pytz
import sys

def swatch_time(datetime_object = None, timezone_literal = 'Europe/Warsaw'):

    reset_clock = {
            'hour': 0,
            'minute': 0,
            'second': 0,
            'microsecond': 0
    }

    if not datetime_object:
        datetime_object = datetime.now()

    if not datetime_object.tzinfo:
        timezone = pytz.timezone(timezone_literal)
        datetime_object = timezone.localize(datetime_object)

    # Biel TZ is a unrecognized TZ that can be expressed as UTC+1
    # Thanks, Swatch
    swatch_timezone = pytz.timezone('UTC')
    datetime_object = datetime_object.astimezone(swatch_timezone) + timedelta(hours=1)

    midnight = datetime_object.replace(**reset_clock)
    time_delta = datetime_object - midnight

    if time_delta.seconds == 0:
        # don't divide against zero
        return 0

    return int(time_delta.seconds / 86.4)

if __name__ == "__main__":
    print("@{}".format(swatch_time()))
  1. Dział ten nazywałem też „Nygusem”, gdyż numery z lat 80-tych miały pana leżącego niedbale dookoła liter loga, standardowym zawołaniem bojowym w weekendy było „Babcia, poczytaj mi «Nygusa»”.