Az előző részekben volt alkalmunk többször is megcsodálni, hogy milyen egyszerűen lehet adatbázist rakni az alkalmazásunk mögé Docker Compose segítségével. De kár lenne itt megállni. A Docker kabalaállata biztos ránk is kiabálna, ha így tennénk. Jöjjön hát még pár kedvcsináló példa.

SFTP szerver

Kezdésnek nézzünk valami egyszerűt. Ha az alkalmazásunknak egy SFTP szerverre lenne szüksége, azt nagyon egyszerűen meg tudjuk oldani. Létezik egy remek image, ami elvégzi helyettünk a munka nagy részét, így csak pár sor és meg is vagyunk.

docker-compose.yml
version: "3"
services:
  sftp:
    image: atmoz/sftp:alpine
    ports:
      - "2222:22"
    command: user:pass

Névfeloldás

A service-eknek megadhatunk egyedi DNS szervert (vagy szervereket) is, ami akár egy másik konténer is lehet. Ehhez először is szükség van egy saját hálózatra, ahol a DNS szervernek fix IP címe van.

docker-compose.yml
networks:
  default:
  dnsnet:
    ipam:
      config:
        - subnet: 172.16.113.0/24
          gateway: 172.16.113.1

A default biztosítja azt, hogy az egymáshoz linkelt konténerek név alapján továbbra is elérik egymást (pl. ha van egy db nevű service-ünk, akkor arra db néven tudunk alkalmazásunkban hivatkozni). A dnsnet-et használjuk arra, hogy a DNS szerverünknek legyen fix címe.

docker-compose.yml
dns:
  build: ./dns
  networks:
    default:
    dnsnet:
      ipv4_address: 172.16.113.2

Már csak az alkalmazás konténernek kell megadni, hogy a mi DNS szerverünket használja.

docker-compose.yml
app:
  build: ./app
  links:
    - dns
  dns: 172.16.113.2
  networks:
    default:
    dnsnet:

Ha minden jól ment, láthatjuk, hogy az egyébként nem létező look-me-up.test domain-t megfelelően fel tudjuk oldani az alkalmazáson belül:

$ host look-me-up.test
Host look-me-up.test not found: 3(NXDOMAIN)

$ docker-compose run --rm app
look-me-up.test has address 172.3.4.5

A teljes példa szokás szerint megtalálható a kapcsolódó Github repóban.

Email (nem) küldés

Fejlesztés közben jó, ha nem kezdünk el random címekre leveleket kiküldeni. Viszont fejlesztés közben néha jó az is, ha mégis csak kimennek ezek a levelek és meg tudjuk nézni őket egy email kliensben. Szóval szeretnénk is levelet küldeni, meg nem is.

Ha szerencsénk van, akkor az általunk használt keretrendszer vagy library beállításai között van valami olyasmi, hogy SMTP szerverhez csatlakozás helyett csak egy fájlba írja ki a levél tartalmát (pl. Django-ban az EMAIL_BACKEND beállítás).
Így már nem kell kézzel összevadásznunk, hogy mi volt az aktivációs link, amit az új regisztrálónak kiküldött (volna) a rendszer, de ez még nem ad választ arra, hogy "Hogyan néz ki a levél Outlook 97-ben?".

A konténerek itt is a segítségünkre lehetnek. Felhúzhatunk egy olyan SMTP szervert, ami címzettől függetlenül az összes megkapott levelet egy lokális fiókba kézbesíti és egy POP3 vagy IMAP szervert, amihez csatlakozva ezeket a leveleket meg is nézhetjük.

Nem hangzik túl bonyolultan, de azért megvannak a maga kis buktatói. Nézzük először az SMTP szervert.

smtp/Dockerfile
FROM alpine:3.5

RUN apk add --no-cache postfix postfix-pcre rsyslog supervisor

RUN adduser -u 1000 -D dev && \
    mkdir -p /home/dev/Maildir && \
    chown -R dev:dev /home/dev

COPY ./etc /etc

RUN touch /etc/aliases && \
    postalias /etc/aliases

EXPOSE 25

CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]

A postfix már önmagában is egy trükkös jószág. Nem egyszerű rábírni, hogy standard output-ra logoljon, vagy hogy éppen az előtérben maradjon. Így szükség van mellé még egy rsyslog-ra és egy supervisor-ra is. Az rsyslog-nak megmondhatjuk, hogy küldjön mindent az stdout-ra. A supervisor pedig van olyan kedves és kérésre az előtérben marad (és mellesleg kezeli a másik két alkalmazás elindítását is).

Ezen kívül még létrehozunk egy dev felhasználót. Ő fogja az összes levelet megkapni a Maildir könyvtárába. Meg persze bemásolunk még egy rakás konfig fájlt, ami nem különösebben érdekes, úgyhogy a szokott helyen meg lehet őket tekinteni. Térjünk át az IMAP szerverre.

imap/Dockerfile
FROM alpine:3.5

RUN apk add --no-cache dovecot

RUN adduser -u 1000 -D dev && \
    mkdir -p /home/dev/Maildir && \
    chown -R dev:dev /home/dev

COPY ./etc /etc

EXPOSE 143

CMD ["/usr/sbin/dovecot", "-F"]

Jól látható, hogy a dovecot egy fokkal jobban viseli a konténerizálást. A dev felhasználót itt is létrehozzuk, ugyanazzal az UID-dal, mint a másik konténerben. A beállításokból csak annyit emelnék ki, hogy egy felhasználó lett megadva dev néven, aki dev jelszóval tud belépni.

Nem maradt más hátra, mint a docker-compose.yml fájl, ami az egészet összefogja.

docker-compose.yml
version: "3"
volumes:
  mail-data:
services:
  app:
    image: python:2.7-alpine
    depends_on:
      - smtp
    volumes:
      - ./app:/app
    working_dir: /app
  smtp:
    build: ./smtp
    depends_on:
      - imap
    volumes:
      - mail-data:/home/dev/Maildir
  imap:
    build: ./imap
    volumes:
      - mail-data:/home/dev/Maildir
    ports:
      - "143:143"

A dev felhasználó Maildir könyvtára meg van osztva az SMTP- és az IMAP service között, ami biztos nem egy optimális megoldás, de a célnak megfelel. Az app service segítségével pedig tudunk küldeni teszt levelet.

$ docker-compose run --rm app python send_mail.py

Amit aztán kedvenc IMAP kliensünkkel el tudunk olvasni a localhost:143-ra csatlakozva.

El is érkeztünk a második részhez, amiben egy kis alkalmazást fogunk összerakni külön kliens és szerver oldali konténerrel. Aki esetleg lemaradt volna az előző részről, itt tudja pótolni.

A szerver oldalt a változatosság kedvéért most a Python fogja képviselni a Flask keretrendszerrel és az SQLAlchemy ORM-mel. A kliens oldalon a Mithril.js keretrendszer lesz segítségünkre a JavaScript-ben, a CSS-t pedig a Milligram szolgáltatja. A függőségek beszerzéséről az npm gondoskodik és végül az egészet a Webpack segítségével fogjuk egybegyúrni. A kliens oldal szereti a sok szereplőt. No de vágjunk is bele.

Az adatbázis

Egy hasonló példával fejeztük be az előző részt, úgyhogy ebben nem lesz sok újdonság:

docker-compose.yml
version: "3"
volumes:
  database-data:
services:
  database:
    image: mysql:5.7
    volumes:
      - database-data:/data
    environment:
      MYSQL_ROOT_PASSWORD: test
      MYSQL_DATABASE: test
      MYSQL_USER: test
      MYSQL_PASSWORD: test

Ha valamilyen oknál fogva közelebbről is meg szeretnénk vizsgálni ezt az adatbázist és a tartalmát, azt a

docker-compose run --rm database mysql -hdatabase -utest -ptest test

paranccsal tehetjük meg. Amennyiben ez galád módon olyasmire panaszkodik, hogy nem ismeri a "database" host-ot, akkor futtassunk egy ilyen parancsot is és próbáljuk újra:

docker-compose start database

A szerver oldal

A Python csomagkezelés egy kicsit eltér a PHP-ban vagy Node.js-ben megszokottól. Docker nélkül valószínűleg Virtualenv-et használnánk és azon belül telepítenénk a függőségeket, de így a legegyszerűbb az lesz, ha a Dockerfile-ban adjuk meg és a konténer build-elése során települnek majd.

server/Dockerfile
FROM python:3.6-alpine
RUN pip install Flask SQLAlchemy Flask-SQLAlchemy MySQL-Connector
WORKDIR /app/src
EXPOSE 5000
CMD ["flask", "run", "--host=0.0.0.0"]

A Flask alkalmazás az 5000-es porton fog alapból figyelni, úgyhogy ezt jelezzük a Docker felé is. A compose fájl services része pedig a következőkkel egészül ki:

docker-compose.yml
server:
  build: ./server
  depends_on:
    - database
  volumes:
    - ./server:/app
  environment:
    PYTHONDONTWRITEBYTECODE: 1
    FLASK_APP: /app/src/app.py
    DATABASE_URI: mysql+mysqlconnector://test:test@database/test

Környezeti változók segítségével megmondjuk a Python-nak, hogy ne generáljon .pyc és .pyo fájlokat, a flask run-nak, hogy hol találja az alkalmazásunkat, az alkalmazásunknak pedig azt, hogy hogyan tud csatlakozni az adatbázishoz.

A teljes alkalmazás kódjától megkímélem az olvasókat, az a kapcsolódó GitHub repo-ban megtekinthető az összes korábbi példával együtt. Egy apró részletet emelnék csak ki:

server/src/app.py
# az alkalmazás többi részének a helye

if __name__ == '__main__':
    db.create_all()

Ez egy kis egyszerűsítés, hogy könnyebben létre tudjuk hozni az adatbázis szerkezetet a frissen felhúzott MySQL-ben, amit így az alábbi módon tudunk megtenni:

docker-compose run --rm server python -m app

A kliens oldal

Hirtelen nem is tudom hol kezdjem, lesz dolgunk bőven. Talán essünk túl gyorsan a függőségeken:

client/package.json
{
  "devDependencies": {
    "babel-core": "^6.23.1",
    "babel-loader": "^6.3.2",
    "babel-preset-es2015": "^6.22.0",
    "css-loader": "^0.26.1",
    "milligram": "^1.3.0",
    "mithril": "^1.0.1",
    "style-loader": "^0.13.1",
    "webpack": "^2.2.1",
    "webpack-dev-server": "^2.3.0"
  }
}

Az első változatban még React-ot használtam, a hozzá tartozó babel csomaggal, egy külön HTTP klienssel és még Less-t is fordítottam.

Aztán megpróbáltam egyszerűsíteni egy kicsit a dolgokon, aminek ez lett az eredménye. Így a client könyvtárban kiadott npm install parancs már csak a fél világot fogja letölteni nekünk, nem az egészet. Amíg ez megtörténik, térjünk át a webpack konfigurálására.

client/webpack.config.js
const path = require('path');
const webpack = require('webpack');

module.exports = {
  entry: './src/app.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
    publicPath: '/dist/'
  }
};

Nem kell megijedni, ez nem a teljes fájl. Ennyivel nem ússzuk meg. Első körben csak megmondjuk a webpack-nak, hogy hol kezdődik az alkalmazásunk és hova szeretnénk tenni a végterméket. A következő lépésben beállítjuk, hogy mit kezdjen a CSS és JS fájlokkal:

client/webpack.config.js
module.exports = {
  // a korábbi beállítások helye

  module: {
    rules: [
      {
        test: /\.css$/,
        use: [ 'style-loader', 'css-loader' ]
      },
      {
        test: /\.js$/,
        use: [
          { loader: 'babel-loader', options: { presets: [ 'es2015' ] } }
        ]
      }
    ]
  }
};

Ebben a formában a CSS is a JS kódba fog belekerülni, ami azt eredményezi, hogy az oldal CSS nélkül jelenik meg egy pillanatra a betöltődés során. Egy példaalkalmazásnál azt hiszem ezzel együtt lehet élni, de még több függőség behúzásával természetesen ez a probléma is megoldható.

Végül, de nem utolsó sorában hátra van még a dev szerver konfigurálása. Itt van egy olyan huncutság, hogy a /api/ kezdetű kéréseket átdobjuk a Python szervernek, hogy ő kezdjen velük valamit.

client/webpack.config.js
module.exports = {
  // a korábbi beállítások helye

  devServer: {
    host: '0.0.0.0',
    proxy: {
      '/api/*': {
        target: 'http://server:5000',
        pathRewrite: { '^/api' : '' }
      }
    }
  }
};

Ha szerencsénk van, mostanra végzett az npm install, úgyhogy át is térhetünk a dolgok konténeres részére. Végül is ez lenne a fő téma.

client/Dockerfile
FROM node:6.9-alpine
WORKDIR /app
EXPOSE 8080
CMD ["/app/node_modules/.bin/webpack-dev-server"]

Semmi extra, a webpack dev szerverét fogjuk futtatni a konténerben, amit a 8080-as portra konfiguráltunk be az előző lépésekben. Természetesen a compose fájl services részét is ki kell még egészíteni.

docker-compose.yml
client:
  build: ./client
  depends_on:
    - server
  ports:
    - 8080:8080
  volumes:
    - ./client:/app

A kliens konténer függ a szerver konténertől, hogy a webpack dev szerverben beállított proxy-zás tudjon működni. A host 8080-as portját pedig bekötjük a konténer 8080-as portjára. Ezt a szerver konténer esetén nem volt szükséges, mivel csak a kliens konténeren keresztül fogunk vele kommunikálni.

Az alkalmazás kódtól itt is eltekintenék, a repóban megtalálható. Ha minden jól ment, egy docker-compose up kiadása után a http://127.0.0.1:8080/-t meglátogatva valami ilyesmit kellene látnunk:

Ezzel a végére is értünk a második résznek. A következő részben a Docker Compose néhány kevésbé szokványos felhasználását fogjuk megvizsgálni.

No nem arra gondolok, hogy költözzünk ki egy kényelmesen berendezett, jól felszerelt konténerházba. Bár lehet, hogy az is segít, de maradjunk a Docker-nél (azon belül is a Docker Compose nevezetű eszköznél) és annál, hogy hogyan tudja segíteni a fejlesztő-, teszt- és build környezetek létrehozását. Kezdjük is valami egyszerűvel.

Hello Compose

Tegyül fel, hogy van egy kis PHP-s alkalmazásunk, mindenféle függőség nélkül:

www/index.php
<?php
echo "Hello World";

Szó, ami szó, nem vittük túlzásba a dolgot. A hozzá tartozó Docker Compose konfiguráció valahogy így nézhetne ki hozzá:

docker-compose.yml
version: "3"
services:
  app:
    image: php:7.1-alpine
    ports:
      - 8080:8080
    command: php -S 0.0.0.0:8080 -t /app/www
    volumes:
      - .:/app

Ezek után, ha egy docker-compose up parancsot kiadunk a megfelelő könyvtárban, elindul a beépített PHP-s webszerver a 8080-as porton és a http://127.0.0.1:8080/ oldalra rápillantva láthatjuk is az ismerős szöveget.

Létezik pl. Apache vagy FPM alapú csomag is, de ha nincsenek nagy igényeink, a beépített webszerver használata talán a legegyszerűbb megoldás.

Hello Composer

Ez mind szép és jó, de nagy rá az esély, hogy szükségünk van valamilyen külső csomagra, amit Composer-rel szeretnénk telepíteni. Ennek a megvalósítására több lehetőségünk is van. Használhatunk például egy olyan Docker image-et, ami tartalmazza a composer parancsot és telepíthetjük a függőségeket a konténeren belül:

docker-compose.yml
version: "3"
services:
  app:
    image: composer:1.3
    volumes:
      - .:/app

Telepíteni pedig a következő paranccsal tudunk:

$ docker-compose run --rm app composer require phpunit/phpunit

A megoldás szépséghibája, hogy a composer által generált fájlok root felhasználóval jönnek létre a host gépen. Ez nem feltétlen kell, hogy zavarjon minket, mivel írni csak a konténeren keresztül fogunk oda, olvasni pedig bárki tudja így is.

Szükség esetén elpakolhatjuk a generált fájlokat egy Docker volume-ra, így csak egy vendor könyvtárunk lesz root-tal, ami üres.

docker-compose.yml
version: "3"
volumes:
  vendor-files:
services:
  app:
    image: composer:1.3
    volumes:
      - .:/app
      - vendor-files:/app/vendor

Már el is hangzott a megoldás fő problémája, a vendor könyvtár üres, a host gép nem látja a generált fájlokat, így például nem tudjuk igénybe venni ezekre az IDE kódkiegészítő funkcióit se.

Lehet persze a composer parancsot simán Docker-en kívül is futtatni, így megfelelő felhasználóval jönnek létre a fájlok és az IDE is látja őket. Ebben az esetben viszont a fejlesztők gépein és a build gépeken is ott kell lennie a telepítéshez szükséges eszközöknek.

Hello MySQL

Feltelepítettük a kedvenc keretrendszerünket, ideje beszerezni egy adatbázist is hozzá. Először is építünk egy olyan PHP-s image-et, ami tartalmazza a megfelelő kiterjesztéseket:

Dockerfile
FROM php:7.1-alpine

RUN docker-php-ext-install pdo pdo_mysql

A konténerek alapesetben úgy működnek, hogy a bennük történt változások nem nevezhetőek maradandónak, ami esetünkben azt jelenti, hogy minden docker-compose up hívásnál egy üres adatbázist kapunk. Ez felhasználástól függően akár még elvárt működés is lehet, de ha tartósabb megoldást szeretnénk, a volume-ok ebben is segítenek.

docker-compose.yml
version: "3"
volumes:
  database-data:
services:
  app:
    build: .
    ports:
      - 8080:8080
    depends_on:
      - database
    command: php -S 0.0.0.0:8080 -t /app/www
    volumes:
      - .:/app
  database:
    image: mysql:5.7
    volumes:
      - database-data:/data
    environment:
      MYSQL_ROOT_PASSWORD: test
      MYSQL_DATABASE: test
      MYSQL_USER: test
      MYSQL_PASSWORD: test

Érdemes megfigyelni a depends_on részt, amitől a database konténer előbb fog elindulni, mint az app. Itt érdemes megjegyezni, hogy csak előbb indul, nem várja azt meg, hogy be is fejezze az elindulását, így ha az app elég gyors, előállhat olyan helyzet, hogy még nem éri el az adatbázist. Ennek kivédésére léteznek megoldások, de ezt meghagyom házi feladatnak.

A másik érdekesség a környezeti változók a database konténer beállításainál. Az image-eknek egész sok beállítása lehet (pl. a mysql image esetén megadhatunk fájlokat is, amikből inicializálódik az adatbázis) és szolgáltathatnak extra parancsokat, hogy könnyebb legyen rájuk saját image-et építeni (mint pl. a php-s image docker-php-ext-install parancsa). Ezekről az image Docker Hub-os oldalán vagy közvetlenül a Docker image forrásából is tájékozódhatunk.

A konténerek között a Docker saját hálózatot épít ki, aminek segítségével a PHP-s kódból egész egyszerűen elérhetjük az adatbázist (a docker-compose.yml fájlban szereplő service név alapján):

www/index.php
<?php

$db = new PDO('mysql:host=database;dbname=test', 'test', 'test');
$stmt = $db->query('SHOW VARIABLES LIKE "%version%"');

var_dump($stmt->fetchAll(PDO::FETCH_ASSOC));

Első körben ennyi lett volna, a példák megtekinthetőek a kapcsolódó Github repóban. A következő részben egy kicsivel bonyolultabb példákkal folytatjuk.

Nagyjából két éve kezdtem el írni egy statikus blog generátort, csak úgy a saját magam szórakoztatására. Közel egy éve többé-kevésbé el is készült a frissített blog design-nal együtt, de valahogy sose jutottam el odáig, hogy lecseréljem a WordPress-es megoldást rá.

Aztán a héten törént egy váratlan esemény. Valószínűleg egy WordPress sebezhetőséget kihasználva elkezdtek lecserélődni meglévő bejegyzések az oldalon. Ennek a problémának a gyors megoldása pedig az lett, hogy leváltottam a jó öreg WordPress-t az új, még nem 100%-osan kész statikus változatra. További érdekesség, hogy az oldal alcímét (system failure) nem az esemény ihlette, hanem jóval korábban született. :)

Már egy ideje az íróasztal fiókom mélyén tanyázik egy MCP23017 chip, amivel például a Raspberry Pi GPIO portjait lehet bővíteni, szám szerint 16 darab új csatlakozással. A 16 darab csatlakozás pedig pont elég arra, hogy 16 darab LED segítségével megjelenítsünk egy bináris órát (bár sajnos csak 12 órás megjelenítéssel).

Hardver és összedrótozás

Először is talán ismerkedjünk meg egy kicsit közelebbről az MCP23017-tel. I²C buszon lehet vele kommunikálni, a hardver címét az A0-2 lábak határozzák meg.

A GPA0-7 és GPB0-7 lábakra fogjuk rákötni a ledeket (esetleg a LED-hez kellhet még egy ellenállás is, de én attól eltekintettem), A VSS-re és az A0-2 lábakra megy a föld (ez a 0x20-as hardvercímet fogja eredményezni), a VDD-re és a RESET-re a 3.3V, az SLC és SDA pedig a Raspberry Pi azonos nevű kivezetéseivel lesz összekötve.

Katt a nagyobb változatért.

Bónuszként a kábelrengeteg miatt még úgy is fog tűnni, mintha valami bonyolult dolog lenne. Ezen kívül, ami még érdekel minket, azok az MCP23017 regiszterei és azok címei. Legalábbis egy részük:

0x00 IODIRA A GPA0-7 lábak inputként vagy outputként működjenek
0x01 IODIRB A GPB0-7 lábak inputként vagy outputként működjenek
0x12 GPIOA Ennek a segítségével lehet a GPA0-7 lábakat elérni
0x13 GPIOB Ennek a segítségével lehet a GPB0-7 lábakat elérni

A chip dokumentációjában utána lehet nézni a többinek is, ha valakit esetleg részletesebben is érdekelne.

Konfigurálás

Ennyit a hardverről, végre rátérhetünk a dolog szoftveres részére. Mint azt már fentebb említettem, a chip I²C-t használ, ami alapból nincsen bekapcsolva az RPi-n. Először is a /etc/modules fájlba kell két új sor, mondjuk a végére:

i2c-bcm2708
i2c-dev

Aztán a /etc/modprobe.d/raspi-blacklist.conf fájlban a blacklist i2c-bcm2708 sort kell törölni (vagy kikommentelni). Szükség van ezen kívül még néhány csomagra telepítésére is:

$ sudo apt-get install python-smbus i2c-tools

Ha ez is végzett, akkor már csak egy újraindítás, és el is kezdhetjük használatba venni a szerkezetet.

Életre keltés

Először is szükségünk lesz a chip címére, amit az i2cdetect parancs segítségével tudunk kideríteni. A fenti drótozással elvileg ezt kell kapnunk:

$ sudo i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: 20 -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --

Amiből kiderül, hogy 0x20 az eszköz címe (régebbi RPi-n előfordulhat, hogy sudo i2cdetect -y 0 a nyerő parancs), úgyhogy rá is térhetünk a kódra.

import time
import smbus

Az már a konfigurálás során kiderült, hogy a Python smbus modulját fogjuk használni (meg egy kis időt, ha már egyszer órát csinálunk).

i2c = 1
addr = 0x20

Néhány beállítás: az i2c értéke ugyanaz, mint amit az i2cdetect parancsban is használtunk -y paraméterként, az addr értéke pedig az i2cdetect válaszából kinyert hardver cím.

bus = smbus.SMBus(i2c)
bus.write_byte_data(addr, 0x00, 0x00)
bus.write_byte_data(addr, 0x01, 0x00)

Kezdődhet a kommunikáció. A chip 0x00-s és 0x01-es regisztereit is kinullázzuk, ami mindkét nyolcas csatlakozó csoportot output-ra állítja.

try:
  while True:
    now = time.localtime()
    h = now.tm_hour
    if h > 12:
      h = h - 12
    
    x = '{:04b}{:06b}{:06b}'.format(h, now.tm_min, now.tm_sec)
    
    bus.write_byte_data(addr, 0x12, int(x[8:], 2))
    bus.write_byte_data(addr, 0x13, int(x[:8], 2))
    time.sleep(0.01)
except KeyboardInterrupt:
    bus.write_byte_data(addr, 0x12, 0x00)
    bus.write_byte_data(addr, 0x13, 0x00)
    print

Először átvarázsoljuk az aktuális időt egy nekünk megfelelő bináris formátumba, aztán beírjuk az egyes byte-okat a 0x12-es és 0x13-as regiszterekbe. Aztán ez megy tovább a végtelenségig. Vagyis amíg Ctrl+C-t nem nyomunk, akkor kinullázzuk mindkét regisztert, ami az összes ledet lekapcsolja. A végeredmény pedig:

Összes bejegyzés