Kdysi byly systémy domácí automatizace, nebo jak se často nazývají „inteligentní domy“, strašně drahé a mohli si je dovolit jen boháči. Dnes na trhu najdete dostatečně levné soupravy se senzory, tlačítky/vypínači a akčními zařízeními pro ovládání osvětlení, zásuvek, ventilace, vodovodu a dalších spotřebičů. A dokonce i ten nejnešikovnější kutil se může zúčastnit tohoto úžasného zážitku a za nízkou cenu si sestavit zařízení pro inteligentní dům.

Obecně nabízena zařízení jsou buď senzory, nebo akční mechanismy. Umožňují snadno realizovat scénáře jako „při aktivaci pohybového senzoru se rozsvítí světlo“ nebo „vypínač u východu zhasne světlo v celém bytě“. Ale co se telemetrie týče, to už jde hůře. V lepším případě je to graf teploty a vlhkosti, nebo okamžitý výkon v konkrétní zásuvce.
Nedávno jsem si nainstaloval vodoměry s impulzním výstupem. Při každém litru, který prošel vodoměrem, se aktivuje hermetický spínač a uzavře kontakt. Zbývá malý úkol — připojit se k drátům a pokusit se z toho vytěžit užitek. Například analyzovat spotřebu vody podle hodin a dnů v týdnu. No a pokud má byt více vodovodních stojanů, je pohodlnější vidět všechny aktuální hodnoty na jednom displeji, než se plazit po těžko dostupných výklencích s baterkou.
Pod tímto odstavcem je můj návrh zařízení založeného na ESP8266, které počítá impulsy z vodoměrů a přes MQTT posílá údaje na server inteligentního domu. Budeme programovat na micropythonu s využitím knihovny uasyncio. Při vytváření firmwaru jsem narazil na několik zajímavých komplikací, o kterých se také zmíním v tomto článku. Pojďme na to!
Schéma

Srdcem celého schématu je modul na mikrokontroléru ESP8266. Původně byl plánován ESP-12, ale můj byl vadný. Musel jsem se spokojit s modulem ESP-07, který byl k dispozici. Naštěstí jsou si stejně a to jak vývody, tak funkcionalita, rozdíl je jen v anténě — u ESP-12 je vestavěná, zatímco u ESP-07 je externí. Nicméně i bez antény se WiFi signál v mé koupelně chytá normálně.
Obvod modulu je standardní:
- resetovací tlačítko s tahem a kondenzátorem (i když obojí již uvnitř modulu je)
- Signál enable (CH_PD) je tažen k napájení
- GPIO15 je připojen k zemi. To je potřeba pouze při spuštění, ale nemám už nic dalšího, co bych na tento pin připojil.
Pro přepnutí modulu do režimu programování je potřeba spojit GPIO2 se zemí, a aby to bylo pohodlnější, vytvořil jsem tlačítko Boot. V normálním stavu je tento pin připojen k napájení.
Stav linie GPIO2 se kontroluje pouze na začátku provozu — při zapnutí napájení nebo ihned po resetu. Modul se buď načte jako obvykle, nebo přejde do režimu programování. Po načtení lze tento vývod používat jako běžný GPIO. A když už tam je tlačítko, lze na něj přiřadit nějakou užitečnou funkci.
Pro programování a ladění budu používat UART, který jsem přivedl na konektor. Kdykoliv potřebuji, jednoduše připojím USB-UART převodník. Jen nesmím zapomenout, že modul je napájen 3.3V. Pokud zapomenu přepnout převodník na toto napětí a připojím 5V, pravděpodobně modul spálím.
S elektřinou v koupelně nemám problémy — zásuvka je umístěna zhruba metr od počítačů, takže ji napájím z 220V. Jako zdroj napájení budu mít malý od Tenstar Robot. Osobně to mám těžké s analogovou a výkonovou elektronikou, a tady je hotový napájecí blok v malém pouzdru.
Pro signalizaci pracovních režimů jsem naplánoval LED, připojenou k GPIO2. Nicméně jsem ji nenapojil, protože v modulu ESP-07 už je LED, připojená ke stejnému GPIO2. Ale na desce ať je — třeba budu chtít tuto LED vytáhnout na kryt.
Přecházíme k tomu nejzajímavějšímu. Vodoměry nemají žádnou logiku, nelze se ptát na aktuální hodnoty. Jediné, co máme k dispozici, jsou impulzy — sepnutí kontaktů hermetických spínačů na každý litr. Vývody hermetických spínačů mám přivedeny do GPIO12/GPIO13. Přitahovací rezistor zapnu programově uvnitř modulu.
Zpočátku jsem zapomněl zahrnout rezistory R8 a R9 a v mé variantě desky je nemám. Ale jelikož už zveřejňuji schéma pro veřejnost, stálo by za to tuto chybu opravit. Rezistory jsou potřeba, aby se nepoškodil port v případě, že programování selže a nastaví jedničku na pinu, zatímco hermetický spínač zkratuje tuto linku na zem (s rezistorem poteče maximálně 3.3V/1000Ohm = 3.3mA).
Je čas přemýšlet, co dělat, pokud vypadne elektrický proud. První možností je při spuštění požádat server o počáteční hodnoty čidel. To by však vyžadovalo podstatné komplikování komunikačního protokolu. Navíc by funkčnost zařízení v tomto případě závisela na stavu serveru. Pokud by server po výpadku elektrického proudu nenaběhl (nebo naběhl později), čidlo vody by nedokázalo požádat o počáteční hodnoty a fungovalo by nesprávně.
Rozhodl jsem se proto realizovat ukládání hodnot čidel do paměťového čipu připojeného pomocí I2C. Nemám zvláštní požadavky na velikost flash paměti - potřebuji uložit jen 2 čísla (množství litrů podle měřičů teplé a studené vody). I ten nejmenší modul by byl vhodný. Však si také musím dát pozor na počet zapisovacích cyklů. Většina modulů má 100 tisíc cyklů, některé až milion.
Na první pohled se může zdát, že milion je hodně. Ale za 4 roky bydlení v mé bytě jsem spotřeboval něco přes 500 kubíků vody, což činí 500 tisíc litrů! A to je 500 tisíc záznamů ve flash paměti. A to je jen studená voda. Mohu, samozřejmě, přepájívat čip každé dva roky, ale existují čipy FRAM. Z pohledu programování je to stejný I2C EEPROM, jen s oooohodně větším počtem přepisovacích cyklů (stovky milionů). Jen se zatím nemohu dostat do obchodu s těmito čipy, takže zatím bude pokračovat běžná 24LC512.
Tištěný obvod
Zpočátku jsem plánoval dělat desku doma. Proto byla deska navržena jako jednostranná. Ale po hodinovém trápení se s laserovým železem a pájecím lakem (bez něj to nějak není comme il faut), jsem se rozhodl objednat desky od Číňanů.

Téměř před objednáním desky jsem si uvědomil, že kromě flash paměťového čipu lze na sběrnici I2C připojit něco užitečného, například displej. Co přesně na něj zobrazit - to je ještě otázka, ale na desce je třeba vyrenderovat propojení. Takže když jsem se chystal objednat desky z továrny, nemělo smysl se omezovat na jednostrannou desku, takže linky na I2C jsou jediné na zadní straně desky.
S jednosměrným zapojením byl také spojen jeden velký problém. Protože deska byla navržena jednosměrně, plánovalo se umístit cesty a SMD komponenty na jednu stranu, zatímco vývodové komponenty, konektory a zdroj napájení na druhou. Když jsem po měsíci dostal desky, zapomněl jsem na původní plán a osadil všechny komponenty na přední stranu. A teprve když došlo na pájení zdroje, zjistil jsem, že plus a minus jsou rozděleny naopak. Musel jsem improvizovat s propojkami. Na obrázku výše jsem již změnil zapojení, ale země se přehazuje z jedné části desky do druhé přes vývody tlačítka Boot (i když by bylo možné udělat cestu na druhé vrstvě).
Výsledkem je tohle

Korpus
Další krok — korpus. Při dostupnosti 3D tiskárny to není problém. Moc jsem se tím nezabýval — jednoduše jsem nakreslil krabici potřebné velikosti a udělal zářezy na potřebných místech. Víko je upevněno k pouzdru pomocí malých samořezných šroubů.

Už jsem zmínil, že tlačítko Boot může být použito jako tlačítko pro obecné účely — tak ho vyvedeme na přední panel. K tomu jsem nakreslil speciální "zátku", kde žije tlačítko.

Uvnitř korpusu jsou také umístěny patky, na které je deska umístěna a upevněna jediným šroubem M3 (na desce nebylo více místa).
Displej jsem vybíral, když jsem vytiskl první zkušební variantu korpusu. Standardní dvouřádkový displej se do tohoto korpusu nevešel, ale v zásobách jsem objevil OLED displej SSD1306 128×32. Je malý, ale na něj se nedívám každý den — stačí.
Při zvažování, jak budou od něj vedeny dráty, jsem se rozhodl přilepit displej doprostřed korpusu. Ergonomie je sice velmi nízká — tlačítko nahoře, displej dole. Ale už jsem říkal, že nápad přidat displej přišel příliš pozdě a byla lenost znovu rozvrhnout desku, abych přesunul tlačítko.
Zařízení je sestaveno. Displejový modul je přilepen termolepidlem.


Konečný výsledek můžete vidět na KDPV.
Firmware
Přejdeme k softwarové části. Pro takové malé výrobky velmi rád používám jazyk Python ()- kód je velmi kompaktní a srozumitelný. Naštěstí zde není potřeba jít na úroveň registrů za účelem vytlačit mikrosekundy — všechno lze udělat z Pythonu.
Zdá se, že je to jednoduché, ale ve skutečnosti to není — v zařízení jsou naplánovány různé nezávislé funkce:
- Uživatel stiskne tlačítko a podívá se na displej
- Litr po litru tiká a aktualizuje hodnoty ve flash paměti
- Modul sleduje signál WiFi a v případě potřeby se připojuje znovu
- Bez blikající LED diody to vůbec nejde
Nelze připustit, aby jedna funkce nefungovala, pokud jiná z nějakého důvodu nepracuje. Už jsem se nabažil kaktusů v jiných projektech a teď už vidím chyby ve stylu 'zhubl další litr, protože se právě aktualizoval displej' nebo 'uživatel nemůže nic udělat, dokud se modul připojuje k WiFi'. Samozřejmě, některé věci lze dělat pomocí přerušení, ale lze narazit na omezení v délce, vnořování volání nebo neatomární změny proměnných. A kód, který se zabývá vším najednou, se rychle změní na chaos.
V používal jsem klasickou vypínací multitasking a FreeRTOS, ale v tomto případě byla mnohem vhodnější modelová . A přitom pythonovská realizace korutin je naprosto úžasná — pro programátora je vše uděláno jednoduše a pohodlně. Stačí napsat logiku, jen říct, v jakých místech se mezi vlákny lze přepínat.
Rozdíly mezi preemptivním a kooperativním multitaskingem doporučuji prostudovat jako volitelnou záležitost. A nyní se konečně pojďme dostat kód.
#####################################
# 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 foreverKaždý čítač je zpracováván instancí třídy Counter. Nejprve se z EEPROM (value_storage) načte počáteční hodnota čítače — tak se provádí obnova po výpadku napájení.
Pin je inicializován s vestavěným taháním k napájení: pokud je magnetický spínač uzavřen — na lince je nula, pokud je otevřen, linka se táhne k napájení a řídicí jednotka čte jedničku.
Také se zde spouští samostatný úkol, který bude realizovat dotazování pinu. Každý čítač spustí svůj vlastní úkol. Zde je jeho kód
""" Dotazuj pin a zvyšte hodnotu, když uplynul další litr """
async def _switchcheck(self):
last_checked_pin_state = self._pin.value() # Získejte počáteční stav
# Dotazujte na změnu pinu
while True:
state = self._pin.value()
if state != last_checked_pin_state:
# Stav se změnil: okamžitě na to reagujte.
last_checked_pin_state = state
if state == 0:
self._another_litre_passed()
# Ignorujte další změny stavu, dokud se přepínač neusadí
await asyncio.sleep_ms(Counter.debounce_ms)Zpoždění 25 ms je potřeba pro filtraci chvění kontaktů a zároveň reguluje, jak často se úloha probouzí (zatímco tato úloha spí, probíhají jiné úlohy). Každých 25 ms se funkce probouzí, kontroluje pin a pokud jsou kontakty геркона uzavřeny, znamená to, že přes čítač prošel další litr a to 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í — jednoduše se zvyšuje čítač. A nové hodnoty by se měly zapisovat na flash.
Pro snadné použití jsou k dispozici „přístupníky“.
def value(self):
self._value_changed = False
return self._value
def set_value(self, value):
self._value = value
self._value_changed = FalseA teď využijeme kouzlo Pythonu a knihovny uasync a uděláme z objektu čítače waitable (jak to přeložit do češtiny? Ten, na který můžeme čekat?)
def __await__(self):
while not self._value_changed:
yield from asyncio.sleep(0)
return self.value()
__iter__ = __await__ To je taková praktická funkce, která čeká, až se hodnota čítače aktualizuje — funkce se občas probudí a zkontroluje příznak _value_changed. Zajímavost této funkce je, že volající kód může usnout na výzvu této funkce a spát, dokud nedostane novou hodnotu.
A co přerušení?Ano, v tomto bodě mě můžete trollit, že jsem říkal o přerušeních, a ve skutečnosti jsem udělal hloupé dotazování pinu. Ve skutečnosti jsou přerušení první věcí, kterou jsem vyzkoušel. V ESP8266 lze uspořádat přerušení podle náběhu a dokonce napsat obslužnou rutinu tohoto přerušení v Pythonu. V tomto přerušení lze aktualizovat hodnotu proměnné. Asi by to stačilo, kdyby byl čítač vedlejším zařízením — takovým, které čeká, až se ho někdo zeptá na tuto hodnotu.
Bohužel (nebo naštěstí?) moje zařízení je aktivní, mělo by samo posílat zprávy pomocí protokolu MQTT a zapisovat data do EEPROM. A tady už nastupují omezení — v přerušeních nelze alokovat paměť a používat velkou zásobník, takže na zasílání zpráv po síti můžeme zapomenout. Existují funkce jako micropython.schedule(), které umožňují spustit nějakou funkci „jakmile to půjde“, ale vzniká otázka „a co z toho?“. Přeci jen, posíláme nějakou zprávu a tune se do toho vklíní přerušení, které pokazí hodnoty proměnných. Nebo třeba na serveru přišla nová hodnota čítače, zatímco jsme ještě nezapsali tu starou. V zásadě je potřeba vytvořit synchronizaci nebo to nějak řešit jinak.
A kromě toho občas vyhodí RuntimeError: schedule stack full a kdo ví, proč?
S explicitním dotazováním a uasync to v tomto případě vypadá nějak hezčeji a spolehlivěji.
Práci s EEPROM jsem přenesl do malé třídy.
class EEPROM():
i2c_addr = const(80)
def __init__(self, i2c):
self.i2c = i2c
self.i2c_buf = bytearray(4) # Zabráníme vytváření/ničení zásobníku při každém volání
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 Pythonu je obtížné pracovat přímo s byty, ale právě byty jsou zapisovány do paměti. Musel jsem vytvořit konverzi mezi celým číslem a byty s využitím knihovny ustruct.
Abych každou chvíli nepředával objekt I2C a adresu paměťové buňky, zabalil jsem to do malé a pohodlné třídy.
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 se vytváří s těmito parametry.
i2c = I2C(freq=400000, scl=Pin(5), sda=Pin(4))Dostáváme se k tomu nejzajímavějšímu — implementaci komunikace se serverem pomocí MQTT. Protokol není potřeba implementovat — na internetových stránkách se našlo . Toto budeme používat.
Všechno zajímavé je soustředěno v třídě CounterMQTTClient, která vychází z knihovního MQTTClient. Začněme s periferiemi.
#####################################
# 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 nastavují piny pro žárovku a tlačítko, stejně jako objekty pro měření studené a teplé vody.
S inicializací to není 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 nastavení je nám ve výchozím nastavení vhodná, ale mnoho nastavení je potřeba určit explicitně. Abychom nemuseli uvádět nastavení přímo v kódu, uchovávám je v textovém souboru config.txt. To umožňuje měnit kód nezávisle na nastaveních a také vyrobit několik stejných zařízení s různými parametry.
Poslední blok kódu spouští několik korutin pro obsluhu různých funkcí systému. Tady je například korutina, která obsluhuje počítadla
async def _counter_coro(self, counter, topic):
# Publikovat počáteční hodnotu
value = counter.value()
await self.publish(topic, str(value))
# Publikovat každou novou hodnotu
while True:
value = await counter
await self.publish_msg(topic, str(value))Korutina v cyklu čeká na nové hodnoty počítadla a jakmile se objeví — odešle zprávu pomocí protokolu MQTT. První kousek kódu posílá počáteční hodnotu, i když voda přes počítadlo neteče.
Základní třída MQTTClient se sama obsluhuje, sama iniciuje spojení přes WiFi a znovu se připojuje, když spojení vypadne. Při změnách stavu spojení 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 je funkční.')
duration = ticks_diff(ticks_ms(), self.internet_outage_start) // 1000
await self.publish_debug_msg('Znovu připojeno po', duration)
else:
self.internet_outages += 1
self.internet_outage_start = ticks_ms()
self.dprint('WiFi je nefunkční.')
await asyncio.sleep(0)Funkce byla převzata z příkladů. V tomto případě počítá počet výpadků (internet_outages) a jejich trvání. Při obnovení připojení se na server odesílá doba výpadku.
Mimochodem, poslední sleep je potřeba jen proto, aby funkce byla asynchronní – v knihovně je volána pomocí await, jinak lze vyvolávat pouze funkce, které obsahují jiný await.
Kromě připojení k WiFi je také potřeba navázat spojení s MQTT brokerem (serverem). Tím se také zabývá knihovna, a my máme možnost udělat něco užitečného, když je spojení navázáno.
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řihlašujeme k několika zprávám – server nyní má možnost nastavit aktuální hodnoty čítačů posláním odpovídající zprávy.
def mqtt_msg_handler(self, topic, msg):
topicstr = str(topic, 'utf8')
self.dprint("Přijata MQTT zpráva, téma={}, 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řijaté zprávy, a v závislosti na tématu (názvu zprávy) se aktualizují hodnoty jednoho z čítačů.
Několik 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 se zabývá odesíláním zprávy, pokud je připojení navázáno. Pokud připojení není – zpráva se ignoruje.
A to je prostě užitečná funkce, která formuluje 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))
Takže tolik textu a ještě jsme nezablikali LED. Tady 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)Připravil jsem 2 režimy blikání. Pokud dojde k výpadku připojení (nebo se teprve navazuje), zařízení bude blikat rychle. Pokud je připojení navázáno – zařízení bliká jednou za 5 sekund. Pokud je potřeba, mohou zde být implementovány i další režimy blikání.
Ale LED dioda je jen taková legrácka. My jsme si ještě vzali na starost displej.
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)Tohle je přesně to, o čem jsem mluvil — jak snadné a pohodlné to je s korutinami. Tato malá funkce popisuje VŠECHNA uživatelská interakce. Korutina jednoduše čeká na stisknutí tlačítka a zapne displej na 3 sekundy. Na displeji se zobrazují aktuální hodnoty čítačů.
Zbývá ještě pár drobností. Tady je funkce, která to celé (pře)spouští. Hlavní smyčka se zabývá pouze zasíláním různých ladicích informací každou minutu. V podstatě to uvádím tak, jak to je — myslím, že není třeba to nějak víc komentovat.
async def main(self):
while True:
try:
await self._connect_to_WiFi()
await self._run_main_loop()
except Exception as e:
self.dprint('Globální komunikační chyba: ', e)
await asyncio.sleep(20)
async def _connect_to_WiFi(self):
self.dprint('Připojuji se k 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('Připojeno!')
self.internet_outage = False
async def _run_main_loop(self):
# Smyčka navždy
mins = 0
while True:
gc.collect() # Pro RAM statistiky.
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("Nastala výjimka: ", e)
mins += 1
await asyncio.sleep(60)Ještě pár 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še se spouští takto.
client = CounterMQTTClient()
loop = asyncio.get_event_loop()
loop.run_until_complete(client.main())Něco se s mou pamětí děje.
Takže, celý kód máme. Soubory jsem nahrával pomocí utility ampy — ta umožňuje nahrávat je na interní (tu, která je v ESP-07) paměť a poté k nim přistupovat z programu jako k běžným souborům. Tam jsem také nahrál knihovny mqtt_as, uasyncio, ssd1306 a collections (používá se uvnitř mqtt_as).
Spouštíme a… dostáváme MemoryError. A čím více jsem se snažil pochopit, kde přesně paměť uniká, tím dříve se tato chyba objevovala, když jsem přidával ladicí výpisy. Krátké hledání na Googlu mě přivedlo k porezření, že v mikrokontroléru je celkem 30 kB paměti, do které se 65 kB kódu (včetně knihoven) prostě nevejde.
Existuje však řešení. Ukazuje se, že micropython neprovádí kód přímo z .py souboru — tento soubor je nejprve zkompilován. A to přímo na mikroprocesoru, přičemž se převádí na bytecode, který je pak uložen v paměti. K práci kompilátoru je také zapotřebí určitý objem operační paměti.
Trik spočívá v tom, že se zbavíme mikroprocesoru náročné kompilace. Můžeme zkompilovat soubory na výkonném počítači a do mikroprocesoru nahrát už hotový bytecode. K tomu je potřeba stáhnout firmware micropython a sestavit .
Nepíšu tedy Makefile, ale ručně jsem prošel a zkompiloval všechny potřebné soubory (včetně knihoven) asi takto
mpy-cross water_counter.pyZbývá jen nahrát soubory s příponou .mpy, aniž bychom zapomněli předtím odstranit odpovídající .py ze souborového systému zařízení.
Celou vývoj jsem vedl v programu (IDE?) ESPlorer. Ten umožňuje nahrávat skripty do mikroprocesoru a hned je provádět. V mém případě veškerá logika a vytvoření všech objektů je v souboru water_counter.py (.mpy). Aby to všechno spouštělo automaticky při startu, musí být také soubor s názvem main.py. A ten musí být přesně .py, nikoli předkompilovaný .mpy. Zde je jeho triviální obsah
import water_counterSpouštíme — vše funguje. Ale volné paměti znepokojivě málo — asi 1 kB. Mám ještě plány na rozšíření funkčnosti zařízení, a tento kilobajt mi rozhodně nebude stačit. Ukázalo se ale, že i pro tento případ existuje řešení.
O co jde. I když jsou soubory zkompilovány do bytecode a nacházejí se na vnitřním souborovém systému, ve skutečnosti se stále načítají do operační paměti a provádějí se odtud. Ale ukazuje se, že micropython umí provádět bytecode přímo z flash paměti, k tomu je ale potřeba ho přímo vložit do firmwaru. Není to složité, i když na mém netbooku to zabralo docela dost času (jenže tam jsem měl Linux).
Algoritmus je následující:
- Stáhnout a nainstalovat . Tato věc sestavuje kompilátor a knihovny pro programy na ESP8266. Sestavuje se podle pokynů na hlavní stránce projektu (vybral jsem instalaci STANDALONE=yes)
- Stáhnout
- Potřebné knihovny umístit do ports/esp8266/modules uvnitř struktury micropython
- Sestavte firmware podle pokynů v souboru
- Nahrávání firmwaru do mikrokontroléru (dělám to na Windows pomocí programů ESP8266Flasher nebo pythonovského esptoolu)
Hotovo, nyní 'import ssd1306' načte kód přímo z firmwaru a operační paměť tím pádem nebude spotřebována. Tímto trikem jsem do firmwaru nahrál pouze kód knihoven, zatímco hlavní kód programu se vykonává ze souborového systému. To umožňuje snadno modifikovat program bez překompilace firmwaru. Momentálně mám k dispozici asi 8.5 kB RAM. To umožní implementovat ještě spoustu užitečných funkcí v budoucnu. A pokud paměť úplně dojde, mohu do firmwaru nahrát i hlavní program.
A co teď s tím dělat?
Dobrá, hardware je zapojen, firmware je napsán, krabička je vytištěna, zařízení je přilepeno na stěnu a šťastně bliká. Ale zatím je to vše černá skříňka (v přímém i přeneseném smyslu) a jeho využití je dosud malé. Je čas něco udělat s MQTT zprávami, které se posílají na server.
Můj "chytrý dům" běží na . MQTT modul buď existuje z krabice, nebo se snadno instaluje z trhu doplňků — už si nepamatuji, odkud jsem ho vzal. MQTT není samostatná věc — potřebuje tzv. broker — server, který přijímá, třídí a přesměrovává klientům MQTT zprávy. Používám mosquitto, které (stejně jako majordomo) běží na tomtéž netbooku.
Jakmile zařízení alespoň jednou odešle zprávu, hodnota se okamžitě objeví v seznamu.

Tyto hodnoty lze nyní propojit s objekty systému, mohou být použity ve scénářích automatizace a podrobeny různé analýze — to vše je mimo rámec tohoto článku. Pokud někoho zajímá systém Majordomo, mohu doporučit — kamarád také staví chytrý dům a srozumitelně vysvětluje nastavení systému.
Ukážu jen pár grafů. Toto je jednoduchý graf hodnot za den

Je vidět, že v noci vodu téměř nikdo nepoužíval. Párkrát někdo zašel na toaletu a zdá se, že filtr zpětné osmózy v noci nasává pár litrů. Ráno spotřeba výrazně vzrůstá. Obvykle používám vodu z bojleru, ale tentokrát jsem se chtěl vykoupat a dočasně jsem přešel na městskou teplou vodu — to je také dobře vidět na dolním grafu.
Z tohoto grafu jsem zjistil, že na toaletu je potřeba 6-7 l vody, sprcha - 20-30 l, k mytí nádobí asi 20 l, a na koupel je potřeba 160 l. Za den moje rodina spotřebuje kolem 500-600 l.
Pro zvlášť zvídavé je možné nahlédnout do záznamů pro každou jednotlivou hodnotu.

Tudíž jsem se dozvěděl, že při otevřeném kohoutku voda teče rychlostí přibližně 1 l za 5 s.
Ale v této podobě asi není statistiku příliš pohodlné sledovat. V majordomo jsou také možnosti sledovat grafy spotřeby podle dnů, týdnů a měsíců. Tady například je graf spotřeby v sloupcích.

Zatím mám data pouze za týden. Po měsíci bude tento graf výstižnější - každému dni bude odpovídat samostatný sloupec. Trochu obraz kazí úpravy hodnot, které zadávám ručně (největší sloupec). A zatím není jasné, zda jsem na začátku špatně zadal první hodnoty téměř o kubík méně, nebo je to chyba v softwaru a ne všechny litry byly započítány. Potřebuji více času.
Na samotných grafech je ještě třeba zapracovat, vybělit a natřít. Možná také budu v debugovacích cílech vytvářet graf spotřeby paměti - co když něco uniká. Možná budu nějak zobrazovat období, kdy internet nefungoval. Zatím je to všechno na úrovni nápadu.
Závěr
Dnes se moje byt stala o něco chytřejší. S tímto malým přístrojem bude pro mě pohodlnější sledovat spotřebu vody v domácnosti. Pokud jsem se dříve rozčiloval „opět jsme za měsíc spotřebovali hodně vody“, teď budu moci najít zdroj této spotřeby.
Někomu se může zdát divné sledovat údaje na obrazovce, když je metr od samotného měřiče. Ale v nedaleké budoucnosti plánuji přestěhovat se do jiného bytu, kde bude několik vodovodních stojanů, a samotné měřiče pravděpodobně budou umístěny na schodišti. Takže zařízení pro dálkové odečty hodnot bude velmi užitečné.
Funkcionalitu zařízení také plánuji rozšířit. Už se dívám po motorických ventilech. Teď pro přepínání mezi ohřívačem a městskou vodou musím otáčet 3 kohouty v obtížně přístupném místě. Bylo by mnohem pohodlnější to udělat jedním tlačítkem s odpovídající indikací. No a samozřejmě, ochranu proti únikům realizovat rozhodně stojí za to.
V článku jsem popsal svou variantu zařízení založeného na ESP8266. Podle mého názoru se mi podařilo vytvořit docela zajímavou variantu firmwaru na micropython s využitím korutin — jednoduché a vkusné. Snažil jsem se popsat řadu nuancí a chyb, se kterými jsem se během procesu setkal. Možná jsem to popisoval příliš podrobně, osobně mi jako čtenáři přijde snadnější přeskočit přebytečné části, než si poté domýšlet, co bylo nedořečeno.
Jako vždy jsem otevřen konstruktivní kritice.
Zdroj: habr.com
