Egy ügyfelünknél probléma volt, hogy a szerverszobában gyakran meghibásodott a légkondicionáló, vagy valami egyéb okból megemelkedett a levegő hőmérséklete, ami a szerverek túlmelegedéséhez vezetett. Ennek megfelelően igény lépett fel egy olyan rendszer kiépítésére, ami a hőmérséklet emelkedése esetén figyelmeztet, és ha kell, kikapcsolja az érintett gépeket. Az egyik szerver egy Banana Pi R1 volt, ennek megfelelően a Banana Pi hivatalos hőmérő moduljára esett a választás, a költséghatékonyság figyelemben tartásával. Ebben a cikkben a szükséges hardver beállításával foglalkozunk.

I2C, SMBus, DDC, …

Az I2C (amely egébként az NXP védjegye) az Inter-Integrated Circuit (integrált áramkörök közötti) szóösszetétel rövidítése. Ez egy aránylag egyszerű, olcsón megvalósítható soros busz, amely viszonylag kis sávszélességet tesz lehetővé.

Az SMBus és a DDC is az I2C-vel többé-kevésbé kompatibilis buszok, hasonló lehetőségekkel, hardveres és szoftveres követelményekkel. Az SMBus a System Management Bus (rendszerkezelési busz) rövidítése, és leginkább az alaplapokra épített szenzorok, ventillátor-vezérlők és hasonlók kezelésére szolgál. A DDC a Display Data Channel (kijelző adat-csatorna) rövidítése, és VGA, DVI, és HDMI monitorokban egyaránt használatos. Ez utóbbival nem fogunk foglalkozni.

A hivatalos hőmérő

A hivatalos hőmérő elméletileg egy NXP gyártmányú LM75A típusú szenzor, ami I2C-n keresztül csatlakozik az alapgéphez. A hivatalos beüzemelési útmutató (amit én is csak a hazai forgalmazón keresztül kaptam meg, mert a bananapi.com-on már nem elérhető) azzal kecsegtet, hogy csak rácsúsztatjuk a modult Banana Pi 1-9-ig számozott GPIO tüskéire (természetesen kikapcsolt állapotban), és már készen is vagyunk. A kernel elvileg felismeri a szenzort, és a következő indításnál a dmesg-bufferben a következőt hagyja:

lm75 1-0048: hwmon0: sensor 'lm75'

Ezek után a sensors paranccsal használhatjuk a szenzort. Persze ennyire csak a mesében egyszerű minden. Sajnos hasonló üzenetnek nyoma sem volt, és a sensors parancs sem mutatta meg a szenzort.

i2c-tools

Egy kis kísérletezés után a felhagytam az lm_sensors keretrendszer használatával, hiába írhattam volna meg a megfelelő kernel-drivert, ehhez újrafordítani a kernelt is kínos lett volna, arról nem is beszélve, hogy ezt egy termelési szerveren kellett volna tesztelnem. Viszont az I2C alacsony szintű használatához megvannak az eszközök Linuxban, ezért telepítettem is az i2c-tools csomagot:

sudo apt-get install i2c-tools

Ezzel kilistáztam az i2c-eszközöket:

$ sudo i2cdetect -l
i2c-0   i2c             sunxi-i2c.0                             I2C adapter
i2c-1   i2c             sunxi-i2c.1                             I2C adapter
i2c-2   i2c             sunxi-i2c.2                             I2C adapter
i2c-3   i2c             sunxi-i2c.3                             I2C adapter
i2c-4   i2c             sunxi-hdmi-i2c                          I2C adapter

Az utolsóról tudjuk, hogy az a Banana Pi HDMI csatlakozójához tartozó DDC. A többi négy busz viszont rejtélyes. Jobb híján kénytelenek vagyunk végigpróbálni ezeket a buszokat, és megnézni, mi van rajtuk. Ez a művelet nem igazán biztonságos, a egy rosszul megtervezett eszköz esetleg megállíthatja az I2C busz működését, vagy másodpercekre megakaszthat egy SMBus-t.
Mivel elméletileg csak olvas az eszközökről, más mellékhatása ennek nincsen, de sajnos ez sem kizárható. Ennek fényében indítsuk el a következőket:

$ sudo i2cdetect sunxi-i2c.1
WARNING! This program can confuse your I2C bus, cause data loss and worse!
I will probe file /dev/i2c-1.
I will probe address range 0x03-0x77.
Continue? [Y/n] y
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
70: -- -- -- -- -- -- -- --                         
$ sudo i2cdetect sunxi-i2c.2
WARNING! This program can confuse your I2C bus, cause data loss and worse!
I will probe file /dev/i2c-1.
I will probe address range 0x03-0x77.
Continue? [Y/n] y
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
40: -- -- -- -- -- -- -- -- 48 -- -- -- -- -- -- -- 
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
70: -- -- -- -- -- -- -- --

Találat!
Pont ezt kerestük: az adattábla szerint a 0x48 és a 0x4f címek között lehet beállítani az LM75 címét, úgy tűnik, a modul készítői a 0x48-at válaszották.

Nincs más hátra, mint előre: az adattábla szerint a 0-s, 16 bit széles regiszter tárolja a hőmérsékletet.

$ sudo i2cget sunxi-i2c.2 0x48 00 w
WARNING! This program can confuse your I2C bus, cause data loss and worse!
I will read from device file /dev/i2c-2, chip address 0x48, data address
0x00, using read word data.
Continue? [Y/n] y
0x4021

Amit kapunk, nos, az a szenzor hőmérséklete, de az adattáblához képest fel vannak cserélve a bájtok, tehát az első bájtban vannak a kevésbé szignifikáns bitek.

python-smbus

Mivel végre valahára megtaláltuk a szenzort, és leolvastuk a hőmérsékletet, ideje írni egy programot erre. Ehhez a Python-ra esett a választásom, mivel jól ismerem, aránylag gyors benne fejlesztani, és van hozzá I2C modul. Ez utóbbit az alábbi paranccsal telepíthetjük:

sudo apt-get install python-smbus

Vegyük észre, hogy ez a modul elvileg SMBus-hoz készült, de az I2C és az SMBus szoftveres szinten gyakorlatilag felcserélhetőek. A hőmérséklet kiolvasásához az alábbi Python kódot írtam:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from smbus import SMBus
import struct

def readTemp(busNumber, deviceAddress):
    bus = SMBus(busNumber)
    rawInt = bus.read_word_data(deviceAddress, 0)
    temperature = struct.unpack('<h', struct.pack('!H', rawInt))[0]/256.0
    return temperature

if __name__ == "__main__":
    busNumber = 2
    deviceAddress = 0x48
    print(readTemp(busNumber, deviceAddress))

Ennek az egyetlen érdekes mozzanata, hogy a struct modul segítségével fordítja meg a kapott 16-bites egészt, és emellett előjelessé is teszi. Nem mintha fagy lenne várható a szerverszobában.

udev

Most már ki is tudjuk olvasni a hőmérsékletet a szenzorból, de még ehhez is root jogosultságok kellenek. Változtassuk ezt meg! Erre a szabványos eszköz az udev. A recept:

  1. Hozzunk létre egy i2c csoportot
  2. Adjuk hozzá a kívánt felhasználókat az i2c csoporthoz
  3. Állítsuk be az udev-et, hogy az i2c-eszközök csoportja legyen i2c
  4. Érjük el, hogy az udev az új beállításokkal is létrehozza a kérdéses eszközt

Az első két lépés Linuxos rutin:

$ sudo addgroup --system i2c
$ sudo adduser temperature i2c

Az udev kezelése egy picit bonyolultabb. Hozzuk létre az alábbi fájlt /etc/udev/rules.d/80-i2c.rules néven:

KERNEL=="i2c-[0-9]*", GROUP="i2c"

Ezek után a legegyszerűbb lenne újraindítani a számítógépet, de ez egy termelési szerver! Így aztán tegyük a következőt:

$ sudo udevadm test /sys/bus/i2c/devices/i2c-2^Grun_command: calling: test
adm_test: version 175
This program is for debugging only, it does not run any program,
specified by a RUN key. It may show incorrect results, because
some values may be different, or not available at a simulation run.

parse_file: reading '/lib/udev/rules.d/42-qemu-usb.rules' as rules file
parse_file: reading '/lib/udev/rules.d/50-udev-default.rules' as rules file
parse_file: reading '/lib/udev/rules.d/55-dm.rules' as rules file
parse_file: reading '/lib/udev/rules.d/60-bridge-network-interface.rules' as rules file
parse_file: reading '/lib/udev/rules.d/60-cdrom_id.rules' as rules file
parse_file: reading '/lib/udev/rules.d/60-gnupg.rules' as rules file
parse_file: reading '/lib/udev/rules.d/60-persistent-alsa.rules' as rules file
parse_file: reading '/lib/udev/rules.d/60-persistent-input.rules' as rules file
parse_file: reading '/lib/udev/rules.d/60-persistent-serial.rules' as rules file
parse_file: reading '/lib/udev/rules.d/60-persistent-storage-dm.rules' as rules file
parse_file: reading '/lib/udev/rules.d/60-persistent-storage-tape.rules' as rules file
parse_file: reading '/lib/udev/rules.d/60-persistent-storage.rules' as rules file
parse_file: reading '/lib/udev/rules.d/60-persistent-v4l.rules' as rules file
parse_file: reading '/lib/udev/rules.d/61-accelerometer.rules' as rules file
parse_file: reading '/run/udev/rules.d/61-dev-root-link.rules' as rules file
parse_file: reading '/etc/udev/rules.d/70-persistent-net.rules' as rules file
parse_file: reading '/lib/udev/rules.d/70-udev-acl.rules' as rules file
parse_file: reading '/lib/udev/rules.d/75-cd-aliases-generator.rules' as rules file
parse_file: reading '/lib/udev/rules.d/75-net-description.rules' as rules file
parse_file: reading '/lib/udev/rules.d/75-persistent-net-generator.rules' as rules file
parse_file: reading '/lib/udev/rules.d/75-probe_mtd.rules' as rules file
parse_file: reading '/lib/udev/rules.d/75-tty-description.rules' as rules file
parse_file: reading '/lib/udev/rules.d/78-sound-card.rules' as rules file
parse_file: reading '/lib/udev/rules.d/80-drivers.rules' as rules file
parse_file: reading '/etc/udev/rules.d/80-i2c.rules' as rules file
parse_file: reading '/lib/udev/rules.d/80-networking.rules' as rules file
parse_file: reading '/lib/udev/rules.d/85-hwclock.rules' as rules file
parse_file: reading '/lib/udev/rules.d/91-permissions.rules' as rules file
parse_file: reading '/lib/udev/rules.d/95-keyboard-force-release.rules' as rules file
parse_file: reading '/lib/udev/rules.d/95-keymap.rules' as rules file
parse_file: reading '/lib/udev/rules.d/95-udev-late.rules' as rules file
udev_rules_new: rules use 24588 bytes tokens (2049 * 12 bytes), 15190 bytes buffer
udev_rules_new: temporary index used 16080 bytes (804 * 20 bytes)
udev_device_new_from_syspath: device 0xc6b748 has devpath '/devices/platform/sunxi-i2c.2/i2c-2'
udev_device_new_from_syspath: device 0xc6ba70 has devpath '/devices/platform/sunxi-i2c.2/i2c-2'
udev_device_read_db: no db file to read /run/udev/data/+i2c:i2c-2: No such file or directory
ACTION=add
DEVPATH=/devices/platform/sunxi-i2c.2/i2c-2
SUBSYSTEM=i2c
UDEV_LOG=6
USEC_INITIALIZED=222655819036

Eddig nincs nagy baj.

$ sudo udevadm control --reload
$ sudo udevadm trigger /sys/devices/platform/sunxi-i2c.2
$ stat /dev/i2c-2
     Fájl: ”/dev/i2c-2”
    Méret: 0            blokkok: 0          IO-blokk: 4096   speciális karakterfájl
   Eszköz: 5h/5d        I-node: 1116        linkek: 1     eszköztípus: 59,2
Hozzáférés: (1660/crw-rw---T)  Uid: (    0/    root)   Gid: (  111/     i2c)
   Elérés: 2016-07-06 22:35:52.951145632 +0200
Módosítás: 2016-07-06 22:35:52.951145632 +0200
 Változás: 2016-07-06 22:35:52.951145632 +0200
 Születés: -

És készen is vagyunk.

A cikk szerzője Kovács Bálint, a Trinity Hall, Cambridge-i egyetem hallgatója