Yhdistämme vesimittarin älykotiin

Aikoinaan kodin automaatiojärjestelmät tai "älykäs koti", kuten niitä usein kutsuttiin, olivat hirvittävän kalliita ja vain rikkailla oli niihin varaa. Nykyään markkinoilta löydät melko edullisia sarjoja, joissa on antureita, painikkeita/kytkimiä ja toimilaitteita valaistuksen, pistorasian, ilmanvaihdon, vesihuollon ja muiden kuluttajien ohjaamiseen. Ja kieroinkin tee-se-itse-ihminen voi päästä mukaan kauneuteen ja koota älykotiin laitteita edullisesti.

Yhdistämme vesimittarin älykotiin

Tyypillisesti ehdotetut laitteet ovat joko antureita tai toimilaitteita. Niiden avulla on helppo toteuttaa skenaarioita, kuten "kun liiketunnistin laukeaa, sytytä valot" tai "uloskäynnin lähellä oleva kytkin sammuttaa valot koko asunnosta". Mutta jotenkin asiat eivät toimineet telemetrian kanssa. Parhaimmillaan se on kaavio lämpötilasta ja kosteudesta tai hetkellisestä tehosta tietyssä pistorasiassa.

Asensin hiljattain vesimittarit pulssilla. Jokaista mittarin läpi kulkevaa litraa kohden reed-kytkin aktivoituu ja sulkee koskettimen. Ainoa asia, mitä voi tehdä, on tarttua johtoihin ja yrittää saada siitä hyötyä. Analysoi esimerkiksi vedenkulutusta tunnin ja viikonpäivän mukaan. No, jos asunnossa on useita vesinousuja, on helpompi nähdä kaikki nykyiset indikaattorit yhdellä näytöllä kuin kiivetä vaikeapääsyisiin markkinarakoihin taskulampulla.

Leikkauksen alla on minun versioni ESP8266-pohjaisesta laitteesta, joka laskee pulsseja vesimittareista ja lähettää lukemat MQTT:n kautta älykodin palvelimelle. Ohjelmoimme micropythonissa käyttämällä uasyncio-kirjastoa. Kun luot laiteohjelmistoa, törmäsin useisiin mielenkiintoisiin vaikeuksiin, joista keskustelen myös tässä artikkelissa. Mennä!

ohjelma

Yhdistämme vesimittarin älykotiin

Koko piirin sydän on ESP8266-mikrokontrollerin moduuli. ESP-12 oli alun perin suunniteltu, mutta omani osoittautui vialliseksi. Meidän piti tyytyä ESP-07-moduuliin, joka oli saatavilla. Onneksi ne ovat samat sekä nastojen että toiminnallisuuden suhteen, ainoa ero on antennissa - ESP-12:ssa on sisäänrakennettu, kun taas ESP-07:ssä on ulkoinen. Kuitenkin myös ilman WiFi-antennia kylpyhuoneessani signaali vastaanotetaan normaalisti.

Vakiomoduulin johdotus:

  • nollauspainike vedolla ja kondensaattorilla (vaikka molemmat ovat jo moduulin sisällä)
  • Aktivointisignaali (CH_PD) vedetään virtaan
  • GPIO15 vedetään maahan. Tätä tarvitaan vain alussa, mutta minulla ei ole vieläkään mitään kiinnitettävää tähän jalkaan, en enää tarvitse sitä

Moduulin siirtämiseksi laiteohjelmistotilaan sinun on oikosuljettava GPIO2 maahan, ja helpottaaksesi sitä, toimitin Boot-painikkeen. Normaalitilassa tämä tappi on vedetty virtaan.

GPIO2-linjan tila tarkistetaan vasta toiminnan alussa - kun virta kytketään tai välittömästi nollauksen jälkeen. Joten moduuli joko käynnistyy normaalisti tai menee laiteohjelmistotilaan. Kun nastaa on ladattu, sitä voidaan käyttää tavallisena GPIO:na. No, koska siellä on jo painike, voit liittää siihen jonkin hyödyllisen toiminnon.

Ohjelmointiin ja virheenkorjaukseen käytän UART:ta, joka lähetetään kampaan. Tarvittaessa liitän siihen vain USB-UART-sovittimen. Sinun tarvitsee vain muistaa, että moduuli saa virtansa 3.3 V:sta. Jos unohdat kytkeä sovittimen tähän jännitteeseen ja syöttää 5V, moduuli todennäköisesti palaa loppuun.

Minulla ei ole ongelmia kylpyhuoneen sähkön kanssa - pistorasia sijaitsee noin metrin päässä mittareista, joten saan virran 220 V:sta. Virtalähteenä minulla on pieni lohko HLK-PM03 kirjoittanut Tenstar Robot. Itselläni on hankaluuksia analogisen ja tehoelektroniikan kanssa, mutta tässä on valmis virtalähde pienessä kotelossa.

Toimintatilojen signaloimiseksi toimitin LEDin, joka on kytketty GPIO2:een. En kuitenkaan purkanut sitä, koska... ESP-07-moduulissa on jo LED, ja se on myös kytketty GPIO2:een. Mutta olkoon se taululla, jos haluan lähettää tämän LEDin koteloon.

Siirrytään mielenkiintoisimpaan osaan. Vesimittareissa ei ole logiikkaa, et voi kysyä niiltä ajankohtaisia ​​lukemia. Ainoa asia, joka meillä on käytettävissämme, ovat impulssit - reed-kytkimen koskettimien sulkeminen litran välein. Reed-kytkimen ulostulot on kytketty GPIO12/GPIO13:een. Otan vetovastuksen käyttöön ohjelmallisesti moduulin sisällä.

Aluksi unohdin toimittaa vastukset R8 ja R9, eikä minun levyversiossani ole niitä. Mutta koska julkaisen kaavion jo kaikkien nähtäväksi, on syytä korjata tämä virhe. Vastuksia tarvitaan, jotta porttia ei polteta, jos laiteohjelmisto häiritsee ja asettaa nastan yhdeksi, ja reed-kytkin oikosulkee tämän linjan maahan (vastuksen maksimi 3.3V/1000Ohm = 3.3mA).

On aika miettiä, mitä tehdä, jos sähköt katkeavat. Ensimmäinen vaihtoehto on pyytää alkulaskurin arvot palvelimelta alussa. Mutta tämä vaatisi huomattavan monimutkaisen vaihtoprotokollasta. Lisäksi laitteen suorituskyky riippuu tässä tapauksessa palvelimen tilasta. Jos palvelin ei käynnisty virran katkaisun (tai käynnistyksen jälkeen) jälkeen, vesimittari ei pystyisi pyytämään alkuarvoja eikä toimi oikein.

Siksi päätin toteuttaa laskurin arvojen tallentamisen I2C:n kautta kytketyssä muistisirussa. Minulla ei ole erityisiä vaatimuksia flash-muistin koolle - sinun tarvitsee vain tallentaa 2 numeroa (litramäärä kuuma- ja kylmävesimittarien mukaan). Jopa pienin moduuli käy. Mutta sinun on kiinnitettävä huomiota tallennusjaksojen määrään. Useimmille moduuleille tämä on 100 tuhatta jaksoa, joidenkin jopa miljoona.

Miljoona näyttää olevan paljon. Mutta asunnossani asuessani 4 vuotta kulutin hieman yli 500 kuutiometriä vettä, se on 500 tuhatta litraa! Ja 500 tuhatta levyä flashissä. Ja se on vain kylmää vettä. Voit tietysti juottaa sirun uudelleen parin vuoden välein, mutta käy ilmi, että FRAM-siruja on olemassa. Ohjelmoinnin näkökulmasta tämä on sama I2C EEPROM, vain erittäin suurella määrällä uudelleenkirjoitusjaksoja (satoja miljoonia). En vain vieläkään pääse kauppaan sellaisilla mikropiireillä, joten toistaiseksi tavallinen 24LC512 kestää.

Piirilevy

Aluksi ajattelin tehdä taulun kotona. Siksi taulu on suunniteltu yksipuoliseksi. Mutta kun olin viettänyt tunnin laserraudan ja juotosmaskin kanssa (ilman sitä ei jotenkin ole comme il faut), päätin silti tilata levyt kiinalaisista.

Yhdistämme vesimittarin älykotiin

Melkein ennen levyn tilaamista tajusin, että I2C-väylään voisi flash-muistisirun lisäksi liittää jotain muutakin hyödyllistä, kuten näytön. Mitä sille tarkalleen tulostaa, on edelleen kysymys, mutta se on reitittävä laudalla. No, koska aioin tilata levyt tehtaalta, ei ollut mitään järkeä rajoittua yksipuoliseen levyyn, joten I2C-linjat ovat ainoita levyn takapuolella.

Yksi iso ongelma oli myös yksisuuntaisessa johdotuksessa. Koska Levy piirrettiin yksipuoliseksi, joten toiselle puolelle suunniteltiin raidat ja SMD-komponentit ja toiselle lähtökomponentit, liittimet ja virtalähde. Kun sain levyt kuukautta myöhemmin, unohdin alkuperäisen suunnitelman ja juotin kaikki komponentit etupuolelle. Ja vasta virtalähteen juottamisessa kävi ilmi, että plus ja miinus oli kytketty päinvastoin. Minun piti viljellä puseroiden kanssa. Yllä olevassa kuvassa olen jo vaihtanut johdotuksen, mutta maadoitus siirtyy levyn osasta toiseen Boot-painikkeen nastojen kautta (tosin toiselle kerrokselle olisi mahdollista piirtää raita).

Siitä tuli näin

Yhdistämme vesimittarin älykotiin

kotelo

Seuraava askel on vartalo. Jos sinulla on 3D-tulostin, tämä ei ole ongelma. En vaivautunut liikaa - piirsin vain oikean kokoisen laatikon ja tein leikkauksia oikeisiin paikkoihin. Kansi kiinnitetään runkoon pienillä itsekierteittävillä ruuveilla.

Yhdistämme vesimittarin älykotiin

Mainitsin jo, että Boot-painiketta voidaan käyttää yleiskäyttöisenä painikkeena - joten näytämme sen etupaneelissa. Tätä varten piirsin erityisen "kaivon", jossa painike asuu.

Yhdistämme vesimittarin älykotiin

Kotelon sisällä on myös tapit, joihin levy asennetaan ja kiinnitetään yhdellä M3-ruuvilla (laudalla ei ollut enää tilaa)

Valitsin näytön jo, kun tulostin kotelon ensimmäisen malliversion. Tavallinen kaksirivinen lukija ei mahtunut tähän koteloon, mutta pohjassa oli OLED-näyttö SSD1306 128×32. Se on vähän pieni, mutta minun ei tarvitse tuijottaa sitä joka päivä – se on liikaa minulle.

Pohdittuani tämän tavan ja kuinka johdot siitä reititetään, päätin kiinnittää näytön kotelon keskelle. Ergonomia on tietysti alle parin - painike on ylhäällä, näyttö on alhaalla. Mutta sanoin jo, että ajatus näytön kiinnittämisestä tuli liian myöhään ja olin liian laiska johdottamaan korttia uudelleen painikkeen siirtämiseksi.

Laite on koottu. Näyttömoduuli on liimattu räkälle kuumaliimalla

Yhdistämme vesimittarin älykotiin

Yhdistämme vesimittarin älykotiin

Lopputulos näkyy KDPV:ssä

lisäys

Siirrytään ohjelmistoosaan. Tällaisissa pienissä askarteluissa tykkään todella käyttää Pythonia (micropython) - koodi osoittautuu erittäin kompaktiksi ja ymmärrettäväksi. Onneksi ei tarvitse laskeutua rekisteritasolle mikrosekuntien puristamiseksi - kaiken voi tehdä Pythonista.

Näyttää siltä, ​​​​että kaikki on yksinkertaista, mutta ei kovin yksinkertaista - laitteella on useita itsenäisiä toimintoja:

  • Käyttäjä painaa painiketta ja katsoo näyttöä
  • Litrat tikkuttavat ja päivittävät arvoja flash-muistissa
  • Moduuli tarkkailee WiFi-signaalia ja muodostaa tarvittaessa yhteyden uudelleen
  • No, ilman vilkkuvaa lamppua se on mahdotonta

Et voi olettaa, että yksi toiminto ei toiminut, jos toinen on jostain syystä jumissa. Olen jo saanut kaktuksia täyteen muissa projekteissa ja nyt näen edelleen häiriöitä tyyliin "toinen litra jäi kesken, koska näyttö päivittyi sillä hetkellä" tai "käyttäjä ei voi tehdä mitään, kun moduuli muodostaa yhteyden WiFi.” Tietysti jotkin asiat voidaan tehdä keskeytysten kautta, mutta saatat törmätä kestoon, puheluiden sisäkkäisiin rajoituksiin tai muuttujien ei-atomillisiin muutoksiin. No, koodi, joka tekee kaiken, muuttuu nopeasti soseeksi.

В vakavampi projekti Käytin klassista ennaltaehkäisevää moniajoa ja FreeRTOSia, mutta tässä tapauksessa malli osoittautui paljon sopivammaksi korutiinit ja uasync-kirjastot . Lisäksi korutiinien Python-toteutus on yksinkertaisesti hämmästyttävää - kaikki tehdään yksinkertaisesti ja kätevästi ohjelmoijalle. Kirjoita vain oma logiikkasi ja kerro minulle, missä paikoissa voit vaihtaa streamien välillä.

Suosittelen tutkimaan ennaltaehkäisevän ja kilpailevan moniajon eroja valinnaisena aiheena. Siirrytään nyt vihdoin koodiin.

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

Jokaista laskuria käsittelee Counter-luokan esiintymä. Ensinnäkin laskurin alkuperäinen arvo vähennetään EEPROM-muistista (value_storage) - näin toteutetaan palautuminen sähkökatkon jälkeen.

Nasta alustetaan sisäänrakennetulla vedolla virtalähteeseen: jos reed-kytkin on kiinni, linja on nolla, jos johto on auki, se vedetään virtalähteeseen ja ohjain lukee yhden.

Täällä käynnistetään myös erillinen tehtävä, joka pollaa pinssin. Jokainen laskuri suorittaa oman tehtävänsä. Tässä on hänen koodinsa

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

25 ms:n viive tarvitaan kontaktien pomppimisen suodattamiseen, ja samalla se säätelee, kuinka usein tehtävä herää (kun tämä tehtävä on lepotilassa, muut tehtävät ovat käynnissä). 25 ms välein toiminto herää, tarkastaa tapin ja jos reed-kytkimen koskettimet ovat kiinni, niin toinen litra on kulkenut mittarin läpi ja tämä on käsiteltävä.

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

        self._value_storage.write(self._value)

Seuraavan litran käsittely on triviaalia - laskuri yksinkertaisesti kasvaa. No, olisi kiva kirjoittaa uusi arvo muistitikulle.

Käytön helpottamiseksi tarjotaan "lisälaitteita".

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

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

No, nyt hyödynnetään Pythonin ja uasync-kirjaston herkkuja ja tehdään odotettava vastaobjekti (miten voimme kääntää tämän venäjäksi? Sellainen, jota voit odottaa?)

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

        return self.value()

    __iter__ = __await__  

Tämä on niin kätevä toiminto, joka odottaa kunnes laskurin arvo päivittyy - funktio herää aika ajoin ja tarkistaa _value_changed -lipun. Hienoa tässä toiminnossa on, että kutsukoodi voi nukahtaa tätä toimintoa kutsuttaessa ja nukkua, kunnes uusi arvo vastaanotetaan.

Entä keskeytykset?Kyllä, tässä vaiheessa voit trollata minua sanomalla, että sanoit itse keskeytyksistä, mutta todellisuudessa teit tyhmän nastakyselyn. Itse asiassa keskeytykset ovat ensimmäinen asia, jonka yritin. ESP8266:ssa voit järjestää reunakeskeytyksen ja jopa kirjoittaa käsittelijän tälle keskeytykselle Pythonissa. Tässä keskeytyksessä muuttujan arvo voidaan päivittää. Luultavasti tämä riittäisi, jos laskuri olisi orjalaite - sellainen, joka odottaa, kunnes siltä kysytään tätä arvoa.

Valitettavasti (tai onneksi?) laitteeni on aktiivinen, sen täytyy itse lähettää viestejä MQTT-protokollan kautta ja kirjoittaa dataa EEPROMiin. Ja tässä tulevat peliin rajoitukset - et voi varata muistia keskeytyksissä ja käyttää suurta pinoa, mikä tarkoittaa, että voit unohtaa viestien lähettämisen verkon kautta. On olemassa pulloja, kuten micropython.schedule(), joiden avulla voit suorittaa jonkin toiminnon "mahdollisimman pian", mutta herää kysymys: "mitä järkeä?" Entä jos lähetämme juuri nyt jonkinlaisen viestin, ja sitten tulee keskeytys ja pilaa muuttujien arvot. Tai esimerkiksi uusi laskuriarvo saapui palvelimelta, kun emme olleet vielä kirjoittaneet vanhaa muistiin. Yleensä sinun on estettävä synkronointi tai päästävä siitä pois jotenkin eri tavalla.

Ja aika ajoin RuntimeError: ajoita pinon täydet kaatumiset ja kuka tietää miksi?

Eksplisiittisellä kyselyllä ja uasyncillä tässä tapauksessa se osoittautuu jotenkin kauniimmaksi ja luotettavammaksi

Toin EEPROM-työtä pienelle luokalle

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)

Pythonissa on vaikea työskennellä suoraan tavujen kanssa, mutta tavut kirjoitetaan muistiin. Minun piti rajata muunnos kokonaislukujen ja tavujen välillä käyttämällä ustruct-kirjastoa.

Jotta en siirtäisi I2C-objektia ja muistisolun osoitetta joka kerta, käännän sen kaiken pieneen ja kätevään klassikkoon

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)

Itse I2C-objekti luodaan näillä parametreilla

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

Tulemme mielenkiintoisimpaan osaan - viestinnän toteuttamiseen palvelimen kanssa MQTT:n kautta. No, itse protokollaa ei tarvitse toteuttaa - löysin sen Internetistä valmis asynkroninen toteutus. Tätä aiomme käyttää.

Kaikki mielenkiintoisimmat asiat kerätään CounterMQTTClient-luokkaan, joka perustuu MQTTClient-kirjastoon. Aloitetaan periferialta

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

Täällä voit luoda ja konfiguroida lampun nastoja ja painikkeita sekä kylmä- ja kuumavesimittariobjekteja.

Alustusjärjestelmässä kaikki ei ole niin triviaalia

    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-kirjaston toimintaparametrien asettamiseen käytetään suurta sanakirjaa eri asetuksista - config. Suurin osa oletusasetuksista sopii meille, mutta monet asetukset on määritettävä erikseen. Jotta asetuksia ei kirjoitettaisi suoraan koodiin, tallennan ne tekstitiedostoon config.txt. Tämän avulla voit muuttaa koodia asetuksista riippumatta sekä niitata useita identtisiä laitteita eri parametreillä.

Viimeinen koodilohko käynnistää useita korutiinia palvelemaan järjestelmän eri toimintoja. Tässä on esimerkiksi korutiini, joka palvelee laskuria

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

Korutiini odottaa silmukassa uutta laskuriarvoa ja lähettää heti sen ilmestyttyä viestin MQTT-protokollan kautta. Ensimmäinen koodinpätkä lähettää alkuarvon, vaikka vesi ei virtaa laskurin läpi.

Perusluokka MQTTClient palvelee itseään, käynnistää WiFi-yhteyden ja muodostaa yhteyden uudelleen, kun yhteys katkeaa. Kun WiFi-yhteyden tilassa tapahtuu muutoksia, kirjasto ilmoittaa meille soittamalla 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)

Toiminto on rehellisesti kopioitu esimerkeistä. Tässä tapauksessa se laskee katkosten määrän (internet_outages) ja niiden keston. Kun yhteys palautuu, palvelimelle lähetetään joutoaika.

Muuten, viimeinen uni tarvitaan vain toiminnon asynkroniseksi tekemiseksi - kirjastossa sitä kutsutaan via awaitiksi, ja vain ne toiminnot, joiden keho sisältää toisen odotuksen, voidaan kutsua.

WiFi-yhteyden muodostamisen lisäksi sinun on myös muodostettava yhteys MQTT-välittäjään (palvelimeen). Kirjasto tekee myös tämän ja saamme mahdollisuuden tehdä jotain hyödyllistä, kun yhteys on muodostettu

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

Täällä tilaamme useita viestejä - palvelimella on nyt mahdollisuus asettaa nykyiset laskuriarvot lähettämällä vastaava viesti.

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

Tämä toiminto käsittelee saapuvat viestit ja aiheesta (viestin otsikko) riippuen yhden laskurin arvot päivitetään

Pari aputoimintoa

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

Tämä toiminto lähettää viestin, jos yhteys on muodostettu. Jos yhteyttä ei ole, viesti ohitetaan.

Ja tämä on vain kätevä toiminto, joka luo ja lähettää virheenkorjausviestejä.

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

Niin paljon tekstiä, emmekä ole vielä vilkanneet LEDiä. Tässä

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

Olen tarjonnut 2 vilkkumistilaa. Jos yhteys katkeaa (tai sitä ollaan juuri muodostamassa), laite vilkkuu nopeasti. Jos yhteys on muodostettu, laite vilkkuu 5 sekunnin välein. Tässä voidaan tarvittaessa toteuttaa muita vilkkumistapoja.

Mutta LED on vain hemmottelua. Tähtäämme myös näyttöön.

    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)

Tästä puhuin - kuinka yksinkertaista ja kätevää se on korutiinien kanssa. Tämä pieni toiminto kuvaa KOKO käyttäjäkokemusta. Korutiini vain odottaa painikkeen painamista ja käynnistää näytön 3 sekunniksi. Näyttö näyttää nykyiset mittarin lukemat.

Pari pientä asiaa on vielä jäljellä. Tässä on toiminto, joka (uudelleen) käynnistää koko yrityksen. Pääsilmukka lähettää vain erilaisia ​​virheenkorjaustietoja kerran minuutissa. Yleisesti ottaen lainaan sitä sellaisena kuin se on - mielestäni ei ole tarvetta kommentoida liikaa

   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, pari muuta asetusta ja vakiota täydentämään kuvausta

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

Kaikki alkaa näin

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

Jotain tapahtui muistissani

Joten kaikki koodi on siellä. Latasin tiedostot ampy-apuohjelmalla - sen avulla voit ladata ne sisäiseen (itse ESP-07:n) flash-asemaan ja käyttää sitä sitten ohjelmasta tavallisina tiedostoina. Latasin sinne myös käyttämäni mqtt_as-, uasyncio-, ssd1306- ja kokoelmakirjastot (käytetty mqtt_as:n sisällä).

Käynnistämme ja... Saamme MemoryError-ilmoituksen. Lisäksi mitä enemmän yritin ymmärtää, mistä muisti vuotaa, mitä enemmän virheenkorjaustulosteita laitoin, sitä aikaisemmin tämä virhe ilmestyi. Lyhyt Google-haku sai minut ymmärtämään, että mikro-ohjaimessa on periaatteessa vain 30 kB muistia, johon 65 kt koodia (kirjastot mukaan lukien) ei yksinkertaisesti mahdu.

Mutta ulospääsy on olemassa. Osoittautuu, että micropython ei suorita koodia suoraan .py-tiedostosta - tämä tiedosto käännetään ensin. Lisäksi se käännetään suoraan mikro-ohjaimelle, muutetaan tavukoodiksi, joka sitten tallennetaan muistiin. No, jotta kääntäjä toimisi, tarvitset myös tietyn määrän RAM-muistia.

Temppu on säästää mikro-ohjain resursseja vaativalta kääntämiseltä. Voit koota tiedostot suurella tietokoneella ja ladata valmiin tavukoodin mikrokontrolleriin. Tätä varten sinun on ladattava micropython-laiteohjelmisto ja rakennettava mpy-cross-apuohjelma.

En kirjoittanut Makefilea, vaan kävin manuaalisesti läpi ja käänsin kaikki tarvittavat tiedostot (mukaan lukien kirjastot) jotain tällaista

mpy-cross water_counter.py

Jäljelle jää vain ladata tiedostot .mpy-tunnisteella unohtamatta ensin poistaa vastaava .py laitteen tiedostojärjestelmästä.

Tein kaiken kehityksen ohjelmassa (IDE?) ESPlorer. Sen avulla voit ladata skriptejä mikro-ohjaimeen ja suorittaa ne välittömästi. Minun tapauksessani kaikki logiikka ja kaikkien objektien luominen sijaitsee water_counter.py (.mpy) -tiedostossa. Mutta jotta tämä kaikki käynnistyisi automaattisesti, alussa on oltava myös tiedosto nimeltä main.py. Lisäksi sen tulee olla täsmälleen .py, ei esikäännetty .mpy. Tässä on sen triviaali sisältö

import water_counter

Käynnistämme sen - kaikki toimii. Mutta vapaata muistia on hälyttävän vähän - noin 1 kt. Suunnitelmissani on vielä laajentaa laitteen toimivuutta, eikä tämä kilotavu selvästikään riitä minulle. Mutta kävi ilmi, että tähänkin tapaukseen on olemassa ulospääsy.

Tässä on asia. Vaikka tiedostot on käännetty tavukoodiksi ja sijaitsevat sisäisessä tiedostojärjestelmässä, todellisuudessa ne ladataan silti RAM-muistiin ja suoritetaan sieltä. Mutta käy ilmi, että micropython voi suorittaa tavukoodin suoraan flash-muistista, mutta tätä varten sinun on rakennettava se suoraan laiteohjelmistoon. Se ei ole vaikeaa, vaikka se kestikin jonkin aikaa netbookillani (vain siellä minulla oli Linux).

Algoritmi on seuraava:

  • lataa ja asenna ESP Open SDK. Tämä asia kokoaa kääntäjän ja kirjastot ohjelmille ESP8266:lle. Koottu projektin etusivun ohjeiden mukaan (valitsin asetukseksi STANDALONE=yes)
  • Lataa micropython-lajit
  • Sijoita tarvittavat kirjastot ports/esp8266/modules micropython-puun sisään
  • Kokoamme laiteohjelmiston tiedoston ohjeiden mukaan ports/esp8266/README.md
  • Lataamme laiteohjelmiston mikro-ohjaimeen (teen tämän Windowsissa ESP8266Flasher-ohjelmilla tai Python esptoolilla)

Siinä kaikki, nyt 'import ssd1306' nostaa koodin suoraan laiteohjelmistosta, eikä RAM-muistia kuluteta tähän. Tällä temppulla latasin vain kirjastokoodin laiteohjelmistoon, kun taas pääohjelmakoodi suoritetaan tiedostojärjestelmästä. Näin voit helposti muokata ohjelmaa ilman laiteohjelmiston uudelleenkääntämistä. Tällä hetkellä minulla on noin 8.5 kb vapaata RAM-muistia. Näin voimme ottaa käyttöön melko paljon erilaisia ​​hyödyllisiä toimintoja tulevaisuudessa. No, jos muistia ei ole tarpeeksi, voit työntää pääohjelman laiteohjelmistoon.

Joten mitä meidän pitäisi tehdä asialle nyt?

Ok, laitteisto juotettu, laiteohjelmisto on kirjoitettu, laatikko painettu, laite on juuttunut seinään ja vilkuttaa iloisesti hehkulamppua. Mutta toistaiseksi se on musta laatikko (kirjaimellisesti ja kuvaannollisesti) ja siitä on vielä vähän hyötyä. On aika tehdä jotain palvelimelle lähetettäville MQTT-viesteille.

"Älykäs kotini" pyörii Majordomo järjestelmä. MQTT-moduuli joko tulee ulos pakkauksesta tai asennetaan helposti lisäosien markkinoilta – en muista mistä sen hankin. MQTT ei ole omavarainen asia - tarvitset ns. välittäjä - palvelin, joka vastaanottaa, lajittelee ja välittää MQTT-viestejä asiakkaille. Käytän mosquittoa, joka (kuten majordomo) toimii samalla netbookilla.

Kun laite on lähettänyt viestin vähintään kerran, arvo näkyy heti luettelossa.

Yhdistämme vesimittarin älykotiin

Nämä arvot voidaan nyt liittää järjestelmäobjekteihin, niitä voidaan käyttää automaatiokomentosarjassa ja kohdistaa erilaisiin analyyseihin - kaikki tämä ei kuulu tämän artikkelin soveltamisalaan. Voin suositella majordomo-järjestelmää kaikille kiinnostuneille kanava Electronics In Lens — ystävä rakentaa myös älykotia ja puhuu selkeästi järjestelmän käyttöönotosta.

Näytän vain muutaman kaavion. Tämä on yksinkertainen kaavio päivittäisistä arvoista

Yhdistämme vesimittarin älykotiin
Voidaan nähdä, että melkein kukaan ei käyttänyt vettä yöllä. Pari kertaa joku kävi wc:ssä ja näyttää siltä, ​​että käänteisosmoosisuodatin imee pari litraa per yö. Aamulla kulutus kasvaa huomattavasti. Käytän yleensä vettä kattilasta, mutta sitten halusin käydä kylvyssä ja vaihdoin tilapäisesti kaupungin kuumaan veteen - tämä näkyy myös selvästi alakaaviossa.

Tästä kaaviosta opin, että wc:ssä käynti vaatii 6-7 litraa vettä, suihkussa käynti 20-30 litraa, astioiden pesu noin 20 litraa ja kylvyssä 160 litraa. Perheeni kuluttaa noin 500-600 litraa päivässä.

Erityisen uteliaat voivat katsoa kunkin yksittäisen arvon tietueita

Yhdistämme vesimittarin älykotiin

Sieltä opin, että kun hana on auki, vesi virtaa noin 1 litran nopeudella 5 sekunnissa.

Mutta tässä muodossa tilastoja ei todennäköisesti ole kovin kätevää tarkastella. Majordomolla on myös mahdollisuus tarkastella kulutuskaavioita päivä-, viikko- ja kuukausikohtaisesti. Tässä on esimerkiksi kulutuskaavio pylväinä

Yhdistämme vesimittarin älykotiin

Toistaiseksi minulla on tietoja vain viikolta. Kuukauden kuluttua tämä kaavio on suuntaa-antavampi - jokaisella päivällä on erillinen sarake. Kuvaa pilaavat hieman manuaalisesti syöttämieni arvojen säädöt (suurin sarake). Eikä ole vielä selvää, asetinko ensimmäiset arvot väärin, melkein kuution vähemmän, vai onko tämä laiteohjelmiston virhe eikä kaikkia litroja ole laskettu. Tarvitsee enemmän aikaa.

Graafit itsessään vaativat vielä taikuutta, valkaisua, maalausta. Ehkä rakennan myös kaavion muistinkulutuksesta virheenkorjausta varten - jos sieltä jotain vuotaa. Ehkä näytän jotenkin ajanjaksot, jolloin Internetiä ei ollut. Toistaiseksi tämä kaikki on ideoiden tasolla.

Johtopäätös

Tänään asunnostani on tullut hieman älykkäämpi. Tällaisella pienellä laitteella minun on helpompi seurata veden kulutusta talossa. Jos aiemmin olin närkästynyt "taas, kulutimme paljon vettä kuukaudessa", nyt voin löytää tämän kulutuksen lähteen.

Joillekin saattaa olla outoa katsoa lukemia näytöltä, jos se on metrin päässä itse mittarista. Mutta ei kovin kaukaisessa tulevaisuudessa aion muuttaa toiseen asuntoon, jossa on useita veden nousuputkia, ja itse mittarit sijaitsevat todennäköisesti tasanteella. Joten etälukulaite on erittäin hyödyllinen.

Aion myös laajentaa laitteen toimivuutta. Katselen jo moottoroituja venttiilejä. Nyt vaihtaakseni kattilan kaupunkiveteen, minun on käännettävä 3 hanaa vaikeapääsyisessä kapeassa. Olisi paljon kätevämpää tehdä tämä yhdellä painikkeella, jossa on vastaava merkintä. No, tietenkin, kannattaa toteuttaa suojaus vuotoja vastaan.

Artikkelissa kuvailin versioni laitteesta, joka perustuu ESP8266:een. Mielestäni keksin erittäin mielenkiintoisen version micropython-laiteohjelmistosta korutiinien avulla - yksinkertainen ja mukava. Yritin kuvata monia vivahteita ja puutteita, joita kohtasin kampanjan aikana. Ehkä kuvailin kaiken liian yksityiskohtaisesti; henkilökohtaisesti lukijana minun on helpompi ohittaa turhat asiat kuin miettiä myöhemmin, mitä jäi sanomatta.

Kuten aina, olen avoin rakentavalle kritiikille.

Lähdekoodi
Piiri ja piirilevy
Kotelomalli

Lähde: will.com

Lisää kommentti