Vodoměr napojíme na chytrou domácnost

Kdysi byly systémy domácí automatizace, nebo jak se často říká „chytrá domácnost“, strašně drahé a mohli si je dovolit jen bohatí. Dnes na trhu najdete poměrně levné sady se senzory, tlačítky / spínači a akčními členy pro ovládání osvětlení, zásuvek, ventilace, zásobování vodou a dalších spotřebitelů. A i ten nejpokřivenější kutil se může připojit ke kráse a sestavit zařízení pro chytrý dům za nízkou cenu.

Vodoměr napojíme na chytrou domácnost

Zpravidla jsou navrhovanými zařízeními buď snímače nebo akční členy. Usnadňují implementaci scénářů jako „když se spustí pohybový senzor, rozsviťte světlo“ nebo „vypínač u východu zhasne světlo v celém bytě“. S telemetrií to ale nějak nevyšlo. V nejlepším případě jde o graf teploty a vlhkosti nebo okamžitého výkonu v konkrétní zásuvce.

Nedávno jsem instaloval vodoměry s pulzním výstupem. Prostřednictvím každého litru, který prošel počítadlem, se aktivuje jazýčkový spínač a sepne kontakt. Jediné, co zbývá udělat, je držet se drátů a pokusit se z toho získat nějaký užitek. Například analyzujte spotřebu vody podle hodin a dnů v týdnu. Pokud je v bytě několik stoupaček na vodu, pak je pohodlnější vidět všechny aktuální indikátory na jedné obrazovce, než lézt do těžko dostupných výklenků s baterkou.

Pod řezem je moje verze zařízení založeného na ESP8266, které počítá pulsy z vodoměrů a odesílá hodnoty na server chytré domácnosti přes MQTT. Budeme programovat v micropythonu pomocí knihovny uasyncio. Při tvorbě firmwaru jsem narazil na několik zajímavých obtíží, kterým se budu v tomto článku také věnovat. Jít!

systém

Vodoměr napojíme na chytrou domácnost

Srdcem celého obvodu je modul na mikrokontroléru ESP8266. Původně byl plánován ESP-12, ale ten můj se ukázal být vadný. Musel jsem se spokojit s modulem ESP-07, který byl k dispozici. Naštěstí jsou stejné jak závěry, tak funkčností, rozdíl je pouze v anténě - ESP-12 ji má zabudovanou, zatímco ESP-07 externí. Nicméně i bez WiFi antény se mi signál v koupelně chytá normálně.

Vazba modulu je standardní:

  • resetovací tlačítko s tahem a kondenzátorem (i když oba jsou již uvnitř modulu)
  • Povolovací signál (CH_PD) je připojen k napájení
  • GPIO15 přitažen k zemi. To je potřeba pouze na začátku, ale stále se nemusím držet této nohy

Chcete-li modul přepnout do režimu firmwaru, musíte GPIO2 zavřít do země a aby to bylo pohodlnější, poskytl jsem tlačítko Boot. V normálním stavu je tento kolík přitažen k napájení.

Stav linky GPIO2 je kontrolován pouze na začátku provozu - při připojení napájení nebo ihned po resetu. Modul se tedy buď nabootuje jako obvykle, nebo přejde do režimu firmwaru. Po načtení lze tento pin použít jako běžný GPIO. No, protože už tam je tlačítko, můžete na něj pověsit nějakou užitečnou funkci.

Pro programování a ladění použiji UART, který jsem si přinesl na hřeben. Když je potřeba, jednoduše tam připojím USB-UART adaptér. Jen je potřeba pamatovat na to, že modul je napájen 3.3V. Pokud zapomenete přepnout adaptér na toto napětí a připojíte 5V, pak se modul s největší pravděpodobností spálí.

S elektřinou v koupelně problémy nemám - zásuvka se nachází cca metr od měřičů, takže ji budu napájet z 220V. Jako zdroj energie budu mít malý blok HLK-PM03 od Tenstar Robot. Osobně to mám s analogovou a výkonovou elektronikou těžké a tady je hotový zdroj v malém pouzdře.

Pro signalizaci provozních režimů jsem poskytl LED připojenou k GPIO2. Nepájel jsem to však, protože. modul ESP-07 již má LED připojenou ke stejnému GPIO2. Ale budiž na desce - najednou chci tuto LED přivést do pouzdra.

Pojďme k tomu nejzajímavějšímu. Vodoměry nemají logiku, nelze po nich chtít aktuální odečty. Jediné, co máme k dispozici, jsou impulsy - sepnutí kontaktů jazýčkového spínače každý litr. Mám jazýčkové výstupy v GPIO12 / GPIO13. Uvnitř modulu programově zapnu pull-up rezistor.

Zpočátku jsem zapomněl dodat rezistory R8 a R9 a v mé verzi desky nejsou. Ale protože už vytyčuji schéma, aby ho všichni viděli, stojí za to toto nedopatření napravit. Rezistory jsou potřeba, aby se nespálil port, pokud je firmware zabugovaný a nastavuje jednotku na pinu a jazýčkový spínač tuto linku zkratuje na zem (rezistorem poteče max. 3.3V / 1000Ω = 3.3mA).

Je čas přemýšlet, co dělat, když vypadne elektřina. První možností je požádat server o počáteční hodnoty čítačů na začátku. To by ale vyžadovalo značnou komplikaci výměnného protokolu. Navíc výkon zařízení v tomto případě závisí na stavu serveru. Pokud by se po zhasnutí kontrolky server nespustil (nebo se spustil později), pak by si vodoměr nemohl vyžádat počáteční hodnoty a nefungoval by správně.

Proto jsem se rozhodl implementovat ukládání hodnot čítače do paměťového čipu připojeného přes I2C. Nemám žádné speciální požadavky na velikost flash paměti - stačí uložit pouze 2 čísla (počet litrů podle vodoměrů na teplou a studenou vodu). Postačí i ten nejmenší modul. Musíte si ale dát pozor na počet cyklů zápisu. U většiny modulů je to 100 tisíc cyklů, u některých až milion.

Zdálo by se, že milion je hodně. Ale za 4 roky života ve svém bytě jsem spotřeboval o něco více než 500 metrů krychlových vody, to je 500 tisíc litrů! A 500 tisíc záznamů ve flashi. A to je jen studená voda. Čip můžete samozřejmě každých pár let znovu připájet, ale ukázalo se, že existují čipy FRAM. Z programátorského hlediska se jedná o stejnou I2C EEPROM, jen s velmi velkým počtem přepisovacích cyklů (stovky milionů). To jen do té doby, než se stále nemohu dostat do obchodu s takovými mikroobvody, takže zatím obstojí obvyklé 24LC512.

Tištěný spoj

Původně jsem plánoval, že si desku vyrobím doma. Proto byla deska navržena jako jednostranná. Ale po hodině strávené s laserovou žehličkou a pájecí maskou (bez ní to nějak nejde), jsem se přesto rozhodl objednat desky od Číňanů.

Vodoměr napojíme na chytrou domácnost

Téměř před objednáním desky jsem si uvědomil, že kromě flash paměťového čipu lze na I2C sběrnici připojit ještě něco užitečného, ​​například displej. Co přesně do něj vyvést je zatím otázkou, ale je potřeba to na desce rozmnožit. No a jelikož jsem se chystal desky objednávat z výroby, nemělo smysl se omezovat na jednostrannou desku, takže I2C linky jsou jediné na zadní straně desky.

S jednosměrnou elektroinstalací byla spojena i jedna velká zárubeň. Protože deska byla nakreslena jednostranně, poté bylo plánováno umístění kolejí a SMD součástek na jednu stranu a výstupní součástky, konektory a napájení na druhou. Když jsem o měsíc později dostal desky, zapomněl jsem na původní plán a připájel všechny součástky na přední straně. A teprve když došlo na pájení napájecího zdroje, ukázalo se, že plus a mínus byly rozvedeny naopak. Musel jsem hospodařit se skokany. Na obrázku výše jsem již měnil kabeláž, ale zem se přenáší z jedné části desky na druhou přes piny tlačítka Boot (i když na druhou vrstvu by šlo nakreslit stopu).

Dopadlo to takto

Vodoměr napojíme na chytrou domácnost

Корпус

Dalším krokem je tělo. Pokud máte 3D tiskárnu, není to problém. Moc jsem se neobtěžoval - jen jsem nakreslil krabici správné velikosti a udělal výřezy na správných místech. Kryt je připevněn k tělu pomocí malých samořezných šroubů.

Vodoměr napojíme na chytrou domácnost

Již jsem zmínil, že tlačítko Boot lze použít jako tlačítko pro všeobecné použití – pojďme ho tedy přenést na přední panel. K tomu jsem nakreslil speciální „studnu“, kde tlačítko žije.

Vodoměr napojíme na chytrou domácnost

Uvnitř skříně jsou také pahýly, na které je deska instalována a upevněna jedním šroubem M3 (na desce už nebylo místo)

Displej byl vybrán již při tisku první montážní verze pouzdra. Běžná dvouřádková tiskárna se do tohoto pouzdra nevešla, ale ve spodní části sudu byl OLED displej SSD1306 128 × 32. Je malý, ale nedívám se na něj každý den - bude se kutálet.

Odhadujic tak a tak, jak z toho budou vedeny dráty, rozhodl jsem se nalepit displej doprostřed pouzdra. Ergonomie je samozřejmě pod soklem – tlačítko je nahoře, displej dole. Už jsem ale řekl, že nápad sešroubovat displej přišel příliš pozdě a byl jsem líný přepojovat desku, abych hýbal tlačítkem.

Sestavené zařízení. Zobrazovací modul je k soplíku přilepen horkým lepidlem

Vodoměr napojíme na chytrou domácnost

Vodoměr napojíme na chytrou domácnost

Konečný výsledek lze vidět na KDPV

Firmware

Přejděme k softwarové části. Pro taková malá řemesla velmi rád používám jazyk Python (mikropython) - kód je velmi kompaktní a srozumitelný. Naštěstí není potřeba sestupovat na úroveň registrů, aby se vymáčkly mikrosekundy – vše lze udělat z pythonu.

Zdá se, že vše je jednoduché, ale ne příliš - zařízení má několik nezávislých funkcí:

  • Uživatel klepne na tlačítko a podívá se na displej
  • Litry tikají a aktualizují hodnoty ve flash paměti
  • Modul monitoruje signál WiFi a v případě potřeby se znovu připojí
  • No, bez blikající žárovky to vůbec nejde

Nemůžete předpokládat, že jedna funkce nefungovala, pokud druhá z nějakého důvodu selže. Kaktusy jsem už jedl v jiných projektech a teď stále vidím závady jako „minul další litr, protože se v tu chvíli aktualizoval displej“ nebo „uživatel nemůže nic dělat, když se modul připojuje k WiFi“. Některé věci lze samozřejmě dělat přes přerušení, ale můžete narazit na omezení délky trvání, vnořování hovorů nebo neatomickou změnu proměnných. No, kód, který dělá všechno a hned se rychle změní v nepořádek.

В serióznější projekt Použil jsem klasický preemptivní multitasking a FreeRTOS, ale v tomto případě se model ukázal jako mnohem vhodnější corutiny a knihovny uasync . Navíc Pythonská implementace coroutin je prostě bomba - vše se dělá jednoduše a pohodlně pro programátora. Stačí napsat vlastní logiku, jen mi řekněte, kde můžete přepínat mezi vlákny.

Navrhuji studovat rozdíly mezi preemptivním a kompetitivním multitaskingem jako volitelný. Nyní pojďme konečně ke 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č je zpracováván instancí třídy Counter. Nejprve se z EEPROM (value_storage) odečte počáteční hodnota čítače - takto je realizována obnova po výpadku napájení.

Pin je inicializován vestavěným pull-upem k napájecímu zdroji: pokud je jazýčkový spínač sepnutý, je linka nulová, pokud je linka otevřená, je přitažena k napájecímu zdroji a ovladač načte jedničku.

Zde je také spuštěna samostatná úloha, která bude dotazovat pin. Každý čítač spustí svůj vlastní úkol. Tady je její kód

    """ Poll pin and advance value when another litre passed """
    async def _switchcheck(self):
        last_checked_pin_state = self._pin.value()  # Get initial state

        # Poll for a pin change
        while True:
            state = self._pin.value()
            if state != last_checked_pin_state:
                # State has changed: act on it now.
                last_checked_pin_state = state
                if state == 0:
                    self._another_litre_passed()

            # Ignore further state changes until switch has settled
            await asyncio.sleep_ms(Counter.debounce_ms)

Pro filtraci odskoků kontaktů je potřeba zpoždění 25 ms a zároveň reguluje, jak často se úloha probudí (zatímco tato úloha spí, ostatní úlohy fungují). Každých 25ms se funkce probudí, zkontroluje pin a pokud jsou kontakty jazýčkového spínače sepnuté, tak počítadlem prošel další litr a ten je potřeba zpracovat.

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

        self._value_storage.write(self._value)

Zpracování dalšího litru je triviální - počítadlo se jen zvyšuje. No, bylo by hezké zapsat novou hodnotu na USB flash disk.

Pro snadné použití jsou k dispozici "příslušenství".

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

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

No, teď použijme kouzla pythonu a knihovny uasync a udělejme objekt čítače čekáním (jak to mohu přeložit do ruštiny? Ten, který lze očekávat?)

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

        return self.value()

    __iter__ = __await__  

To je taková šikovná funkce, která čeká na aktualizaci hodnoty počítadla – funkce se čas od času probudí a zkontroluje příznak _value_changed. Trik této funkce je v tom, že volací kód může při volání této funkce usnout a spát, dokud není přijata nová hodnota.

Ale co přerušení?Ano, v tuto chvíli mě můžete trollovat a říkat, že on sám řekl o přerušení, ale ve skutečnosti zařídil hloupou anketu o pin. Přerušení jsou vlastně první věc, kterou jsem zkusil. V ESP8266 můžete uspořádat přerušení na přední straně a dokonce pro toto přerušení napsat obsluhu přerušení v pythonu. V tomto přerušení můžete aktualizovat hodnotu proměnné. Pravděpodobně by to stačilo, kdyby čítač byl slave zařízení - takové, které čeká, až bude požádáno o tuto hodnotu.

Bohužel (nebo naštěstí?) je moje zařízení aktivní, mělo by samo odesílat zprávy přes protokol MQTT a zapisovat data do EEPROM. A zde již přicházejí omezení – nemůžete alokovat paměť v přerušeních a používat velký zásobník, což znamená, že můžete zapomenout na odesílání zpráv přes síť. Existují buchty jako micropython.schedule (), které vám umožňují spustit nějakou funkci „co nejdříve a okamžitě“, ale vyvstává otázka „co to má smysl?“. Najednou právě teď posíláme nějakou zprávu a pak se vloupe přerušení a zkazí hodnoty proměnných. Nebo například ze serveru dorazila nová hodnota počítadla, zatímco starou jsme ještě nezaznamenali. Obecně je potřeba synchronizaci zablokovat nebo se dostat ven nějak jinak.

A čas od času RuntimeError: naplánujte pády plného zásobníku a kdo ví proč?

S explicitním dotazováním a uasync se to v tomto případě nějak ukazuje jako krásnější a spolehlivější.

V malé třídě jsem si dal práci s EEPROM

class EEPROM():
    i2c_addr = const(80)

    def __init__(self, i2c):
        self.i2c = i2c
        self.i2c_buf = bytearray(4) # Avoid creation/destruction of the buffer on each call


    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)

Přímo v pythonu je obtížné pracovat s byty a právě byty se zapisují do paměti. Musel jsem ohradit převod mezi celým číslem a bajty pomocí knihovny ustruct.

Aby se pokaždé nepřenášel I2C objekt a adresa paměťové buňky, zabalil jsem to všechno do malé a pohodlné klasiky

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)

S těmito parametry je vytvořen samotný I2C objekt

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

Přistupujeme k tomu nejzajímavějšímu - implementaci komunikace se serverem přes MQTT. Samotný protokol nemusíte implementovat - našel jsem ho na internetu hotová asynchronní implementace. Zde jej použijeme.

Vše nejzajímavější je shromážděno ve třídě CounterMQTTClient, která je založena na knihovně MQTTClient. Začněme periferií

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

Zde se vytvářejí a konfigurují žárovky a knoflíkové kolíky, stejně jako objekty vodoměrů na studenou a teplou vodu.

S inicializací není vše tak triviální

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

Pro nastavení parametrů knihovny mqtt_as se používá velký slovník různých nastavení - config. Většina výchozích nastavení nám funguje, ale spoustu nastavení je potřeba nastavit explicitně. Abych nepředepisoval nastavení přímo v kódu, ukládám je do textového souboru config.txt. To vám umožní změnit kód bez ohledu na nastavení a také nýtovat několik stejných zařízení s různými parametry.

Poslední blok kódu spustí několik korutin, které slouží různým systémovým funkcím. Zde je příklad korutinu, která obsluhuje přepážky

    async def _counter_coro(self, counter, topic):
        # Publish initial value
        value = counter.value()
        await self.publish(topic, str(value))

        # Publish each new value
        while True:
            value = await counter
            await self.publish_msg(topic, str(value))

Korutina ve smyčce čeká na novou hodnotu čítače a jakmile se objeví, odešle zprávu protokolem MQTT. První část kódu odešle počáteční hodnotu, i když počítadlem neprotéká žádná voda.

Základní třída MQTTClient se obsluhuje sama, iniciuje WiFi připojení a znovu se připojí, když se spojení ztratí. Při změně stavu WiFi připojení nás knihovna informuje voláním wifi_connection_handler

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

Funkce je poctivě vylízaná z ukázek. V tomto případě počítá počet výpadků (internet_outages) a jejich trvání. Po obnovení připojení se na server odešle doba nečinnosti.

Mimochodem, poslední spánek je potřeba pouze k tomu, aby se funkce stala asynchronní - v knihovně se volá přes wait a lze volat pouze funkce, v jejichž těle je další wait.

Kromě připojení k WiFi je potřeba také navázat spojení s MQTT brokerem (serverem). To také dělá knihovna a my máme možnost udělat něco užitečného, ​​když je navázáno spojení

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

Zde se přihlásíme k odběru několika zpráv - server má nyní možnost nastavit aktuální hodnoty počítadel odesláním příslušné zprávy.

    def mqtt_msg_handler(self, topic, msg):
        topicstr = str(topic, 'utf8')
        self.dprint("Received MQTT message 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))

Tato funkce zpracovává příchozí zprávy a v závislosti na tématu (název zprávy) se aktualizují hodnoty jednoho z počítadel

Pár pomocných funkcí

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

Tato funkce je zodpovědná za odeslání zprávy, pokud je navázáno spojení. Pokud není spojení, zpráva je ignorována.

A to je jen pohodlná funkce, která generuje a odesílá ladicí zprávy.

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

Tolik textu a ještě jsme neblikali LEDkou. Tady

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

Nabízel jsem 2 režimy blikání. Pokud se spojení ztratí (nebo se právě navazuje), zařízení bude rychle blikat. Pokud je spojení navázáno, zařízení každých 5 sekund bliká. V případě potřeby zde lze implementovat další režimy blikání.

Ale LED je taková, rozmazlující. Také jsme se rozhoupali u displeje.

    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)

To je to, o čem jsem mluvil - jak jednoduché a pohodlné je to s coroutines. Tato malá funkce popisuje VŠECHNY uživatelské interakce. Korutina jen čeká na zmáčknutí tlačítka a rozsvítí displej na 3 sekundy. Displej zobrazuje aktuální stav měřiče.

Ještě zbývá pár drobností. Zde je funkce, která (znovu) nastartuje celou tuto ekonomiku. Hlavní smyčka se zabývá pouze odesíláním různých ladicích informací jednou za minutu. Obecně to dávám tak, jak to je - myslím, že nemusím konkrétně komentovat

   async def main(self):
        while True:
            try:
                await self._connect_to_WiFi()
                await self._run_main_loop()
                    
            except Exception as e:
                self.dprint('Global communication failure: ', e)
                await asyncio.sleep(20)

    async def _connect_to_WiFi(self):
        self.dprint('Connecting to WiFi and 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('Connected!')
        self.internet_outage = False

    async def _run_main_loop(self):
        # Loop forever
        mins = 0
        while True:
            gc.collect()  # For RAM stats.
            mem_free = gc.mem_free()
            mem_alloc = gc.mem_alloc()

            try:
                await self.publish_debug_msg("Uptime", mins)
                await self.publish_debug_msg("Repubs", self.REPUB_COUNT)
                await self.publish_debug_msg("Outages", 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("Exception occurred: ", e)
            mins += 1

            await asyncio.sleep(60)

No, pár dalších nastavení a konstant pro úplnost popisu

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

Všechno to začíná takhle

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

Něco se mi stalo s pamětí

Takže tam je celý kód. Soubory jsem nahrál pomocí utility ampy - umožňuje je nahrát na interní (ten v samotném ESP-07) flash disk a pak k němu přistupovat z programu jako k normálním souborům. Tam jsem také nahrál knihovny mqtt_as, uasyncio, ssd1306 a kolekce, které jsem použil (používá se uvnitř mqtt_as).

Spustíme a... Obdržíme MemoryError. Navíc, čím více jsem se snažil pochopit, kde přesně paměť uniká, čím více jsem ladil výtisky, tím dříve k této chybě došlo. Krátké googlení mě přivedlo k pochopení, že v mikrokontroléru je v principu pouze 30 kb paměti, do které se 65 kb kódu (spolu s knihovnami) nijak nevejde.

Ale existuje cesta ven. Ukázalo se, že micropython nespouští kód přímo ze souboru .py – tento soubor je nejprve zkompilován. Navíc se zkompiluje přímo na mikrokontroléru, přemění se na bajtkód, který se pak uloží do paměti. Kompilátor také potřebuje ke své práci určité množství paměti RAM.

Trik je zachránit mikrokontrolér před kompilací náročnou na zdroje. Můžete kompilovat soubory na velkém počítači a nahrát hotový bajtkód do mikrokontroléru. Chcete-li to provést, musíte si stáhnout firmware micropythonu a sestavit nástroj mpy-cross.

Nepsal jsem Makefile, ale ručně prošel a zkompiloval všechny potřebné soubory (včetně knihoven) takto

mpy-cross water_counter.py

Zbývá pouze vyplnit soubory s příponou .mpy, přičemž nezapomeňte nejprve odstranit odpovídající soubory .py ze systému souborů zařízení.

Veškerý vývoj jsem dělal v programu (IDE?) ESPlorer. Umožňuje nahrát skripty do mikrokontroléru a okamžitě je spouštět. V mém případě je veškerá logika a tvorba všech objektů umístěna v souboru water_counter.py (.mpy). Ale aby se to vše spustilo automaticky při startu, musí existovat také soubor s názvem main.py. Navíc to musí být přesně .py a ne předem zkompilované .mpy. Zde je jeho triviální obsah

import water_counter

Začínáme - vše funguje. Volná paměť je ale hrozivě malá – asi 1kb. Stále mám v plánu rozšířit funkcionalitu zařízení a tento kilobajt mi evidentně stačit nebude. Ale ukázalo se, že existuje cesta ven.

Jde o to. I když jsou soubory kompilovány do bajtkódu a jsou umístěny v interním souborovém systému, jsou ve skutečnosti načteny do paměti RAM a stejně tak odtud spouštěny. Ukázalo se však, že micropython může spouštět bajtový kód přímo z paměti flash, ale k tomu jej musíte zabudovat přímo do firmwaru. Není to těžké, i když na mém netbooku to zabralo slušnou dobu (jen tam jsem měl Linux).

Algoritmus je následující:

  • Stáhněte a nainstalujte ESP Open SDK. Tato věc vytváří kompilátor a knihovny pro programy pod ESP8266. Sestavuje se podle návodu na hlavní stránce projektu (zvolil jsem nastavení STANDALONE=ano)
  • download mikropythonové druhy
  • Vložte potřebné knihovny do portů/esp8266/modulů uvnitř stromu micropython
  • Firmware shromažďujeme podle pokynů v souboru porty/esp8266/README.md
  • Nahrajte firmware do mikrokontroléru (provádím to ve Windows pomocí programů ESP8266Flasher nebo esptool Pythonu)

Všechno, nyní 'import ssd1306' vyvolá kód přímo z firmwaru a RAM se za to neutratí. Tímto trikem jsem do firmwaru nahrál pouze kód knihovny, zatímco hlavní programový kód se spouští ze souborového systému. To usnadňuje úpravu programu bez překompilování firmwaru. V tuto chvíli mám volných cca 8.5kb RAM. To nám v budoucnu umožní implementovat poměrně hodně různých užitečných funkcí. No, pokud vůbec není dostatek paměti, můžete hlavní program vložit do firmwaru.

A co s tím teď dělat?

Ok, kus železa je připájen, firmware napsán, krabice vytištěna, zařízení přilepené na zdi a kontrolka vesele bliká. Ale zatím je to všechno černá skříňka (doslova i obrazně) a stále z toho nedává smysl. Je čas něco udělat se zprávami MQTT, které se odesílají na server.

Můj „chytrý dům“ se točí dál Systém Majordomo. Modul MQTT je buď vybalený z krabice, nebo se snadno instaluje z trhu doplňků - nepamatuji si, odkud pochází. MQTT není soběstačná věc – potřebujete tzv. broker – server, který přijímá, třídí a předává zprávy MQTT klientům. Používám mosquitto, který (jako majordomo) běží na stejném netbooku.

Poté, co zařízení alespoň jednou odešle zprávu, hodnota se okamžitě objeví v seznamu.

Vodoměr napojíme na chytrou domácnost

Tyto hodnoty mohou být nyní spojeny se systémovými objekty, mohou být použity v automatizačních skriptech a podrobeny různým analýzám - to vše je mimo rozsah tohoto článku. Koho zajímá systém majordomo, mohu doporučit Kanálová elektronika v objektivu - kamarád také staví chytrou domácnost a srozumitelně mluví o nastavení systému.

Ukážu vám jen pár grafů. Toto je jednoduchý graf hodnot za den

Vodoměr napojíme na chytrou domácnost
Je vidět, že v noci vodu skoro nikdo nepoužíval. Několikrát někdo šel na záchod a vypadá to, že filtr reverzní osmózy nasaje pár litrů za noc. Ráno spotřeba výrazně stoupá. Obvykle používám vodu z bojleru, ale pak jsem se chtěl vykoupat a dočasně přešel na městskou teplou vodu - to je také dobře vidět na spodním grafu.

Z této tabulky jsem se dozvěděl, že chození na toaletu je 6-7 litrů vody, sprchování 20-30 litrů, mytí nádobí asi 20 litrů a koupání vyžaduje 160 litrů. Za den moje rodina spotřebuje někde kolem 500-600l.

Pro ty, kteří jsou obzvláště zvědaví, se můžete podívat do záznamů pro každou jednotlivou hodnotu.

Vodoměr napojíme na chytrou domácnost

Odtud jsem se dozvěděl, že při otevřeném kohoutku teče voda rychlostí cca 1 litr za 5 sekund.

Ale v této podobě se na statistiky asi moc nekouká. majordomo má také možnost zobrazit grafy spotřeby podle dne, týdne a měsíce. Zde je například graf spotřeby ve sloupcích

Vodoměr napojíme na chytrou domácnost

Zatím mám data jen jeden týden. Za měsíc bude tento graf více prozrazující – každému dni bude odpovídat samostatný sloupec. Obrázek mírně kazí úpravy hodnot, které zadávám ručně (největší sloupec). A ještě není jasné, zda jsem nesprávně nastavil úplně první hodnoty téměř o kostku méně, nebo zda se jedná o chybu ve firmwaru a nebyly zohledněny všechny litry. Potřebovat více času.

Nad samotnými grafy je potřeba ještě kouzlit, bělit, malovat. Snad sestavím i graf spotřeby paměti pro účely ladění - najednou tam něco prosakuje. Snad nějak zobrazím období, kdy nebyl internet. Zatímco to vše se točí na úrovni myšlenky.

Závěr

Dnes je můj byt o něco chytřejší. S takto malým zařízením mi přijde pohodlnější sledovat spotřebu vody v domě. Pokud jsem byl dříve rozhořčen „za měsíc bylo spotřebováno opět hodně vody“, nyní mohu najít zdroj této spotřeby.

Někomu se bude zdát divné dívat se na údaje na obrazovce, pokud je to metr od samotného měřiče. Ale v ne příliš vzdálené budoucnosti se plánuji přestěhovat do jiného bytu, kde bude několik stoupaček vody a samotné měřiče budou pravděpodobně umístěny na přistání. Takže zařízení na dálkové čtení by bylo velmi užitečné.

Plánuji také rozšíření funkčnosti zařízení. Už koukám na motorizované ventily. Nyní, abych přepnul kotel-městskou vodu, musím otočit 3 kohoutky v těžko dostupném výklenku. Mnohem pohodlnější by to bylo udělat jedním tlačítkem s odpovídající indikací. Samozřejmě stojí za to implementovat ochranu proti úniku.

V článku jsem řekl svou verzi zařízení založené na ESP8266. Podle mého názoru jsem získal velmi zajímavou verzi mikropythonového firmwaru pomocí coroutines - jednoduchý a pěkný. Snažil jsem se popsat mnoho nuancí a záseků, se kterými jsem se během kampaně setkal. Snad jsem vše popsal až příliš podrobně, pro mě osobně jako čtenáře je snazší nadbytek promrhat, než později vymýšlet, co zůstalo nevyřčeno.

Jako vždy jsem otevřený konstruktivní kritice.

Zdrojový kód
Schéma a deska
Model případu

Zdroj: www.habr.com

Přidat komentář