Pripojíme vodomer k inteligentnej domácnosti

Kedysi boli systémy domácej automatizácie, často nazývané "inteligentné domy", hrozne drahé a mohli si ich dovoliť len boháči. Dnes na trhu nájdete dostatočne cenovo dostupné sady so senzormi, tlačidlami/ vypínačmi a výkonnými zariadeniami na riadenie osvetlenia, zásuviek, vetrania, zásobovania vodou a ďalších spotrebiteľských zariadení. A dokonca aj ten najneobratnejší DIY nadšenec môže za prijateľnú cenu zhromaždiť zariadenia pre inteligentný dom.

Pripojíme vodomer k inteligentnej domácnosti

Zvyčajne ponúkané zariadenia sú buď senzory, alebo výkonné mechanizmy. Umožňujú jednoducho realizovať scénáre ako "pri aktivácii pohybového senzora zapnúť svetlo" alebo "vypínač pri výstupe zhasne svetlo v celom byte". Ale s telemetriou to nejak nefungovalo. V najlepšom prípade ide o graf teploty a vlhkosti, alebo okamžitý výkon v konkrétnej zásuvke.

Nedávno som si nainštaloval vodomery s impulzným výstupom. Pri každom litri prechádzajúcom cez vodomer sa aktivuje magnetický kontakt a uzatvára spoj. Zostáva iba pripojiť drôty a pokúsiť sa z toho získať užitočnosť. Napríklad analyzovať spotrebu vody podľa hodín a dní v týždni. No a ak je v byte niekoľko vodných stúp, je pohodlnejšie vidieť všetky aktuálne údaje na jednom displeji, než sa s baterkou prekopávať po ťažko dostupných zákutiach.

Pod článkom sa nachádza moja verzia zariadenia postaveného na ESP8266, ktoré počíta impulzy z vodomerov a odosiela údaje na server inteligentného domu prostredníctvom MQTT. Programovať budeme na micropython s použitím knižnice uasyncio. Pri vytváraní firmvéru som narazil na niekoľko zaujímavých ťažkostí, o ktorých sa tiež zmienim v tomto článku. Ideme na to!

Schéma

Pripojíme vodomer k inteligentnej domácnosti

Srdcom celej schémy je modul na mikrokontroléri ESP8266. Pôvodne sa plánoval ESP-12, ale môj bol vadný. Musel som sa uspokojiť s modulom ESP-07, ktorý bol k dispozícii. Našťastie sú si podobné čo sa týka pinov a funkcií, rozdiel je len v anténe — ESP-12 má integrovanú, zatiaľ čo ESP-07 má vonkajšiu. Napriek tomu sa aj bez antény WiFi signál v mojej kúpeľni chytá normálne.

Obvod modulu je štandardný:

  • resetovací tlačidlo s ťahaním a kondenzátorom (hoci to všetko už je vo vnútri modulu)
  • signál enable (CH_PD) je pripojený k napájaniu
  • GPIO15 je pripojený na zem. To je potrebné iba pri štarte, ale mne už nie je potrebné nič na tento pin pripojiť.

Na prechod modulu do režimu programovania je potrebné pripojiť GPIO2 na zem, a aby to bolo pohodlnejšie, naplánoval som tlačidlo Boot. V normálnom stave je tento pin pripojený k napájaniu.

Stav línie GPIO2 sa kontroluje iba na začiatku činnosti — pri zapnutí napájania alebo hneď po resetovaní. Tak sa modul buď nabootuje ako obvykle, alebo prejde do režimu programovania. Po nabootovaní tento výstup môžeme použiť ako obvyklý GPIO. A keďže už tam je tlačidlo, môžeme naň pripojiť nejakú užitočnú funkciu.

Na programovanie a ladenie budem používať UART, ktorý je pripojený na konektor. Keď je to potrebné, jednoducho tam pripojím USB-UART prevodník. Treba len nezabudnúť, že modul je napájaný z 3.3V. Ak zabudnem prepnúť prevodník na toto napätie a podám 5V, modul pravdepodobne prehrie.

S elektrinou v kúpeľni nemám problémy — zásuvka je zhruba meter od meračov, takže napájam z 220V. Ako zdroj napájania mi bude slúžiť malý blok HLK-PM03 od Tenstar Robot. Osobne mám problémy s analógovou a výkonovou elektronikou, a tu je hotový napájací blok v malom case.

Na signalizáciu pracovných režimov som naplánoval LED diódu pripojenú k GPIO2. Napriek tomu som ju neprepojil, pretože v module ESP-07 už je LED dióda, a to pripojená k rovnakému GPIO2. Ale na doske nech bude — možno budem chcieť vytiahnuť túto LED diódu na skriňu.

Prechádzame k tomu najzaujímavejšiemu. Merače vody nemajú žiadnu logiku, nemôžeme sa opýtať na aktuálne hodnoty. Jediné, čo máme k dispozícii, sú impulzy — kontakt hermetického relé sa uzatvára na každý liter. Vývody hermetických relé mám pripojené ku GPIO12/GPIO13. Pull-up rezistor budem zapínať programovo v module.

Pôvodne som zabudol predpokladať rezistory R8 a R9 a v mojej verzii dosky ich nemám. Ale keď už zverejňujem schému na všeobecné posúdenie, stojí za to opraviť túto chybu. Rezistory sú potrebné, aby nedošlo k spáleniu portu, ak sa firmware pokazí a nastaví vysokú úroveň na pine, a hermetické relé tento vodič skratí na zem (s rezistorom potečie maximálne 3.3V/1000Ω = 3.3mA).

Je čas premýšľať, čo robiť, ak prúdenie elektriny prestane. Prvou možnosťou je na začiatku požiadať server o počiatočné hodnoty čítačov. To by však znamenalo významné zloženie výmenného protokolu. Navyše, funkčnosť zariadenia v takom prípade závisí od stavu servera. Ak by server po výpadku elektriny nenastartoval (alebo nastartoval neskôr), čítač vody by nemohol získať počiatočné hodnoty a nefungoval by správne.

Preto som sa rozhodol realizovať uchovanie hodnôt čítačov v pamäťovom čipe pripojenom cez I2C. Nemám špeciálne požiadavky na veľkosť flash pamäte — potrebujem uložiť len 2 čísla (počet litrov podľa čítačov teplej a studenej vody). Aj ten najmenší modul by bol vhodný. Ale na počet cyklov zápisu treba dávať pozor. Väčšina modulov má 100 tisíc cyklov, niektoré až milión.

Na prvý pohľad je milión veľa. Ale za 4 roky života vo svojom byte som spotreboval niečo vyše 500 kubíkov vody, čo sú 500 tisíc litrov! A to je 500 tisíc zápisov do flash pamäte. A to je len studená voda. Môžete samozrejme prepojiť čip každé dva roky, ale objavili sa čipy FRAM. Z pohľadu programovania je to ten istý I2C EEPROM, len s ooooooohne veľkým počtom cyklov prepísania (stovky miliónov). Len som sa zatiaľ nemohol dostať do obchodu s takýmito čipmi, preto zatiaľ zostáva bežná 24LC512.

Tlačová doska

Pôvodne som plánoval vyrábať dosku doma. Preto bola doska navrhnutá ako jednostranná. Ale po tom, čo som sa s laserovou žehličkou a pájkovou maskou (bez nej to akosi nie je comme il faut) snažil viac ako hodinu, som sa rozhodoval objednať dosky od Číňanov.

Pripojíme vodomer k inteligentnej domácnosti

Takmer pred objednaním dosky som si uvedomil, že okrem pamäťového čipu na zbernici I2C môžem pripojiť aj niečo iné užitočné, napríklad displej. Čo presne naň zobraziť — to zatiaľ ostáva otázkou, ale na doske to treba vyviesť. A keďže som sa rozhodol objednať dosky v továrni, nemalo viac zmysel obmedzovať sa jednostrannou doskou, preto sú linky pre I2C jedinými na zadnej strane dosky.

S jednosmerným zapojením súvisel aj jeden veľký problém. Keďže doska bola navrhnutá ako jednosmerná, plánovalo sa umiestniť obvody a SMD komponenty na jednu stranu a vývodové komponenty, konektory a napájanie na druhú. Keď som po mesiaci dostal dosky, zabudol som na pôvodný plán a spájkoval som všetky komponenty na prednej strane. Až keď prišlo na spájkovanie napájacieho zdroja, zistil som, že plus a mínus sú zapojené naopak. Musel som improvizovať pomocou jumperov. Na obrázku vyššie som už zmenil zapojenie, ale zem sa prenáša z jednej časti dosky do druhej cez vývody tlačidla Boot (aj keď by sa dalo viesť trasu na druhej vrstve).

Vyzerá to takto

Pripojíme vodomer k inteligentnej domácnosti

Kryt

Ďalší krok — korpus. Pri prítomnosti 3D tlačiarne to nie je problém. Nemusel som sa s tým veľmi zaoberať — jednoducho som nakreslil krabicu požadovanej veľkosti a urobil výrezy na potrebných miestach. Kryt sa upevňuje na korpus malými skrutkami.

Pripojíme vodomer k inteligentnej domácnosti

Už som spomínal, že tlačidlo Boot môže byť použité ako univerzálne tlačidlo — tak ho aj vytiahneme na predný panel. Na tento účel som nakreslil špeciálny "priehlbina", kde sa tlačidlo nachádza.

Pripojíme vodomer k inteligentnej domácnosti

Vo vnútri korpusu sa tiež nachádzajú úchyty, na ktoré sa upevňuje doska a fixuje jediným skrutkou M3 (na doske viac miesta neostalo).

Displej som vyberal už keď som vytlačil prvý skúšobný variant korpusu. Štandardný dvojriadkový displej sa do tohto korpusu nezmestil, ale v zásobách som objavil OLED displej SSD1306 128×32. Trochu malý, ale na neho každý deň nekukám — stačí.

Keď som premýšľal, ako budú od neho vedené káble, rozhodol som sa prilepiť displej uprostred korpusu. Ergonomia je, samozrejme, nízka — tlačidlo hore, displej dole. Ale už som hovoril, že nápad pripevniť displej prišiel príliš neskoro a lenivosť ma prinútila neprepracovávať dosku, aby som presunul tlačidlo.

Jednotka kompletne zložená. Displejový modul je prilepený na teplovzdušné lepidlo.

Pripojíme vodomer k inteligentnej domácnosti

Pripojíme vodomer k inteligentnej domácnosti

Konečný výsledok môžete vidieť na KDPV.

Firmware

Prejdeme k programovej časti. Pre takéto malé projekty sa mi veľmi páči používať jazyk Python (micropython)- kód je veľmi kompaktný a zrozumiteľný. Našťastie tu nie je potrebné klesnúť na úroveň registrov s cieľom získať mikrosekundy — všetko sa dá urobiť z Pythona.

Zdá sa, že je to jednoduché, ale nie je to až také — v zariadení sú naplánované viaceré nezávislé funkcie:

  • Používateľ klikne na tlačidlo a pozrie sa na displej
  • Litre tikajú a aktualizujú hodnoty v pamäti flash
  • Modul sleduje signál WiFi a v prípade potreby sa prepojí znova
  • Bez blikajúcej kontrolky by to nebolo možné

Nesmie sa stať, že jedna funkcia prestane fungovať, ak z nejakého dôvodu druhá zavádza. Už som mal dosť kaktusov v iných projektoch a teraz jasne vidím chyby typu „preskočili sme ďalší liter, pretože sa práve aktualizoval displej“ alebo „používateľ nemôže nič robiť, kým sa modul nepripojí k WiFi“. Samozrejme, niektoré veci sa dajú spraviť cez prerušovania, ale môže sa prejaviť obmedzenie dĺžky, hĺbky volania alebo neatomárne zmeny premenných. A samozrejme, kód, ktorý sa zaoberá všetkým naraz, sa rýchlo mení na chaos.

V v projekte s väčšími ambíciami Používal som klasickú preemptívnu multitasking a FreeRTOS, ale v tomto prípade bola omnoho vhodnejšia kooperatívna multitasking (coroutines) a knižnica uasync . Navyše implementácia korutín v Pythone je skvelá — pre programátora je všetko jednoduché a pohodlné. Jednoducho píšte svoju logiku, stačí povedať, kde sa môže medzi vláknami prepínať.

Rozdiely medzi preemptívnym a kooperatívnym multitaskingom odporúčam študovať ako voliteľné. A teraz sa konečne prejdime k kódu.

#####################################
# 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

Každý čítač spracováva inštancia triedy Counter. Najprv sa z EEPROM (value_storage) načíta počiatočná hodnota čítača — tým sa realizuje zotavenie po výpadku napájania.

Pin sa inicializuje so zabudovaným ťahom na napájanie: ak je relé uzavreté — na linke je nula, ak je otvorené, linka sa ťahá na napájanie a ovládač číta jednotku.

Taktiež sa spúšťa samostatná úloha, ktorá bude vykonávať dotazovanie pinu. Každý čítač spúšťa svoju vlastnú úlohu. Tu je jej kód

    """ Dotazuj pin a zvýš hodnotu, keď prejde ďalší liter """\n    async def _switchcheck(self):\n        last_checked_pin_state = self._pin.value()  # Získať počiatočný stav\n\n        # Dotazovanie na zmenu pinu\n        while True:\n            state = self._pin.value()\n            if state != last_checked_pin_state:\n                # Stav sa zmenil: teraz na to reaguj.\n                last_checked_pin_state = state\n                if state == 0:\n                    self._another_litre_passed()\n\n            # Ignorovať ďalšie zmeny stavu, až kým sa spínač neusadí\n            await asyncio.sleep_ms(Counter.debounce_ms)

Zdržanie 25 ms je potrebné na filtrovanie chvenia kontaktov a zároveň reguluje, ako často sa úloha prebúdza (kým táto úloha spí, fungujú iné úlohy). Každých 25 ms sa funkcia prebúdza, kontroluje pin a ak sa kontakty zmenšovača uzavrú, znamená to, že cez početník prešiel ďalší liter a je potrebné to spracovať.

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

        self._value_storage.write(self._value)

Spracovanie ďalšieho litra je triviálne — jednoducho sa zvyšuje počítadlo. A nové hodnoty by bolo dobré zapísať na flash.

Pre pohodlie použitia sú k dispozícii „prístupníky“

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

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

A teraz využijeme krásy Pythona a knižnice uasync a urobíme objekt počítadla čakateľným (ako to preložiť do slovenčiny? To, ktorý sa dá očakávať?)

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

        return self.value()

    __iter__ = __await__  

Je to veľmi pohodlná funkcia, ktorá čaká, kým sa hodnota počítadla neaktualizuje — funkcia sa občas prebúdza a kontroluje príznak _value_changed. Zaujímavé na tejto funkcii je, že volajúci kód môže zaspať pri volaní tejto funkcie a spať, až kým nedostane novú hodnotu.

A čo prerušenia?Áno, v tomto mieste ma môžete trollovať, že som hovoril o prerušeniach a v skutočnosti som zorganizoval hlúpy dotaz pinu. V skutočnosti sú prerušenia prvé, čo som vyskúšal. V ESP8266 je možné usporiadať prerušenie na fronte a dokonca napísať obslužný program tohto prerušenia v Pythone. V tomto prerušení je možné aktualizovať hodnotu premennej. Pravdepodobne by to stačilo, keby bol počítadlo podriadeným zariadením — teda takým, ktoré čaká, kým sa ho niekto opýta na túto hodnotu.

Bohužiaľ (alebo k šťastiu?) moje zariadenie je aktívne, malo by samo posielať správy prostredníctvom protokolu MQTT a zapisovať dáta do EEPROM. A tu už prichádzajú obmedzenia — v prerušení sa nedá alokovať pamäť a používať veľký zásobník, takže na posielanie správ v sieti môžeme zabudnúť. Existujú výhody ako micropython.schedule(), ktoré umožňujú spustiť nejakú funkciu „hned, ako to pôjde“, ale vzniká otázka „a aký to má zmysel?“. Zrazu posielame nejakú správu a v tom prídete s prerušením, ktoré pokazí hodnoty premenných. Alebo, napríklad, zo servera prišla nová hodnota počítadla, kým sme ešte tú starú nedopísali. V konečnom dôsledku je potrebné vytvoriť synchronizáciu alebo sa nejako inak prispôsobiť.

A ešte raz za čas sa objaví RuntimeError: schedule stack full a kto vie, prečo?

S jasným dotazovaním a uasync to v tomto prípade vychádza nejak krajšie a spoľahlivejšie

Prácu s EEPROM som preniesol do malej triedy

class EEPROM():
    i2c_addr = const(80)

    def __init__(self, i2c):
        self.i2c = i2c
        self.i2c_buf = bytearray(4) # Vyhnite sa vytváraniu/zničeniu bufferu pri každom volaní


    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)

V Pythone je prácu s bajtmi dosť ťažké, a predsa sú to práve bajty, ktoré sa zapisujú do pamäte. Musel som vytvoriť konverziu medzi celým číslom a bajtmi pomocou knižnice ustruct.

Aby som každý raz nemusel prenášať objekt I2C a adresu pamäťovej bunky, zabalil som to všetko do malej a pohodlnej triedy

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)

Samotný objekt I2C sa vytvára s takými parametrami

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

Prichádzame k najzaujímavejšiemu — implementácii komunikácie so serverom cez MQTT. Protokol samostatne implementovať nie je potrebné — na internete sa našla hotová asynchrónna implementácia. Tú budeme používať.

Všetko najzaujímavejšie je zhromaždené v triede CounterMQTTClient, ktorá je postavená na knižničnom MQTTClient. Začneme s perifériou

#####################################
# 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))

Tu sa vytvárajú a nastavujú piny lampy a tlačidla, ako aj objekty počítadiel studenej a teplej vody.

S inicializáciou to nie je všetko také triviálne

    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())

Na nastavenie parametrov knižnice mqtt_as sa používa veľký slovník rôznych nastavení — config. Väčšina nastavení z predvolených je pre nás vhodná, ale mnohé nastavenia je potrebné zadať explicitne. Aby som nemusel zapisovať nastavenia priamo do kódu, uchovávam ich v textovom súbore config.txt. To umožňuje meniť kód nezávisle od nastavení, a taktiež vytvárať niekoľko rovnakých zariadení s rôznymi parametrami.

Posledný blok kódu spúšťa niekoľko korutín na obsluhu rôznych funkcií systému. Napríklad korutína, ktorá obsluhuje počítače

    async def _counter_coro(self, counter, topic):
        # Publikovať počiatočnú hodnotu
        value = counter.value()
        await self.publish(topic, str(value))

        # Publikovať každú novú hodnotu
        while True:
            value = await counter
            await self.publish_msg(topic, str(value))

Korutína čaká v cykle na nové hodnoty počítadla a hneď ako sa objaví — odošle správu cez protokol MQTT. Prvý kúsok kódu posiela počiatočnú hodnotu aj keď voda cez počítadlo netečie.

Základná trieda MQTTClient sa sama obsluhuje, sama inicjuje spojenie cez WiFi a prepojuje sa, keď spojenie vypadne. Pri zmenách stavu spojenia WiFi nás knižnica informuje volaním wifi_connection_handler

    async def wifi_connection_handler(self, state):
        self.internet_outage = not state
        if state:
            self.dprint('WiFi je online.')
            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('WiFi je offline.')
            
        await asyncio.sleep(0)

Funkcia je čestne prevzatá z príkladov. V tomto prípade počíta počet výpadkov (internet_outages) a ich trvanie. Pri obnovení spojenia sa na server odosiela čas neaktivity.

Mimochodom, posledný sleep slúži len na to, aby sa funkcia stala asynchrónnou — v knižnici sa volá cez await, a tak môžu byť volané len funkcie, v ktorých je iný await.

Okrem pripojenia k WiFi je potrebné tiež nadviazať spojenie s MQTT brokerom (serverom). Týmto sa tiež zaoberá knižnica, a nám zostáva priestor na vykonanie niečoho užitočného, keď je spojenie nadviazané.

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

Tu sa prihlasujeme na niekoľko správ — server teraz má možnosť zadať aktuálne hodnoty čítačov odoslaním príslušnej správy.

    def mqtt_msg_handler(self, topic, msg):
        topicstr = str(topic, 'utf8')
        self.dprint("Prijatá MQTT správa topic={}, msg={}".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))

Táto funkcia spracováva prijaté správy, a v závislosti od témy (názvu správy) sa aktualizujú hodnoty jedného z čítačov.

Pár pomocných funkcií.

    # 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")

Táto funkcia sa zaoberá odosielaním správy v prípade, že je spojenie nadviazané. Ak spojenie nie je, správa sa ignoruje.

A toto je jednoducho praktická funkcia, ktorá formuluje a odosiela ladacie správy.

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

Tak veľa textu, a ešte sme nezableskli LED diódou. Tu je.

    # 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)

Predpokladal som 2 režimy blikania. Ak spojenie zanikne (alebo sa len ustanovuje), zariadenie bude blikať rýchlo. Ak je spojenie nadviazané, zariadenie bliká raz za 5 sekúnd. Pri potrebe je tu možné implementovať aj iné režimy blikania.

Ale LED dióda je len taká zábavka. Veď sme sa ešte pustili do displeja.

    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)

Toto je presne to, o čom som hovoril – ako jednoduché a pohodlné je to s korutinami. Táto malá funkcia popisuje VŠETKY interakcie s používateľom. Korutina jednoducho čaká na stlačenie tlačidla a zapína displej na 3 sekundy. Na displeji sa zobrazujú aktuálne hodnoty počítadiel.

Ešte zostáva pár drobností. Tu je funkcia, ktorá to celé (re)spúšťa. Hlavný cyklus sa zaoberá iba odosielaním rôznych ladacích informácií raz za minútu. V podstate to uvádzam tak, ako to je — špeciálne komentovať, myslím, nie je potrebné.

   async def main(self):
        while True:
            try:
                await self._connect_to_WiFi()
                await self._run_main_loop()
                    
            except Exception as e:
                self.dprint('Globálna komunikačná chyba: ', e)
                await asyncio.sleep(20)

    async def _connect_to_WiFi(self):
        self.dprint('Pripojovanie na WiFi a MQTT')
        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('Pripojené!')
        self.internet_outage = False

    async def _run_main_loop(self):
        # Cyklus navždy
        mins = 0
        while True:
            gc.collect()  # Pre štatistiky RAM.
            mem_free = gc.mem_free()
            mem_alloc = gc.mem_alloc()

            try:
                await self.publish_debug_msg("Prevádzkový čas", mins)
                await self.publish_debug_msg("Repubs", self.REPUB_COUNT)
                await self.publish_debug_msg("Výpadky", self.internet_outages)
                await self.publish_debug_msg("MemFree", mem_free)
                await self.publish_debug_msg("MemAlloc", mem_alloc)
            except Exception as e:
                self.dprint("Nastala chyba: ", e)
            mins += 1

            await asyncio.sleep(60)

No, ešte pár nastavení a konštánt pre úplnosť opisu.

#####################################
# 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)

Toto sa spúšťa takto.

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

Niečo sa mi stalo s pamäťou.

Tak teda, celý kód je tu. Súbory som nahrával pomocou utilitky ampy — tá umožňuje nahrávať ich na internú (tú, ktorá je priamo v ESP-07) pamäť a potom k nim pristupovať z programu ako k bežným súborom. Tam som tiež nahrával knižnice mqtt_as, uasyncio, ssd1306 a collections (používané v mqtt_as).

Spúšťame a… Dostávame MemoryError. A čím viac som sa snažil pochopiť, kde presne dochádza k úniku pamäte, tým skôr sa táto chyba vyskytovala. Krátke googlenie ma priviedlo k pochopeniu, že v mikrokontroléri je vo všeobecnosti len 30 kB pamäte, do ktorej 65 kB kódu (vrátane knižníc) sa jednoducho nezmestí.

Ale existuje východisko. Ukazuje sa, že micropython nevykonáva kód priamo z .py súboru — tento súbor sa najprv kompiluje. A to priamo na mikrokontroléri, premení sa na bajtkód, ktorý sa potom uchováva v pamäti. A na fungovanie kompilátora je tiež potrebný určité množstvo operačnej pamäte.

Trik spočíva v tom, aby sme zbavili mikrokontrolér náročnej kompilácie. Môžeme skompilovať súbory na výkonnom počítači a do mikrokontroléra už nahrať hotový bajtkód. Na to je potrebné stiahnuť firmvér micropython a zostaviť nástroj mpy-cross.

Nezvolil som Makefile, ale ručne som prešiel a skompiloval všetky potrebné súbory (vrátane knižníc) približne takto

mpy-cross water_counter.py

Stačí len nahrať súbory s príponou .mpy, nezabudnúť predtým odstrániť zodpovedajúce .py zo súborového systému zariadenia.

Celú vývojovú prácu som vykonával v programe (IDE?) ESPlorer. Umožňuje nahrávať skripty do mikrokontroléra a okamžite ich vykonávať. V mojom prípade sa celá logika a vytváranie všetkých objektov nachádza vo súbore water_counter.py (.mpy). Aby sa to spúšťalo automaticky pri štarte, musí tu byť ešte súbor s názvom main.py. A ten musí byť presne vo formátu .py, a nie predkompilovaný .mpy. Tu je jeho triviálny obsah

import water_counter

Spúšťame — všetko funguje. Ale voľnej pamäte je znepokojujúco málo — okolo 1 kB. Mám ešte plány na rozšírenie funkčnosti zariadenia, a tohto kilobajtu mi jednoznačne bude málo. Ale na tento prípad sa tiež našlo riešenie.

Ide o to, že aj keď sú súbory skompilované do bajtkódu a nachádzajú sa na vnútornej súborovej sústave, v skutočnosti sa stále načítajú do operačnej pamäte a vykonávajú odtiaľ. Ukazuje sa však, že micropython dokáže vykonávať bajtkód priamo z flash pamäte, ale na to je potrebné integrovať ho priamo do firmvéru. Nie je to zložité, hoci na mojom netbooku to trvalo dosť dlho (len tam som mal linux).

Algoritmus je takýto:

  • Stiahnite a nainštalujte ESP Open SDKTento nástroj zostavuje kompilátor a knižnice pre programy pod ESP8266. Zostavuje sa podľa návodu na hlavnej stránke projektu (vybral som inštaláciu STANDALONE=yes)
  • Stiahnuť súbory micropython
  • Potrebné knižnice umiestnite do ports/esp8266/modules v rámci stromu micropython
  • Zostavujeme firmvér podľa pokynov v súbore ports/esp8266/README.md
  • Nahrávame firmware do mikrokontroléra (robím to na Windows pomocou programov ESP8266Flasher alebo Python esptool).

Teraz bude ‚import ssd1306‘ načítavať kód priamo z firmware, a nebude sa na to míňať operačná pamäť. Týmto spôsobom som do firmware nahrál iba kód knižníc, zatiaľ čo hlavný kód programu sa vykonáva z súborového systému. To umožňuje ľahko modifikovať program bez prekompilovania firmware. Momentálne mám voľných približne 8,5 kB RAM. To umožní implementovať ešte dosť rôznych užitočných funkcií v budúcnosti. Ak však pamäť úplne dôjde, môžem hlavný program tiež vložiť do firmware.

A čo s tým teraz robiť?

Tak, zariadenie je zmontované, firmware napísané, krabička vytlačená, zariadenie prilepené na stenu a šťastne bliká lampičkou. Ale zatiaľ je to stále čierna skrinka (v doslovnom aj obraznom zmysle) a zatiaľ málo z toho vyplýva. Je čas niečo urobiť s MQTT správami, ktoré sa posielajú na server.

Môj „inteligentný dom“ beží na systéme Majordomo.Modul MQTT buď existuje z krabice, alebo sa ľahko inštaluje z trhoviska — už si nepamätám, odkiaľ ho mám. MQTT nie je samostatná záležitosť — potrebujete tzv. broker — server, ktorý prijíma, triedi a preposiela klientom MQTT správy. Používam mosquitto, ktorý (rovnako ako majordomo) beží na tom istom netbooku.

Keď zariadenie aspoň raz pošle správu, hodnota sa okamžite objaví v zozname.

Pripojíme vodomer k inteligentnej domácnosti

Tieto hodnoty je teraz možné spojiť s objektmi systému, dajú sa použiť v scénároch automatizácie a podrobiť rôznej analýze — to všetko presahuje rámec tohto článku. Kto má záujem o systém Majordomo, môžem odporučiť kanál Elektronika v Objektíve. Druhá osoba tiež stavia inteligentný dom a zrozumiteľne vysvetľuje nastavenie systému.

Ukážem len pár grafov. Toto je jednoduchý graf hodnôt za 24 hodín.

Pripojíme vodomer k inteligentnej domácnosti
Je vidieť, že v noci takmer nikto nepoužíval vodu. Niekoľkokrát niekto šiel na toaletu, a zdá sa, že filter opačnej osmózy počas noci nasáva asi pár litrov. Ráno sa spotreba výrazne zvyšuje. Zvyčajne používam vodu z bojlera, ale tentoraz som chcel ísť do vane a dočasne som prepol na mestskú teplú vodu — to je tiež dobre vidieť na dolnom grafe.

Z tohto grafu som sa dozvedel, že ísť na toaletu je 6-7l vody, sprcha — 20-30l, umývanie riadu približne 20l, a aby som sa mohol okúpať, potrebujem 160l. Môj rodina denne spotrebuje okolo 500-600l.

Pre zvlášť zvedavých je možné nazrieť do záznamov každého jednotlivého hodnoty.

Pripojíme vodomer k inteligentnej domácnosti

Odtiaľ som sa dozvedel, že pri otvorenom kohútiku voda tečie rýchlosťou približne 1l za 5s.

Ale v tejto forme nie je pravdepodobne štatistika veľmi pohodlná na sledovanie. V majordomo sú aj ďalšie možnosti sledovať grafy spotreby za dni, týždne a mesiace. Tu je napríklad graf spotreby v stĺpcoch.

Pripojíme vodomer k inteligentnej domácnosti

Zatiaľ mám údaje iba za týždeň. O mesiac bude tento graf viac reprezentatívny — každému dňu bude zodpovedať samostatný stĺpec. Trochu to kazia úpravy hodnôt, ktoré zadávam manuálne (najväčší stĺpec). A zatiaľ nie je jasné, či som nesprávne zadal prvé hodnoty skoro o kubík menej, alebo je to bug vo firmvéri a nie všetky litre sú zahrnuté. Potrebujem viac času.

Ešte treba dolaďovať samotné grafy, vymaľovať, natierať. Možno tiež budem z debugovacích dôvodov zostavovať graf spotreby pamäte — čo ak tam niečo uniká. Možno nejako zobraziť obdobia, keď nebol internet. Zatiaľ je všetko iba na úrovni nápadu.

Záver

Dnes sa moja domácnosť stala o čosi inteligentnejšou. S takýmto malým zariadením bude pre mňa pohodlnejšie sledovať spotrebu vody v dome. Ak som predtým nadával na "opäť sme spotrebovali veľa vody za mesiac", teraz budem môcť nájsť zdroj tejto spotreby.

Niekomu sa môže zdať zvláštne sledovať údaje na obrazovke, keď je meter od samotného vodomeru. Ale v nie veľmi vzdialenej budúcnosti plánujem presťahovať sa do iného bytu, kde budú niekoľko stúpačiek vody, a samotné vodomery pravdepodobne budú umiestnené na schodisku. Takže zariadenie na diaľkové odčítanie hodnôt bude veľmi vhodné.

Funkcionalitu zariadenia plánujem tiež rozšíriť. Už sa dívam na motorické ventily. Momentálne pre prepínanie medzi bojlerom a mestskou vodou musím otáčať 3 kohúty v ťažko prístupnej niky. Bolo by oveľa pohodlnejšie robiť to jedným tlačidlom so zodpovedajúcou indikáciou. A samozrejme, ochranu proti únikom by bolo rozumné realizovať.

V článku som predstavil svoju verziu zariadenia založeného na ESP8266. Podľa môjho názoru sa mi podarilo vytvoriť zaujímavú verziu firmvéru na micropython s využitím korutín — jednoducho a atraktívne. Snažil som sa popísať množstvo nuáns a chýb, s ktorými som sa stretol po ceste. Možno som všetko popísal príliš detailne, pre mňa ako čitateľa je jednoduchšie preskočiť zbytočné, než potom domýšľať to, čo bolo nedopovedané.

Ako vždy som otvorený pre konštruktívnu kritiku.

Zdrojový kód
Schéma a doska
Model skrinky

Zdroj: habr.com

Kúpte si spoľahlivý hosting pre webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Kúpte si spoľahlivý hosting pre webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster