Vesi loendur ühendamine nutika koju

Kunagi olid koduautomaatika süsteemid, või nagu neid sageli nimetatakse 'nutikodu', äärmiselt kallid ning neid said endale lubada vaid rikkaimad. Tänapäeval on turul saadaval piisavalt taskukohaseid komplekte anduritega, nuppudega/lülititega ja täitemehhanismidega, et juhtida valgustust, pistikupesi, ventilatsiooni, veetarnet ja muid tarbijaid. I isegi kõige algajam DIY-entusiast saab endale lubada kokku panna seadmeid nutika kodu jaoks.

Vesi loendur ühendamine nutika koju

Tavaliselt pakutavad seadmed on kas andurid või täitemehhanismid. Need võimaldavad hõlpsasti teostada stsenaariume nagu 'liikumisanduri aktiveerimisel süüdata tuli' või 'väljalülitaja ukselukus kustutab kogu korteri valguse'. Kuid telemeetria on olnud veidi kehv. Parimal juhul on see temperatuuri ja niiskuse graafik või konkreetse pistikupesa hetkevõimsus.

Hiljuti installisin endale veemõõturid impulssväljundiga. Iga liitri juures, mis veemõõturist läbi voolab, aktiveerub magnetkontakt ja sulgeb kontakti. Jäänud on lihtsalt juhtmed ühendada ja proovida, kuidas sellest kasu saada. Näiteks analüüsida veetarbimist tundide ja nädalapäevade lõikes. Kui korteris on mitu veetõstikut, on mugavam näha kõiki hetkeandmeid ühel ekraanil, kui roomata taskutest lamp käes.

Allpool on minu lahendus ESP8266 baasil, mis loendab impulsside arvu veemõõturitest ja edastab näidud MQTT kaudu nutikodu serverisse. Programmeerime micropythonis, kasutades uasyncio raamatukogu. Firmavahi loomisel olen kokku puutunud mitmete huvitavate probleemidega, millest räägin samuti selles artiklis. Alustame!

Schneem

Vesi loendur ühendamine nutika koju

Kogu süsteemi keskpunkt on ESP8266 mikrokontrollerimoodul. Alguses oli plaanis ESP-12, kuid minule jäi defektne. pidin leppima ESP-07 mooduliga, mis oli saadaval. Õnneks on need nii väljundite kui ka funktsionaalsuse poolest identsed, erinevus on ainult antennis — ESP-12-l on see sisse ehitatud, ESP-07-l aga väline. Isegi antennita püüab WiFi signaal minu vannitoas normaalselt kinni.

Mooduli sidumine on standardne:

  • reset-nupp koos tõmbega ja kondensaatoriga (kuigi mõlemad on juba mooduli sees olemas)
  • enable-signaal (CH_PD) on ühendatud toite külge
  • GPIO15 on ühendatud maapinnaga. Seda on vaja ainult käivitamishetkel, aga mul pole enam midagi sellele jalale kinnitada.

Mooduli jõudmiseks programmeerimise režiimi tuleb GPIO2 ühendada maapinnaga ning mugavuse huvides on mul ette nähtud Boot-nupp. Normaalolukorras tõmmatakse see pinni toite külge.

GPIO2 olekontrollitakse ainult töökäigus alguses — toite sisselülitamisel või kohe pärast taaskäivitamist. Sel juhul kas moodul käivitub nagu tavaliselt või läheb urchoogu režiimile. Pärast käivitamist saab seda väljundit kasutada tavalise GPIO-na. Ja kuna seal juba on nupp, saab sellele kinnitada mõne kasuliku funktsiooni.

Programmeerimiseks ja tõrkeotsinguks kasutan UART-i, mille viisin ribale. Kui on vaja, siis liitun lihtsalt USB-UART adapteriga. Peab lihtsalt meeles pidama, et moodul töötab 3.3V-l. Kui unustada adapteri lülitamine sellepingele ja anda 5V, siis moodul tõenäoliselt läheb katki.

Mul ei ole vannitoas elektriga probleeme — pistik on umbes ühe meetri kaugusel arvestitest, nii et toitan sealt 220V-st. Toiteallikana kasutab mul väikest HLK-PM03 plokki Tenstar Robotilt. Isiklikult on mul analoog- ja võimsuselektroonikaga keeruline, kuid siin on valmis toiteplokk väikese korpuse sees.

Töötamise režiimide näitamiseks on mul ette nähtud LED, mis on ühendatud GPIO2. Kuid ma ei hakanud seda paigaldama, kuna ESP-07 moodulis on juba LED, mis on ühendatud samuti GPIO2-le. Aga plaadil olgu see igaks juhuks — äkki tahan selle LEDi korpusele viia.

Liigume kõige huvitavama juurde. Veearvestitel ei ole mingit loogikat, nende käest ei saa küsida praeguseid näite. Ainus, mis meil saadaval on, on impulsid — kontaktide sulgemine reedimise korral iga liitri kohta. Reedid on mul ühendatud GPIO12/GPIO13. Tõmbamisresistori lülitan sisse programmikoodis moodulis.

Algselt unustasin ma ette näha R8 ja R9 takistid ning minu plaadi versioonis ei ole neid. Aga kuna ma juba jagan skeemi üldiseks vaatamiseks, siis tasub see eksimus parandada. Takistid on vajalikud, et ei põletaks porti juhul, kui püsivara jätab vea ja seab pinni väärtuseks ühe, samas kui reed lühistab selle joone maapinnale (takistiga voolab maksimum 3.3V/1000Ω = 3.3mA).

On oluline mõelda, mida teha, kui elekter peaks ära katkemas. Esimene variant oleks algväärtuste pärimine serverilt käivitumise ajal. Kuid see tooks kaasa protokolli vahetuse oluliselt keerukamaks muutmise. Veelgi enam, seadme töökindlus sõltub sel juhul serveri olekust. Kui server elektri täieliku kadumise järel ei käivituks (või käivituks hiljem), ei suudaks veemõõtja algväärtusi küsida ning töötaks valesti.

Seetõttu otsustasin realiseerida mõõtjate väärtuste salvestamise mälukiipis, mis on ühendatud I2C kaudu. Mul ei ole suuri nõudeid mäluflashi suuruse osas — tuleb salvestada vaid 2 numbrit (kuupmeetrid sooja ja külma veemõõtjatelt). Isegi kõige väiksem moodul sobib. Kuid kirjutustsüklite arvu osas tuleks tähelepanu pöörata. Enamik mooduleid talub 100 tuhat tsüklit, mõned kuni miljoni.

Tundub, et miljon on palju. Kuid nelja aasta jooksul, mil olen elanud oma korteris, olen tarbinud veidi üle 500 kuubi vett, see on 500 tuhat liitrit! Ja 500 tuhat kirjet mälupulgale. Ja see on ainult külm vesi. Võib muidugi mikroskeemi iga paar aasta tagant ümber jootma hakata, aga selgus, et eksisteerivad FRAM mikroskeemid. Programmeerimise seisukohalt on see sama, mis I2C EEPROM, ainult et väga suure arvu kirjutamisringidega (sadu miljonit). Ainult et olen siiani poodi, kus neid mikroskeeme müüakse, jõudnud, seega seisab tavaline 24LC512 veel edasi.

Trükkplaat

Alguses plaanisin plaati kodus teha. Seetõttu projekteeriti plaat ühesuunalisena. Aga pärast tundi toimetamist lasertriikraudi ja jootemaskiga (ilma selleta ei ole see kuidagi comme il faut), otsustasin ikkagi tellida plaadid hiinlastelt.

Vesi loendur ühendamine nutika koju

Peaaegu tellimuse esitamise hetkel mõistsin, et lisaks flash-mächipile saab I2C bussile ühendada midagi muud kasulikku, näiteks ekraani. Mis täpselt sellele välja tuua — on veel küsimus, kuid lahti tuleb see plaadil teha. Kui ma aga otsustasin tellida plaadid tehases, siis ei olnud mõtet end piirata ühepoolse plaadiga, seega I2C read on ainsad plaadi tagaküljel.

Ühepoolse joonisega seondus ka üks suur viga. Kuna plaat oli joonistatud ühepoolne, siis kavandati, et joad ja SMD-komponendid paigutatakse ühele poole ning väljundkomponendid, pesad ja toiteallikas teisele. Kui sain plaadid kätte kuu aega hiljem, unustasin ma algse plaani ja paigaldasin kõik komponendid esipaneelile. Ja ainult siis, kui jõudsin toiteblokki jootma, selgus, et pluss ja miinus on vastupidiselt joonistatud. Tuli improviseerida hüpiküngastega. Ülaltoodud pildil olen ma juba joonistust muutnud, kuid maaühendust viiakse ühelt plaadi poolt teisele läbi Boot-nupu väljundeid (kuigi oleks võinud ka teisel kihil joont tõmmata).

Saime midagi sellist

Vesi loendur ühendamine nutika koju

Korpus

Järgmine samm on korpus. 3D-printeri olemasolul ei ole see probleem. Ei hakanud liiga keeruliseks minema — lihtsalt joonistasin vajaliku suurusega kasti ja tegin vajalikud lõiked. Ülemine kaas kinnitub korpusele väikeste kruvidega.

Vesi loendur ühendamine nutika koju

Olen juba maininud, et Boot-nuppu saab kasutada universaalse nupuna — seega toome selle esipaneelile. Selleks joonistasin spetsiaalse 'auk' nuppude jaoks.

Vesi loendur ühendamine nutika koju

Korpuse sees on ka jupid, kuhu plaat kinnitatakse ja fikseeritakse ühe M3 kruviga (plaadil ei olnud rohkem ruumi).

Valisin ekraani, kui olin välja prindinud esimese näidisversiooni korpusest. Tavaline kaherealine ekraan ei mahtunud sellesse, kuid leidsin kogudest OLED-ekraani SSD1306 128×32. Veidi väike, aga mulle ei meeldi sellega iga päev vahtida — sobib küll.

Mõeldes ja püüdes välja mõelda, kuidas saab juhtmeid juhtida, otsustasin kinnitada ekraani korpuse keskele. Ergonomika on muidugi madal — nupp on peal, ekraan all. Aga olen juba öelnud, et idee ekraani lisamiseks tuli liiga hilja ja ei viitsinud plaati ümber tõmmata, et nuppu paigutada.

Kokku pandud seade. Ekraanimoodul on liimitud termokleebi abil.

Vesi loendur ühendamine nutika koju

Vesi loendur ühendamine nutika koju

Lõpptulemust on võimalik näha KDPV-s.

Käivitus

Liigume programmi osa juurde. Selliste väikeste projekti jaoks meeldib mulle väga kasutada Pythonit (micropython) - kood muutub väga kompaktseks ja arusaadavaks. Õnneks ei ole siin vaja süveneda registritase, et välja pigistada mikrosekunde — kõik on tehtav Pythonist.

Paistab, et kõik on lihtne, aga mitte päris - seadmes on mitu iseseisvat funktsiooni:

  • Kasutaja klikkib nuppu ja vaatab ekraanile.
  • Liitrite väärtused uuenevad ja salvestatakse flash-mällu.
  • Moodul jälgib WiFi signaali ja vajadusel uuesti ühendub.
  • Ilma vilkuva lambita ei saa hakkama.

Ei saa lubada, et üks funktsioon ei tööta, kui teine mingil põhjusel takerdub. Olen juba ütlematult palju kaktusi söönud teistes projektides ja näen nüüd pidevalt vigu, nagu "hävis järgmine liiter, sest sel ajal uuendati ekraani" või "kasutaja ei saa midagi teha, kuni moodul on WiFi-ga ühendatud". Muidugi, mõningaid asju saab teha katkestustega, kuid võib tekkida piiranguid kestuse, sügava lahenduse või mitteatomaarsete muutuste osas muutujates. Ja kood, mis tegeleb kõigega korraga, muutub kiiresti mõnusaks pudruks.

V projekti tõsisem kasutasin klassikalist preemsidega korraga täitmist ja FreeRTOS-i, kuid antud juhul osutus sobivamaks mudel koorprogrammid (coroutines) ja teek uasync . Ning Python'i kooride teostus on lihtsalt suurepärane – kõik on programmerimisest lihtsalt ja mugavalt tehtud. Lihtsalt kirjuta oma loogika, lihtsalt ütle, kus kohtades saab voogude vahel vahetada.

Võite uurida erinevusi preemsidega ja konkurentsivõimelisuse vahel valikuliselt. Kuid nüüd liigume lõpuks koodi juurde.

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

Iga loendur töödeldakse Counter klassi näitega. Esmalt loetakse EEPROM-ist (value_storage) välja loenduri algväärtus — nii toimub taastamine pärast voolu katkestamist.

Pinna initsialiseeritakse sisseehitatud toite tõmbe abil: kui andur on suletud, on joon null, kui avatud, tõmbab joon toite poole ja kontroller loeb ühte.

Siin käivitatakse ka eraldi ülesanne, mis viib läbi pinni küsitluse. Iga loendur käivitab oma ülesande. Siin on selle kood:

    """ Küsi pinni ja suurenda väärtust, kui teine liiter on möödunud """
    async def _switchcheck(self):
        last_checked_pin_state = self._pin.value()  # Hangi algne olek

        # Küsitle pinni muutust
        while True:
            state = self._pin.value()
            if state != last_checked_pin_state:
                # Oleku muutus: tegutse nüüd.
                last_checked_pin_state = state
                if state == 0:
                    self._another_litre_passed()

            # Ignoreeri edasisi oleku muutusi, kuni lülitus on paika loksunud
            await asyncio.sleep_ms(Counter.debounce_ms)

25 ms viivitus on vajalik kontaktide müratõrjumiseks ning see reguleerib ka seda, kui tihti ülesanne ärkab (kui see ülesanne magab — töötavad teised ülesanded). Iga 25 ms jooksul ärkab funktsioon, kontrollib pinda ja kui hermeetikud on suletud, tähendab see, et üle loendur on kulgenud järgmine liiter, mis tuleb töödelda.

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

        self._value_storage.write(self._value)

Järgmise liitri töötlemine on triviaalne — lihtsalt suurendatakse loenduri väärtust. Ja oleks hea, kui uus väärtus salvestataks ka mälupulgale.

Kasutusmugavuse huvides on ette nähtud "juurdepääsud".

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

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

Nüüd kasutame Python'i ja raamatukogu uasync eeliseid ning teeme loenduri objektist ootava (kuidas seda eesti keelde tõlkida? Ootava objekti?).

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

        return self.value()

    __iter__ = __await__  

See on nii mugav funktsioon, mis ootab, kuni loenduri väärtus uuendatakse — funktsioon ärkab aeg-ajalt ja kontrollib märklauale _value_changed. Selle funktsiooni juures on see, et väljakutsuv kood võib jääda magama, kui seda funktsiooni kutsutakse, ja magada kuni uue väärtuse saamiseni.

Aga kuidas on katkestustega?Jah, siin kohal võite mind tüssata, et ma ju ütlesin katkestustest, aga tegelikult korraldasin ma rumala pinni küsitluse. Tegelikult on katkestused esimesed, mida proovisin. ESP8266-s saab korraldada katkestuse äärtes, ja isegi kirjutada selle katkestuse töötlejat pythoni keeles. Selle katkestuse abil saab värskendada muutuja väärtust. Tõenäoliselt piisaks sellest, kui loendur oleks orienteeritud seadmest — sellisest, mis ootab, kuni temalt küsitakse seda väärtust.

Kahjuks (või õnneks?) on minu seade aktiivne, see peaks ise saatma sõnumeid MQTT protokolli kaudu ja salvestama andmeid EEPROM-i. Siin tulevad mängu piirangud — katkestustes ei saa mälu eraldada ja kasutada suurt steki, seega võime unustada sõnumite saatmise üle võrgu. On olemas sellised kasulikud funktsioonid nagu micropython.schedule(), mis võimaldavad käivitada mingit funktsiooni 'niipea kui võimalik', kuid tekib küsimus 'mis mõtet on?'. Äkki saadame just praegu mingi sõnumi ja siia vahele tuleb katkestus, mis rikub muutujate väärtused. Või näiteks, serverilt tuli uus väärtus, enne kui me vanad kirjutamise lõpetasime. Ühesõnaga, tuleb korraldada sünkroonimine või leida mõni muu lahendus.

Ja aeg-ajalt ilmneb RuntimeError: schedule stack full ja kes seda teab, miks?

Selge küsimise ja uasync korral toimib see sel juhul kuidagi ilusamalt ja usaldusväärsemalt.

EEPROM-i töö olen viinud väiksemasse klassi.

class EEPROM():
    i2c_addr = const(80)

    def __init__(self, i2c):
        self.i2c = i2c
        self.i2c_buf = bytearray(4) # Vältige puhverloomist / -hävitamist iga kutsumise ajal


    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)

Pythoni otstarbel on otstarbel töötada baitidega, kuid just need baidid salvestatakse mällu. Pidin looma konversiooni täisarvu ja baitide vahel kasutades ustruct'i teeki.

Kuna ma ei tahtnud iga kord edasi anda I2C objekti ja mäluaadressi, panin kõik kokku väikseks ja mugavaks klassiks.

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)

I2C objekt luuakse järgmiste parameetritega

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

Jõuame kõige huvitavama juurde — suhtlemise elluviimine serveriga MQTT kaudu. Protokolli rakendamine pole vajalik — internetis leidub valmis asünkroonne rakendus. Kasutame seda.

Kõik huvitav on kogutud klassis CounterMQTTClient, mis põhineb teegil MQTTClient. Algame perifeeriast

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

Siin luuakse ja seadistatakse lambipirnide ja nuppude jalad, samuti külma ja sooja vee loendurite objektid.

Algatamine ei ole nii triviaalne

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

mqtt_as teegi seadistuste määramiseks kasutatakse suurt erinevate seadistuste sõnastikku — config. Enamik vaike seadistusi sobib meile, kuid paljusid seadistusi tuleb seada selgelt. Et mitte kirjutada seadistusi otse koodi, hoian neid tekstifailis config.txt. See võimaldab muuta koodi sõltumatult seadistustest ja samuti paljundada mitmeid identseid seadmeid erinevate parameetritega.

Viimane koodiblokk käivitab mitmeid koorute, mis teenindavad süsteemi erinevaid funktsioone. Näiteks on siin kooruta, mis teenindab loendureid.

    async def _counter_coro(self, counter, topic):
        # Avalda algväärtus
        value = counter.value()
        await self.publish(topic, str(value))

        # Avalda iga uus väärtus
        while True:
            value = await counter
            await self.publish_msg(topic, str(value))

Kooruta ootab tsüklis uut loenduri väärtust ja kohe kui see ilmub — saadab sõnumi MQTT protokolli kaudu. Esimene koodilõik saadab algväärtuse isegi siis, kui vesi loenduri kaudu ei voola.

MQTTClient põhiklass hooldab end ise, alustades WiFi-ühenduse loomist ja taasalustades ühenduse katkemisel. WiFi-ühenduse oleku muutumise korral teavitab meid raamatukogu funktsioon wifi_connection_handler.

    async def wifi_connection_handler(self, state):
        self.internet_outage = not state
        if state:
            self.dprint('WiFi on üles.')
            duration = ticks_diff(ticks_ms(), self.internet_outage_start) / 1000
            await self.publish_debug_msg('Taasiseseisvumine', duration)
        else:
            self.internet_outages += 1
            self.internet_outage_start = ticks_ms()
            self.dprint('WiFi on all.')
            
        await asyncio.sleep(0)

Funktsioon on otse näidetest üle võetud. Antud juhul loendab see katkestuste (internet_outages) arv ja nende kestvuse. Ühenduse taastumisel saadetakse serverisse seisaku aeg.

Muide, viimane sleep on vajalik ainult selleks, et funktsioon oleks asünkrooniline – raamatukogus kutsutakse seda välja await kaudu, samas võivad ainult need funktsioonid, mille kehas on teine await, välja kutsuda.

Lisaks WiFi-ühendusele tuleb luua ka ühendus MQTT brokeriga (serveriga). Sellega tegeleb samuti raamatukogu, ja meil on võimalus teha midagi kasulikku, kui ühendus on loodud.

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

Siin tellime mitu teadet — serveril on nüüd võimalus määrata praegused arvnäitajad, saates vastava sõnumi.

    def mqtt_msg_handler(self, topic, msg):
        topicstr = str(topic, 'utf8')
        self.dprint("Saadud MQTT sõnumi teema={}, sõnum={}".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))

See funktsioon töötleb saabunud sõnumeid ja teema (sõnumi nimi) alusel uuendatakse ühe arvnäitaja väärtusi.

Mõned abifunktsioonid

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

See funktsioon saadab sõnumi, kui ühendus on loodud. Kui ühendust ei ole — sõnumit eiratakse.

Ja see on lihtsalt mugav funktsioon, mis koostab ja saadab silumis-sõnumeid.

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

Nii palju teksti, aga me pole ikka veel LED-tuld vilgutanud. Siin see on.

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

Ma olen loonud 2 vilkumise režiimi. Kui ühendus on kadunud (või just loodi), vilgub seade kiiresti. Kui ühendus on loodud, vilgub seade iga 5 sekundi järel. Vajadusel saab siia rakendada ka muid vilkumise režiime.

Aga LED on siiski vaid üks lõbu. Me oleme ju plaaninud ka ekraani.

    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)

See ongi see, millest ma rääkisin — kui lihtne ja mugav on kasutada korutine. See väike funktsioon kirjeldab KÕIKI kasutajaga suhtlemist. Korutine lihtsalt ootab nupu vajutamist ja lülitab ekraani sisse 3 sekundiks. Ekraanil kuvatakse praegused arvestite näidud.

Jäänud on veel paar pisiasja. Siin on funktsioon, mis kõike seda (taas)käivitab. Peamine tsükkel saadab vaid erinevat silumisinfo kord kuus. Ühesõnaga, toon nagu on — eraldi kommenteerimist, ma arvan, pole vaja.

   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)

Noh, veel paar seadistust ja konstantide täiendamiseks

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

Käivitame kõik nii

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

Minu mälu tundub kummaline

Nii, kogu kood on olemas. Failid laadisin üles ampy utiliidi abil — see võimaldab neid laadida ESP-07 sise-flash'isse ja seejärel pääseda programmiga ligi nagu tavalistele failidele. Sinna laadisin ka oma kasutatavad raamatukogud mqtt_as, uasyncio, ssd1306 ja collections (kasutatakse mqtt_as sees).

Käivitame ja… Saame MemoryError'i. Mida rohkem üritasin välja selgitada, kus täpselt mälu lekib, seda varem see viga ilmus. Lühike googeldamine viis mind arusaamiseni, et mikrokontrolleris on kokku 30 kB mälu, kuhu 65 kB koodi (koos raamatukogudega) kuidagi ei mahtuda.

Kuid väljapääs on olemas. Selgub, et micropython ei täida koodi otse .py failist — see fail kompileeritakse esmalt. Ja see kompileeritakse otse mikrokontrolleril, muundudes baitkoodiks, mis siis mälu salvestatakse. Noh, ja kompilatori tööks on samuti vajalik teatud kogus töömäluga.

Trikk seisneb ressursimahukast kompileerimisest vabastamises. Saate faile kompileerida suurel arvutil ja laadida valmis bytecode mikrokontrollerisse. Selleks peate alla laadima Micropythoni püsivara ja koguma. utiliidi mpy-cross.

Ma ei hakanud kirjutama Makefile'i, vaid läksin käsitsi ja kompileerisin kõik vajalikud failid (sealhulgas raamatukogud) umbkaudu nii.

mpy-cross water_counter.py

Jäänud on ainult laadida failid .mpy laiendiga, unustamata eelnevalt eemaldada vastavad .py seadme failisüsteemist.

Kogu arendust tegin programmis (IDE?) ESPlorer. See võimaldab laadida skripte mikrokontrollerisse ja neid kohe käivitada. Minu puhul asub kogu loogika ja kõik objektide loomine failis water_counter.py (.mpy). Kuid selle automaatseks käivitamiseks peab olema ka fail nimega main.py. See peab olema just .py, mitte eelnevalt kompileeritud .mpy. Siin on selle triviaalne sisu.

import water_counter

Käivitame — kõik töötab. Kuid mälu on ohtlikult vähe — ligikaudu 1 kb. Mul on veel plaanid seadme funktsionaalsuse laiendamiseks, ja see kilobait ei ole mulle selgelt piisav. Kuid selgub, et ka selle jaoks on lahendus.

Asi on selles. Isegi kui failid on kompileeritud baitkoodiks ja asuvad sisemisel failisüsteemil, laaditakse nad ikkagi korralikult operatiivmälu ja täidetakse sealt. Kuid osutub, et micropython oskab täita baitkoodi otse välkmälust, kuid selleks tuleb see otse tarkvarale sisse ehitada. See ei ole keeruline, kuigi minu netbookis võttis see märkimisväärselt aega (ainult seal oli mul Linux).

Algoritm on selline:

  • Laadi alla ja installi ESP Open SDK. See tööriist kogub kompilaatori ja raamatukogud ESP8266 programmide jaoks. Kogumine toimub projekti peamise lehe juhendi järgi (valisin STANDALONE=yes installatsiooni).
  • Laadi alla micropython allikakoodid
  • Vajadusel raamatukogud paigutada ports/esp8266/modules kausta micropythonis.
  • Kogume tarkvara vastavalt juhendile failis. ports/esp8266/README.md
  • Laime tarkvara mikrokontrollerisse (teen seda Windowsis programmide ESP8266Flasher või Pythoniga esptool’iga).

Nüüd, kui ‘import ssd1306’ koormab koodi otse püsivara, ei kulu operatiivmälu selleks. Nii sain püsivara lihtsalt raamatukogude koodi, samas kui programmi põhikood töötab failisüsteemist. See võimaldab mul hõlpsasti programmi muuta ilma püsivara uuesti kompileerimata. Praegu on mul vabalt umbes 8,5 kB RAM-i. See võimaldab tulevikus rakendada veel palju erinevaid kasulikke funktsioone. Aga kui mälu väheneb, siis võib ka põhiprogrammi püsivara peale tõukuda.

Ja mida nüüd sellega teha?

Olgu, riistvara on kokku joodetud, püsivara on kirjutatud, kast on trükitud, seade on seinale kinnitatud ja vilgub rõõmsalt. Aga seni on see kõik must kast (otseses ja ülekantud tähenduses) ja sellest on veel vähe kasu. On aeg midagi teha MQTT sõnumitega, mis saadetakse serverisse.

Minu "nutikodu" töötab Majordomo süsteemil. MQTT moodul on kas juba olemas või hõlpsasti paigaldatav lisandite turult — ei mäleta täpselt, kust see mul on. MQTT ei ole iseseisev — vajalik on nn vahendaja — server, mis võtab vastu, sorteerib ja suunab MQTT sõnumid klientidele. Kasutan mosquitto, mis (nagu ka majordomo) töötab samal netbookil.

Pärast seda, kui seade on korra sõnumi saatnud, kuvatakse väärtus kohe nimekirjas.

Vesi loendur ühendamine nutika koju

Neid väärtusi saab nüüd siduda süsteemi objektidega, neid saab kasutada automatiseerimise stsenaariumites ja erinevate analüüsideks — kõik see ei ole antud artikli teema. Neile, kes on huvitatud majordomo süsteemist, soovitan kanalit Elektroniika objektiivis — sõber ehitab samuti nutikat kodu ja selgitab süsteemi seadistamist arusaadavalt.

Näitan vaid paar graafikut. See on lihtne päevane väärtuste graafik.

Vesi loendur ühendamine nutika koju
On näha, et öösel vett peaaegu ei kasutata. Mõned korrad käidi vetsus ja näib, et pöördfiltri kulutused on paar liitrit öö jooksul. Hommikul tarbimine tõuseb märgatavalt. Tavaliselt kasutan ma vett boilerist, kuid seekord tahtsin vannis käia ning lülitasin ajutiselt linnakuumavee peale — seda on ka alumiselt grafikult hästi näha.

Sellest graafikust sain teada, et vetsus käimine tähendab 6-7 liitrit vett, duši all käimine 20-30 liitrit, nõudepesemine umbes 20 liitrit, kuid vannis käimiseks kulub 160 liitrit. Päeva jooksul tarbib mu pere kuskil 500-600 liitrit.

Eriti uudishimulikele on võimalik vaadata iga eraldi väärtuse kirjeid.

Vesi loendur ühendamine nutika koju

Siit sain teada, et avatud kraanist voolab vesi umbes 1 liitri kiirusel 5 sekundi jooksul.

Aga sellisel kujul on statistikat ilmselt ebamugav vaadata. Majordomos on ka võimalusi vaadata tarbimisgraafikuid päevade, nädalate ja kuude kaupa. Näiteks, siin on tarbimisgraafik veergudena.

Vesi loendur ühendamine nutika koju

Praegu on mul andmeid ainult nädala kohta. Kuu pärast näeb see graafik välja informatiivsem — iga päev vastab eraldi baarile. Veidi rikub pilti väärtuste korrektuur, mida ma käsitsi sisestan (suurim baar). Ja praegu ei ole selge, kas ma määrasin esialgsed väärtused vale, peaaegu kuubiku võrra madalamaks, või on see tarkvara tõrge ja mitte kõik liitrid läksid arvesse. Vajan rohkem aega.

Graadikud vajavad veel kohendamist, valgendamist, värvimist. Võib-olla hakkan ka katsetamise eesmärgil looma mälu kasutamise graafikut — äkki seal lekkib midagi. Võib-olla näitan kuidagi perioode, mil internet puudus. Praegu on kõik see veel idee tasemel.

Kokkuvõte

Täna sai minu korter veidi nutikamaks. Sellise väikese seadmega on mul mugavam jälgida vee tarbimist minu kodus. Kui varem olin ma häiritud, et 'taas oleme kuu jooksul liiga palju vett kasutanud', siis nüüd suudan ma leida selle tarbimise allika.

Mõnele võib tunduda veider vaadata näitu ekraanilt, kui ta on meetri kaugusel loendist. Kuid mitte väga kauges tulevikus plaanin kolida teise korterisse, kus on mitu veetoru ning loendid asuvad tõenäoliselt trepikojas. Seega on eemalolekute näitude eemaldamise seade vägagi teretulnud.

Seadme funktsionaalsust plaanin samuti laiendada. Olen juba silma peal hoidnud mootorradil ventiilidel. Praegu pean boileri ja linna vee vahetamiseks keerama 3 kraani raskesti ligipääsetavas nišis. Oluliselt mugavam oleks seda teha ühe nupuvajutusega koos vastava indikaatoriga. Loomulikult tasub samuti teostada lekke kaitse.

Artiklis tutvustan oma varianti seadme kohta, mis põhineb ESP8266-l. Minu arvates on mul välja tulnud üsna huvitav mikropython versiooni, mis kasutab korutine — lihtne ja kena. Olen püüdnud kirjeldada palju nüansse ja vigu, millega kohtusin teel. Võib-olla kirjeldasin ma liiga detailselt, isiklikult oleks mul kui lugejal lihtsam üle vaadata liigne, kui hiljem mõelda sellele, mis jäi ütlemata.

Nagu alati, olen konstruktiivseks kriitikaks avatud.

Allikasood
Scheem ja plaadid
Korpuse mudel

Allikas: habr.com

Osta usaldusväärne veebihosting DDoS kaitsega, VPS VDS serverid 🔥 Osta usaldusväärne veebihosting DDoS kaitsega, VPS VDS serverid | ProHoster