Csatlakoztatjuk a vízmérőt az okos házhoz

Régen a háztartási automatizálási rendszerek, vagy ahogy gyakran nevezik őket, az „okosházak” rettenetesen drágák voltak, és csak a gazdagok engedhették meg maguknak őket. Ma a piacon már találhatók megfizethető készletek érzékelőkkel, gombokkal/kikapcsolókkal és végrehajtó eszközökkel, amelyek a világítás, aljzatok, szellőztetés, vízellátás és egyéb fogyasztók irányítására szolgálnak. Még a legügyetlenebb DIY-rajongók is könnyedén hozzáférhetnek a csodához, és olcsón építhetnek okosházi eszközöket.

Csatlakoztatjuk a vízmérőt az okos házhoz

Általában a kínált eszközök érzékelőkből vagy végrehajtó mechanizmusokból állnak. Ezek lehetővé teszik az olyan forgatókönyvek egyszerű megvalósítását, mint például "amikor a mozgásérzékelő aktiválódik, kapcsolja be a világítást" vagy "a kijáratnál lévő kapcsoló lekapcsolja a világítást az egész lakásban". De a telemetriával valahogy nem alakult jól a dolog. Legjobb esetben csak egy hőmérséklet- és páratartalom-grafikon, vagy egy adott aljzat azonnali teljesítménye áll rendelkezésre.

Nemrég vízórákat szereltem fel impulszus kimenettel. Minden liter, amely áthalad az órán, aktiválja a reed relét, amely zárja a kontaktust. Csak annyi maradt hátra, hogy csatlakozzak a vezetékekhez, és próbáljam hasznossá tenni. Például elemezhetem a vízfogyasztást óránként és a hét napjain. Ha pedig több vízvezeték is van a lakásban, akkor kényelmesebb mindent egy képernyőn látni, mint fáklyával mászkálni a nehezen hozzáférhető helyeken.

A következő bejegyzésben az én ESP8266 alapú eszközöm változatát mutatom be, amely számolja a vízmérőktől érkező impulzusokat, és MQTT-n keresztül küldi az adatokat az okosház szerverére. A programozás micropythonban fog történni, az uasyncio könyvtár használatával. A firmware létrehozása során több érdekes nehézségbe ütköztem, amelyekről ebben a cikkben szintén beszámolok. Lássuk hát!

Séma

Csatlakoztatjuk a vízmérőt az okos házhoz

Az egész rendszer szíve egy ESP8266 mikrokontroller modul. Eredetileg ESP-12-t terveztem, de az sajnos hibás lett. Meg kellett elégednem a készleten lévő ESP-07 modullal. Szerencsére ezek az I/O kivezetések és a funkciók szempontjából azonosak, a különbség csupán az antennában rejlik – az ESP-12-nek beépített antennája van, míg az ESP-07-nek külső antennája. Mindenesetre még antenna nélkül is jól fogja a WiFi jelet a fürdőszobámban.

A modul bekötése standard:

  • reset gomb húzással és kondenzátorral (bár mindkettő már belül is megtalálható a modulban)
  • Az enable (CH_PD) jel csatlakoztatva van az áramforráshoz
  • A GPIO15 a földre van húzva. Erre csak az indításkor van szükség, de nekem amúgy sincs semmi dolgom ezen a lábon.

A modul firmware módba állításához össze kell zárni a GPIO2-t a földdel, és hogy kényelmesebb legyen, előkészítettem egy Boot gombot. Normál állapotban ez a pin tápfeszültségre van húzva.

A GPIO2 állapotát csak a munka kezdetén ellenőrizzük — a tápláláskor vagy közvetlenül a reset után. Így a modul vagy a megszokott módon indul, vagy átmegy firmware módba. A betöltés után ezt a kimenetet normál GPIO-ként használhatjuk. Mivel már van egy gomb, bármilyen hasznos funkciót ráköthetek.

A programozáshoz és hibakereséshez UART-ot fogok használni, amit a csatlakozóra kivittem. Amikor szükséges, egyszerűen csatlakoztatom a USB-UART átalakítót. Csak arra kell figyelni, hogy a modul 3.3V-ról működik. Ha elfelejtem átkapcsolni az átalakítót erre feszültségre és 5V-ot kap, valószínűleg ki fog égni a modul.

Az elektromossággal a fürdőszobában nincsenek problémáim — a konnektor körülbelül egy méterre található a mérőktől, szóval 220V-ból fogom táplálni. Az áramforrásom egy kis HLK-PM03 blokkból fog működni. A Tenstar Robot-tól. Személy szerint nekem nehezemre esik az analóg és erő-elektronika, de itt van egy kis házban elhelyezett kész kapcsolóüzemű tápegység.

A működési módok jelzésére LED-et készítettem, amit a GPIO2-höz csatlakoztattam. Egyébként nem forrasztottam fel, mivel az ESP-07 modulban már van egy LED, ami szintén a GPIO2-höz van kötve. De a lapon hadd legyen — hátha szeretném kivezetni ezt a LED-et a házra.

Most jön a legérdekesebb rész. A vízmérőknek nincs logikájuk, nem kérdezhetjük le a jelenlegi értékeket. Az egyetlen, ami elérhető számunkra, az az impulzusok — a kontaktusok zárása minden liter után. A kontaktusok kimenetei a GPIO12/GPIO13-ra vannak vezetve. A húzóellenállást programozással fogom bekapcsolni a modulon belül.

Kezdetben elfelejtettem bekalkulálni az R8 és R9 ellenállásokat, és az én nyomtatott áramkörömben nincsenek. De mivel már közzéteszem a schemát nyilvánosan, érdemes lenne korrigálni ezt a mulasztást. Az ellenállásokra van szükség, hogy ne égessenek ki a portok, ha a firmware hibásan működik és 1-et állít be a pinen, miközben a relé összeölel a földdel (ellenállással a maximális áram 3.3V/1000Ω = 3.3mA).

Itt az ideje, hogy gondolkodjunk, mi a teendő, ha áramkimaradás történik. Az első lehetőség, hogy a szervertől kérjük el az induló értékeket. De ez jelentős bonyolultságot jelentene a kommunikációs protokollban. Továbbá, az eszköz működésének megvalósítása ilyen esetben a szerver állapotától függ. Ha az áramkimaradás után a szerver nem indulna el (vagy később indulna), akkor a vízmérő nem tudná lekérdezni az induló értékeket, és hibásan működne.

Ezért döntöttem úgy, hogy az értékek tárolását egy, az I2C-hez csatlakoztatott memóriachipben valósítom meg. Nincs különösebb követelményem a flash memória méretével kapcsolatban — mindössze két számot kell megőriznem (a meleg és hideg vízmérők által mért liter mennyiséget). Még a legkisebb modul is megfelel. Viszont a ciklusok számára érdemes figyelni. A legtöbb modul esetében ez 100 ezer ciklus, egyeseknél akár egymillió.

Elsőre úgy tűnik, hogy az egymillió sok. De az elmúlt 4 évben a lakásomban kicsit több mint 500 köbméter vizet fogyasztottam, ami 500 ezer liter! És ez 500 ezer bejegyzés a flash memóriában. És ez csak a hideg víz. Persze, újra lehetne forrasztani a chipeket kétévente, de felfedeztem a FRAM chipeket. Programozási szempontból ezek ugyanazok, mint az I2C EEPROM-ok, csak óriási mennyiségű újraírási ciklussal bírnak (százmilliók). Csak éppen eddig nem jutottam el a boltba, ahol ilyen chipeket árulnak, ezért egyelőre a szokásos 24LC512 áll a helyén.

Nyomtatott áramkör

Eredetileg otthoni körülmények között terveztem készíteni az áramkört. Ezért a nyákot egylapos formában terveztem. De miután egy óráig szenvedtem a lézerpákkal és a forrasztási maszk nélkül (anélkül nem igazán helyénvaló), végül úgy döntöttem, hogy megrendelem a nyákokat a kínaiaktól.

Csatlakoztatjuk a vízmérőt az okos házhoz

Már majdnem megrendeltem a nyákot, amikor rájöttem, hogy a flash memória chipek mellett az I2C buszhoz csatlakoztatható valami más is, például egy kijelző. Hogy pontosan mit fogok megjeleníteni rajta — az még kérdés, de az áramkörön el kell készíteni. Nos, ha már úgyis a gyárból tervezem a nyákokat rendelni, akkor már nincs értelme magam korlátozni az egylapos megoldásra, ezért az I2C vonalak egyedüliként a nyák hátoldalán kerültek elhelyezésre.

A egyoldalas nyomtatott áramkörhöz egy nagyobb hiba is kapcsolódott. Mivel az áramkör egyoldalas rajzról készült, a nyomtatott vezetékeket és az SMD komponenseket terveztem az egyik oldalon elhelyezni, míg a kimeneti elemeket, csatlakozókat és tápegységet a másikon. Amikor egy hónap múlva megkaptam a nyomtatott áramköröket, elfelejtettem az eredeti tervet, és minden komponenst az elülső oldalra forrasztottam. Csak amikor a tápegység forrasztásához érkeztem, derült ki, hogy a + és - pólusok megfordítva vannak. Át kellett alakítanom a vezetékeket. A fenti képen már módosítottam a kapcsolást, de a földvezető a nyomtatott áramkör egy részből a másikba a Boot gomb lábaival van összekötve (bár a második rétegen is el lehetett volna vezetni a nyomtatott vezetékeket).

Így sikerült

Csatlakoztatjuk a vízmérőt az okos házhoz

Tokat

A következő lépés a ház. 3D nyomtató birtokában ez nem jelent problémát. Nem különösebben bonyolítottam túl — egyszerűen megrajzoltam a szükséges méretű dobozt és kivágtam a szükséges helyeket. A tető kis csavarokkal rögzíthető a házhoz.

Csatlakoztatjuk a vízmérőt az okos házhoz

Már korábban említettem, hogy a Boot gomb általános használatra is alkalmas — ezt fogjuk a hátlapra kihozni. Ehhez egy speciális "mélyedést" rajzoltam, ahol a gomb található.

Csatlakoztatjuk a vízmérőt az okos házhoz

A ház belsejében is vannak kiemelkedések, amelyekre a nyomtatott áramkör rögzíthető egyetlen M3-as csavarral (a nyomtatott áramkörön több hely nem volt).

A kijelzőt már akkor választottam, amikor az első prototípust kinyomtattam. A standard, kétsoros kijelző nem fért el ebben a házban, viszont találtam egy OLED kijelzőt, az SSD1306-ot 128×32 méretben. Kicsi, de nem kell mindennap ránéznem — elmegy.

Miközben próbáltam kitalálni, hogyan fognak a vezetékek futni, úgy döntöttem, hogy a kijelzőt a ház közepére ragasztom. Az ergonómiája persze elég gyenge — a gomb fent, a kijelző lent. De már mondtam, hogy a kijelző ötlete túl későn jött, és lusta voltam áttervezni az áramkört, hogy áthelyezzem a gombot.

Az eszköz összeállt. A kijelzőmodul ragasztóval van rögzítve.

Csatlakoztatjuk a vízmérőt az okos házhoz

Csatlakoztatjuk a vízmérőt az okos házhoz

A végső eredményt a KDPV-n láthatjuk.

Firmware

Lépjünk át a szoftveres részre. Az ilyen kis projekteknél nagyon szeretem a Python nyelvet használni (micropython)- a kód nagyon kompakt és átlátható. Szerencsére itt nincs szükség a regiszterek szintjéig menni, hogy mikroszekundumokat nyerjünk ki — mindent meg lehet oldani Pythonból.

Úgy tűnik, minden egyszerű, de nem annyira – az eszközön több független funkcióval készülünk.

  • A felhasználó megnyomja a gombot és nézi a kijelzőt.
  • A literek folyamatosan frissítik az értékeket a flash memóriában.
  • A modul figyeli a WiFi jelet és újracsatlakozik, ha szükséges.
  • De a villogó lámpa nélkül egyáltalán nem lehet élni.

Nem engedhetjük meg, hogy egy funkció ne működjön, ha a másik valamilyen okból lelassul. Már belefáradtam a kaktuszokba más projektekben, és most már így látom a hibákat, mint például "kihagytunk egy literet, mert éppen a kijelző frissült" vagy "a felhasználó semmit sem tud tenni, míg a modul csatlakozik a WiFi-hez". Persze, egyes dolgokat megszakításokkal is lehet végezni, de belefuthatunk az időtartam, a hívás mélysége vagy a nem atomikus változók megváltoztatásának korlátaiba. Ahol a kód mindent és azonnal csinál, az gyorsan káosszá válik.

A komolyabb projekthez A klasszikus preemptív multitaskingot és a FreeRTOS-t használtam, de ebben az esetben sokkal megfelelőbbnek bizonyult a kooperatív (coroutines) és az uasync könyvtár. A Python korutinnal való megvalósítása egyszerűen nagyszerű – a programozónak minden kényelmesen és egyszerűen van megoldva. Csak írj logikát, csak mondd meg, hogy hol lehet átváltani a szálak között.

A preemptív és a konkurens multitasking közötti különbségeket ajánlom fakultatív tanulmányozásra. Most pedig nézzük meg végre a kódot.

#####################################
# Counter class - implements a single water counter on specified pin
#####################################
class Counter():
    debounce_ms = const(25)
    
    def __init__(self, pin_num, value_storage):
        self._value_storage = value_storage
        
        self._value = self._value_storage.read()
        self._value_changed = False

        self._pin = Pin(pin_num, Pin.IN, Pin.PULL_UP)

        loop = asyncio.get_event_loop()
        loop.create_task(self._switchcheck())  # Thread runs forever

Minden számláló egy Counter osztály példányával dolgozik. Először is az EEPROM-ból (value_storage) kiolvassuk a számláló kezdeti értékét – így valósul meg az áramkimaradás utáni visszaállítás.

A lábat beépített felhúzással inicializáljuk: ha a Reed kapcsoló zárva van – a vonalon nulla, ha nyitva van, a vonal a tápegységhez húzódik és a vezérlő egység egyes értéket olvas.

Itt egy külön feladatot is indítunk, amely a láb kérdezését végzi. Minden számláló elindítja a saját feladatát. Íme a kódja:

    """ A láb kérdése és az érték előrehaladása, amikor egy új liter áthalad """\n    async def _switchcheck(self):\n        last_checked_pin_state = self._pin.value()  # Kezdeti állapot lekérése\n\n        # Kérdése egy láb állapotváltozást\n        while True:\n            state = self._pin.value()\n            if state != last_checked_pin_state:\n                # Az állapot megváltozott: cselekedjünk most.\n                last_checked_pin_state = state\n                if state == 0:\n                    self._another_litre_passed()\n\n            # Figyelmen kívül hagyjuk a további állapotváltozásokat, amíg a kapcsoló be nem áll.\n            await asyncio.sleep_ms(Counter.debounce_ms)

A 25ms delay is necessary to filter contact bounce, and it also regulates how often the task wakes up (while this task is sleeping, other tasks are running). Every 25ms, the function wakes up, checks the pin, and if the reed switch is closed, it means another liter has passed through the counter and needs to be processed.

    def _another_litre_passed(self):
        self._value += 1
        self._value_changed = True

        self._value_storage.write(self._value)

Processing the next liter is trivial — it's simply about increasing the counter. Also, it would be good to write the new value to flash.

For ease of use, 'accessors' are provided.

    def value(self):
        self._value_changed = False
        return self._value

    def set_value(self, value):
        self._value = value
        self._value_changed = False

Now, let’s take advantage of Python and the uasync library and make the counter object waitable (how should this be translated into Hungarian? One that can be awaited?)

    def __await__(self):
        while not self._value_changed:
            yield from asyncio.sleep(0)

        return self.value()

    __iter__ = __await__  

This is a convenient function that waits until the counter value updates — it periodically wakes up and checks the _value_changed flag. The catch of this function is that the calling code can sleep on this function's call and remain asleep until a new value is received.

What about interrupts?Yes, at this point, you can troll me, saying I mentioned interrupts but actually set up a dumb pin polling. In reality, interrupts were the first thing I tried. On the ESP8266, you can set up an interrupt on the edge, and even write a handler for this interrupt in Python. In this interrupt, it’s possible to update the variable's value. Probably, that would be enough if the counter were a slave device — one that waits until someone asks for its value.

Sajnos (vagy szerencsére?) az eszközöm aktív, magának kellene üzeneteket küldenie MQTT protokollon és adatokat rögzítenie az EEPROM-ba. Itt azonban korlátokba ütközünk — a megszakítások alatt nem lehet memóriát foglalni és nagy veremméretet használni, ami azt jelenti, hogy a hálózati üzenetküldésről lemondhatunk. Vannak előnyök, mint például a micropython.schedule(), amelyek lehetővé teszik, hogy egy funkciót „ahogy éppen így” indítsunk el, de felmerül a kérdés, hogy „mi értelme van?”. Előfordulhat, hogy éppen egy üzenetet küldünk, amikor egy megszakítás közbeszól, és elrontja a változók értékeit. Vagy például, míg még nem írtuk bele a régit, a szerverről egy új számlálóérték érkezett. Összességében szinkronizációt kellene létrehozni, vagy másképp kibúvót találni.

Időnként megjelenik a RuntimeError: schedule stack full, és ki tudja, miért?

A közvetlen lekérdezés és az uasync esetében ez a helyzet valahogy szebben és megbízhatóbban alakul.

Az EEPROM-mal való munkát egy kis osztályba szerveztem.

class EEPROM():
    i2c_addr = const(80)

    def __init__(self, i2c):
        self.i2c = i2c
        self.i2c_buf = bytearray(4) # Elkerülni a buffer létrehozását/megsemmisítését minden híváskor


    def read(self, eeprom_addr):
        self.i2c.readfrom_mem_into(self.i2c_addr, eeprom_addr, self.i2c_buf, addrsize=16)
        return ustruct.unpack_from("<I", self.i2c_buf)[0]
        
    
    def write(self, eeprom_addr, value):
        ustruct.pack_into("<I", self.i2c_buf, 0, value)
        self.i2c.writeto_mem(self.i2c_addr, eeprom_addr, self.i2c_buf, addrsize=16)

Pythonban nehéz közvetlenül bájtokkal dolgozni, pedig éppen a bájtok íródnak a memóriába. Kényszerültem a különbségtételre az egész szám és a bájtok között az ustruct könyvtár használatával.

Annak érdekében, hogy ne kelljen állandóan I2C objektumot és a memóriacellák címét átadni, mindezt körbetekertem egy kis és kényelmes klasszá.

class EEPROMValue():
    def __init__(self, i2c, eeprom_addr):
        self._eeprom = EEPROM(i2c)
        self._eeprom_addr = eeprom_addr
        

    def read(self):
        return self._eeprom.read(self._eeprom_addr)


    def write(self, value):
        self._eeprom.write(self._eeprom_addr, value)

Az I2C objektum a következő paraméterekkel jön létre.

i2c = I2C(freq=400000, scl=Pin(5), sda=Pin(4))

Megérkeztünk a legérdekesebb részhez — a MQTT szerverrel való kommunikáció végrehajtásához. A protokoll megvalósítását nem kell elvégezni — az internet mélyén találtunk egy kész aszinkron megvalósítást.. Ezt fogjuk használni.

Minden érdekes összegyűjtve a CounterMQTTClient osztályban, amely az MQTTClient könyvtáron alapul. Kezdjük a perifériákkal.

#####################################
# Class handles both counters and sends their status to MQTT
#####################################
class CounterMQTTClient(MQTTClient):

    blue_led = Pin(2, Pin.OUT, value = 1)
    button = Pin(0, Pin.IN)

    hot_counter = Counter(12, EEPROMValue(i2c, EEPROM_ADDR_HOT_VALUE))
    cold_counter = Counter(13, EEPROMValue(i2c, EEPROM_ADDR_COLD_VALUE))

Itt létrejönnek és beállításra kerülnek a lámpák és gombok pinjei, valamint a hideg és meleg víz számláló objektumai.

A kezdeményezés nem annyira trivialis.

    def __init__(self):
        self.internet_outage = True
        self.internet_outages = 0
        self.internet_outage_start = ticks_ms()

        with open("config.txt") as config_file:
            config['ssid'] = config_file.readline().rstrip()
            config['wifi_pw'] = config_file.readline().rstrip()
            config['server'] = config_file.readline().rstrip()
            config['client_id'] = config_file.readline().rstrip()
            self._mqtt_cold_water_theme = config_file.readline().rstrip()
            self._mqtt_hot_water_theme = config_file.readline().rstrip()
            self._mqtt_debug_water_theme = config_file.readline().rstrip()

        config['subs_cb'] = self.mqtt_msg_handler
        config['wifi_coro'] = self.wifi_connection_handler
        config['connect_coro'] = self.mqtt_connection_handler
        config['clean'] = False
        config['clean_init'] = False
        super().__init__(config)

        loop = asyncio.get_event_loop()
        loop.create_task(self._heartbeat())
        loop.create_task(self._counter_coro(self.cold_counter, self._mqtt_cold_water_theme))
        loop.create_task(self._counter_coro(self.hot_counter, self._mqtt_hot_water_theme))
        loop.create_task(self._display_coro())

A mqtt_as könyvtár működési paramétereinek beállításához egy nagy szótárat használunk a különféle beállításokkal — config. A legtöbb alapértelmezett beállítás megfelelő számunkra, de sok beállítást egyértelműen meg kell határozni. Annak érdekében, hogy ne kelljen közvetlenül a kódba írni a beállításokat, őket egy text fájlban, a config.txt-ben tárolom. Ez lehetővé teszi a kód független módosítását a beállításoktól, valamint több azonos eszköz létrehozását eltérő paraméterekkel.

A kód utolsó blokkja több korutint indít el a rendszer különböző funkcióinak kiszolgálásához. Például itt van egy korutint, amely a számlálók kezelését végzi.

    async def _counter_coro(self, counter, topic):
        # Publikálja a kezdeti értéket
        value = counter.value()
        await self.publish(topic, str(value))

        # Publikálja minden új értéket
        while True:
            value = await counter
            await self.publish_msg(topic, str(value))

A korutin folyamatosan várja az új számlálóértéket, és amint az megérkezik, üzenetet küld az MQTT protokollon keresztül. Az első kódrészlet a kezdeti értéket küldi el, még akkor is, ha a víz nem folyik a számlálón.

Az MQTTClient alap osztály saját magát kezeli, önállóan kezdeményezi a csatlakozást WiFi-n keresztül, és újracsatlakozik, amikor a kapcsolat megszakad. A WiFi kapcsolat állapotának változása esetén a könyvtár értesít minket a wifi_connection_handler hívásával.

    async def wifi_connection_handler(self, state):
        self.internet_outage = not state
        if state:
            self.dprint('A WiFi működik.')
            duration = ticks_diff(ticks_ms(), self.internet_outage_start) // 1000
            await self.publish_debug_msg('ReconnectedAfter', duration)
        else:
            self.internet_outages += 1
            self.internet_outage_start = ticks_ms()
            self.dprint('A WiFi nem működik.')
            
        await asyncio.sleep(0)

A funkció őszintén átvette a példákból. Ebben az esetben az internetkimaradások (internet_outages) számát és időtartamát számolja. Amikor a kapcsolat helyreáll, a szerverre elküldi a leállás időpontját.

Egyébként az utolsó sleep csak arra szükséges, hogy a funkció aszinkron legyen — a könyvtárban await-on keresztül van meghívva, így csak olyan funkciók hívhatók, amelyekben van más await.

A WiFi kapcsolaton kívül a MQTT brokerrel (szerverrel) is kapcsolatot kell létesíteni. Ezt is a könyvtár végzi, nekünk pedig lehetőséget ad arra, hogy hasznos dolgot csináljunk, amikor a kapcsolat létrejött.

    async def mqtt_connection_handler(self, client):
        await client.subscribe(self._mqtt_cold_water_theme)
        await client.subscribe(self._mqtt_hot_water_theme)

Itt több üzenetre feliratkozunk — a szervernek most lehetősége van megadni a jelenlegi számlálók értékeit az arra vonatkozó üzenet elküldésével.

    def mqtt_msg_handler(self, topic, msg):
        topicstr = str(topic, 'utf8')
        self.dprint("Megkaptunk egy MQTT üzenetet, téma={}, üzenet={}".format(topicstr, msg))

        if topicstr == self._mqtt_cold_water_theme:
            self.cold_counter.set_value(int(msg))

        if topicstr == self._mqtt_hot_water_theme:
            self.hot_counter.set_value(int(msg))

Ez a funkció kezeli a beérkező üzeneteket. A téma (az üzenet neve) alapján frissül az egyik számláló értéke.

Néhány segédfunkció

    # Publish a message if WiFi and broker is up, else discard
    async def publish_msg(self, topic, msg):
        self.dprint("Publishing message on topic {}: {}".format(topic, msg))
        if not self.internet_outage:
            await self.publish(topic, msg)
        else:
            self.dprint("Message was not published - no internet connection")

Ez a funkció az üzenet küldésével foglalkozik, ha a kapcsolat létrejött. Ha nincs kapcsolat — az üzenetet figyelmen kívül hagyja.

Ez csupán egy kényelmes funkció, amely formázza és elküldi a hibakeresési üzeneteket.

    async def publish_debug_msg(self, subtopic, msg):
        await self.publish_msg("{}\/{}".format(self._mqtt_debug_water_theme, subtopic), str(msg))

Annyi szöveg, és még nem villogtattuk a LED-et. Íme.

    # Blink flash LED if WiFi down
    async def _heartbeat(self):
        while True:
            if self.internet_outage:
                self.blue_led(not self.blue_led()) # Fast blinking if no connection
                await asyncio.sleep_ms(200) 
            else:
                self.blue_led(0) # Rare blinking when connected
                await asyncio.sleep_ms(50)
                self.blue_led(1)
                await asyncio.sleep_ms(5000)

Két villogási módot terveztem. Ha megszakad a kapcsolat (vagy éppen létrejön), az eszköz gyorsan fog villogni. Ha a kapcsolat létrejött — az eszköz 5 másodpercenként villog. Szükség esetén itt más villogási módokat is megvalósíthatunk.

De a LED csak játék. Még a kijelzőre is rátérünk.

    async def _display_coro(self):
        display = SSD1306_I2C(128,32, i2c)
    
        while True:
            display.poweron()
            display.fill(0)
            display.text("COLD: {:.3f}".format(self.cold_counter.value() / 1000), 16, 4)
            display.text("HOT:  {:.3f}".format(self.hot_counter.value() / 1000), 16, 20)
            display.show()
            await asyncio.sleep(3)
            display.poweroff()

            while self.button():
                await asyncio.sleep_ms(20)

Ez az, amiről beszéltem – mennyire egyszerű és kényelmes a korutinokkal. Ez a kis függvény leírja az összes felhasználói interakciót. A korutina csak várakozik a gomb megnyomására, és bekapcsolja a kijelzőt 3 másodpercre. A kijelzőn a számlálók aktuális mérései jelennek meg.

Még pár apróság maradt. Itt van a függvény, amely mindezt (újra)indítja. A fő ciklus csak a különböző hibakeresési információk elküldésével foglalkozik percenként. Szóval, itt van, ahogy van – különösebb magyarázatot nem hiszem, hogy szükséges.

   async def main(self):
        while True:
            try:
                await self._connect_to_WiFi()
                await self._run_main_loop()
                    
            except Exception as e:
                self.dprint('Globális kommunikációs hiba: ', e)
                await asyncio.sleep(20)

    async def _connect_to_WiFi(self):
        self.dprint('Csatlakozás a WiFi-hez és MQTT-hez')
        sta_if = network.WLAN(network.STA_IF)
        sta_if.connect(config['ssid'], config['wifi_pw'])
        
        conn = False
        while not conn:
            await self.connect()
            conn = True

        self.dprint('Csatlakozva!')
        self.internet_outage = False

    async def _run_main_loop(self):
        # Végtelen ciklus
        mins = 0
        while True:
            gc.collect()  # A RAM statisztikákhoz.
            mem_free = gc.mem_free()
            mem_alloc = gc.mem_alloc()

            try:
                await self.publish_debug_msg("Algó", mins)
                await self.publish_debug_msg("Republikációk", self.REPUB_COUNT)
                await self.publish_debug_msg("Hibák", self.internet_outages)
                await self.publish_debug_msg("SzabadMemória", mem_free)
                await self.publish_debug_msg("FoglaltMemória", mem_alloc)
            except Exception as e:
                self.dprint("Hiba történt: ", e)
            mins += 1

            await asyncio.sleep(60)

Még pár beállítás és konstanta a teljes leírás érdekében.

#####################################
# Constants and configuration
#####################################


config['keepalive'] = 60
config['clean'] = False
config['will'] = ('/ESP/Wemos/Water/LastWill', 'Goodbye cruel world!', False, 0)

MQTTClient.DEBUG = True

EEPROM_ADDR_HOT_VALUE = const(0)
EEPROM_ADDR_COLD_VALUE = const(4)

Ez így indul.

client = CounterMQTTClient()
loop = asyncio.get_event_loop()
loop.run_until_complete(client.main())

Valami baj van a memóriámmal.

Tehát itt van az összes kód. A fájlokat az ampy segédprogrammal töltöttem fel – ez lehetővé teszi, hogy azokat a belső (az ESP-07-ben található) flash memóriára töltsük fel, és aztán a programból normál fájlként elérjük. Ide töltöttem fel az általam használt mqtt_as, uasyncio, ssd1306 és collections könyvtárakat (az mqtt_as-ben van használva).

Indítjuk és... MemoryError-t kapunk. Minél inkább próbáltam megérteni, hol szivárog a memória, annál korábban jött elő ez a hiba, ahogy egyre több hibakereső kinyomtatást helyeztem el. Egy rövid google keresés arra vezetett, hogy megértettem: a mikrokontrollerben összesen 30 kB memória van, míg 65 kB kód (könyvtárakkal együtt) egyszerűen nem fér el benne.

De van kiút. Kiderült, hogy a micropython nem hajtja végre a kódot közvetlenül a .py fájlból - ez a fájl először le van fordítva. Ráadásul a fordítás közvetlenül a mikrokontrolleren történik, és bájtkódra alakul, amelyet később a memóriában tárolnak. A fordító működéséhez pedig bizonyos mennyiségű RAM is szükséges.

A trükk abban rejlik, hogy megszabadítsuk a mikrokontrollert az erőforrásigényes fordítástól. A fájlokat egy nagyobb számítógépen lehet lefordítani, és a kész bájtkódot tölthetik fel a mikrokontrollerre. Ehhez le kell tölteni a micropython firmware-t és össze kell állítani. az mpy-cross segédprogramot..

Nem írtam Makefile-t, hanem manuálisan végigjártam és lefordítottam az összes szükséges fájlt (beleértve a könyvtárakat) nagyjából így:

mpy-cross water_counter.py

Csak a .mpy kiterjesztésű fájlokat kell feltölteni, nem felejtve el előtte törölni a megfelelő .py fájlokat az eszköz fájlrendszeréből.

A fejlesztést az ESPlorer programban (IDE?) végeztem. Ez lehetővé teszi, hogy feltöltsem a szkripteket a mikrokontrollerre, és azonnal végrehajtsam azokat. Az esetemben az összes logika és az összes objektum létrehozása a water_counter.py (.mpy) fájlban található. Ahhoz, hogy mindez automatikusan elinduljon a rendszerindításkor, szükség van egy main.py nevű fájlra is. Ennek pontosan .py formátumúnak kell lennie, nem pedig előfordított .mpy-nak. Íme a triviális tartalom:

import water_counter

Elindítjuk - minden működik. De a szabad memória ijesztően kevés - körülbelül 1 kB. Terveim vannak az eszköz funkcionalitásának bővítésére, és ez a kilobájt egyértelműen nem lesz elegendő. De kiderült, hogy erre az esetre is van megoldás.

A helyzet a következő: még ha a fájlok bájtkódra vannak fordítva és az belső fájlrendszeren található, valójában mindezek mégis a RAM-ba töltenek be és onnan hajtódnak végre. De kiderült, hogy a micropython képes a bájtkódot közvetlenül a flash memóriából végrehajtani, ehhez azonban be kell építeni közvetlenül a firmware-be. Ez nem nehéz, bár a netbookomon ez eltartott egy ideig (ott viszont Linux volt).

Az algoritmus a következő:

  • 🥇Geary 3.36 — E-mail kliens a GNOME környezethez | ProHoster ESP Open SDK. Ez a program összegyűjti a fordítót és a könyvtárakat az ESP8266 alatti programokhoz. A projekt főoldalán található utasítások szerint épül fel (én az STANDALONE=yes telepítést választottam).
  • Letöltés a micropython források.
  • A szükséges könyvtárakat a micropython mappájában a ports/esp8266/modules könyvtárba kell elhelyezni.
  • A firmware-t az utasítások szerint állítsuk össze a fájlban. ports/esp8266/README.md
  • Firmware feltöltése a mikrokontrollerre (én ezt Windowson az ESP8266Flasher vagy a Python-alapú esptool programokkal csinálom)

Most már az 'import ssd1306' közvetlenül a firmware-ből fogja felhúzni a kódot, így a RAM-ból nem fog elfoglalni helyet. Ezzel a trükkel csak a könyvtárak kódját töltöttem fel a firmware-be, míg a program fő kódja a fájlrendszerből fut. Ez lehetővé teszi számomra, hogy könnyen módosítsam a programot a firmware újra kompillálása nélkül. Jelenleg körülbelül 8,5 kB RAM van szabadon. Ez a jövőben még sok különböző hasznos funkció megvalósítását lehetővé teszi. Hát, ha a memória mégis nagyon kevés lenne, a fő programot is elhelyezhetem a firmware-ben.

És most mit kezdjek ezzel?

Oké, a hardver össze van forrasztva, a firmware fel van írva, a ház kinyomtatva, az eszköz a falra van rögzítve és boldogan villog a lámpája. De eddig ez egy fekete doboz (szó szerint és átvitt értelemben) és nem sok haszna van. Ideje foglalkozni az MQTT üzenetekkel, amelyeket a szerverre küldenek.

Az én "okos otthonom" a következőn fut a Majordomo rendszerén. Az MQTT modul vagy benne van a dobozban, vagy könnyen telepíthető a kiegészítők boltjából — már nem emlékszem, honnan jutott hozzám. Az MQTT nem önálló dolog — szükség van egy ún. brokerre — egy szerverre, amely fogadja, válogatja és továbbítja az MQTT üzeneteket a klienseknek. Én a mosquitto-t használom, amely (mint a majordomo) ugyanazon a netbookon fut.

Miután az eszköz legalább egyszer elküldte az üzenetet, azonnal megjelenik a listában.

Csatlakoztatjuk a vízmérőt az okos házhoz

Ezeket az értékeket most összekapcsolhatjuk a rendszer objektumaival, használhatjuk az automatizálási forgatókönyvekben, és különböző elemzéseknek alávethetjük őket — mindez a cikk terjedelmén kívül esik. Akinek érdekes a Majordomo rendszer, tudom ajánlani a Műszaki Csatornát — a barátom is okos házat épít és érthetően beszél a rendszer beállításáról.

Csak néhány grafikont mutatok. Ez egy egyszerű grafikon a napi értékekről

Csatlakoztatjuk a vízmérőt az okos házhoz
Látszik, hogy éjszaka szinte senki nem használta a vizet. Párszor valaki elment a WC-re, és úgy tűnik, a fordított ozmózis szűrő éjszaka pár liter vizet felhasznál. Reggel a fogyasztás jelentősen megnő. Általában a bojlerből használom a vizet, de most fürdeni akartam, és ideiglenesen átváltottam a városi meleg vízre — ez szintén jól látható az alsó grafikonon.

A grafikonból megtudtam, hogy a WC-re menni 6-7 liter vizet jelent, a zuhanyzás 20-30 liter, az edények elmosogatása körülbelül 20 liter, míg a fürdőhöz 160 liter szükséges. A családom napi vízfogyasztása körülbelül 500-600 liter.

A különösen kíváncsiak számára belenézhetnek a különböző értékek feljegyzéseibe.

Csatlakoztatjuk a vízmérőt az okos házhoz

Ebből azt is megtudtam, hogy nyitott csapnál a víz körülbelül 1 liter sebességgel folyik 5 másodperc alatt.

De így talán nem túl kényelmes nézni a statisztikákat. A Majordomo további lehetőségeket kínál a vízfogyasztás grafikonjának megtekintésére napok, hetek és hónapok szerint. Például itt van a vízfogyasztás oszlopdiagramja.

Csatlakoztatjuk a vízmérőt az okos házhoz

Jelenleg csak egy hét adatom van. Egy hónap múlva ez a grafikon sokkal beszédesebb lesz — minden nap egy külön oszlopot fog képviselni. Kicsit rontja a képet az az értékbeállítás, amit kézzel viszek be (a legnagyobb oszlop). És még nem világos, hogy vagy én állítottam be a legelső értékeket majdnem egy köbméterrel alacsonyabbra, vagy ez egy hiba a firmware-ben, és nem minden liter ment a számlálásba. Több időre van szükségem.

Még a grafikonokon is kell dolgozni, kicsit fehéríteni és festeni. Lehet, hogy tesztelési célból memóriafogyasztási grafikont is készítek — hátha ott is szivárog valami. Lehet, hogy valahogy meg fogom jeleníteni az internetkimaradások periódusait. Jelenleg minden ez egy ötlet szintjén van.

Zárás

Ma a lakásom egy kicsit okosabb lett. Ezzel a kis eszközzel könnyebben nyomon tudom követni a vízfogyasztást a házban. Ha korábban dühöngtem, hogy "megint sok vizet fogyasztottunk ebben a hónapban", most már meg tudom találni a fogyasztás forrását.

Valakinek furcsa lehet a kijelzőn nézni a méréseket, ha éppen egy méterre van a számlálótól. De nem túl távoli jövőben tervezem, hogy egy másik lakásba költözöm, ahol több vízvezeték is lesz, és a számlálók valószínűleg a lépcsőházban lesznek elhelyezve. Így a távoli mérések elvételére szolgáló eszköz nagyon hasznos lesz.

A készülék funkcióit is szeretném bővíteni. Már nézegetem a motoros szelepeket. Jelenleg a bojler-városi víz váltásához 3 csapot kell elforgatom egy nehezen hozzáférhető helyen. Sokkal kényelmesebb lenne ezt egy gombnyomással végezni, a megfelelő jelzésekkel. És természetesen érdemes lenne megvalósítani a szivárgásvédelmet.

A cikkben bemutattam az általam tervezett rendszert ESP8266 alapokon. Véleményem szerint egy nagyon érdekes megoldást sikerült létrehoznom Micropython firmware-rel, coroutine-okkal — egyszerű és esztétikus. Megpróbáltam sok részletet és hibát leírni, amikkel a folyamat során találkoztam. Lehet, hogy túl részletesen írtam mindent, de nekem, mint olvasónak, könnyebb átlapozni a felesleges részeket, mint később kitalálni azt, amit nem mondtak el.

Mint mindig, nyitott vagyok a konstruktív kritikára.

Forráskód
Schéma és nyomtatott áramkör
Háztestenő modell

Forrás: habr.com

Megbízható hosting vásárlása weboldalak számára, DDoS védelemmel, VPS VDS szerverek. 🔥 Megbízható webtárhely vásárlása DDoS védelemmel, VPS VDS szerverek | ProHoster