Ni konektas la akvomezurilon al la inteligenta hejmo

Iam, hejmaj aŭtomatigaj sistemoj, aŭ kiel oni ofte nomas ilin "inteligenta hejmo", estis terure multekostaj kaj nur riĉuloj povis pagi ilin. Hodiaŭ sur la merkato vi povas trovi sufiĉe buĝetajn ilojn kun sensiloj, butonoj / ŝaltiloj kaj aktuarioj por kontroli lumigadon, ingojn, ventoladon, akvoprovizadon kaj aliajn konsumantojn. Kaj eĉ la plej kurba DIY-shnik povas aliĝi al la beleco kaj kunmeti aparatojn por inteligenta hejmo por malmultekosta prezo.

Ni konektas la akvomezurilon al la inteligenta hejmo

Kiel regulo, la proponitaj aparatoj estas aŭ sensiloj aŭ aktuarioj. Ili faciligas efektivigi scenarojn kiel "kiam la movsensilo estas ekigita, ŝaltu la lumon" aŭ "la ŝaltilo proksime de la elirejo malŝaltas la lumon en la tuta apartamento." Sed iel ĝi ne funkciis kun telemetrio. En la plej bona kazo, ĉi tio estas grafikaĵo de temperaturo kaj humideco, aŭ tuja potenco en aparta ellasejo.

Mi lastatempe instalis akvomezurilon kun pulsa eligo. Tra ĉiu litro, kiu trapasis la vendotablo, la junŝaltilo estas aktivigita kaj fermas la kontakton. Restas nur alkroĉi al la dratoj kaj provi iom da profito el ĝi. Ekzemple, analizu akvokonsumon laŭ horoj kaj tagoj de la semajno. Nu, se estas pluraj leviloj por akvo en la apartamento, tiam estas pli oportune vidi ĉiujn nunajn indikilojn sur unu ekrano ol grimpi malfacile atingeblajn niĉojn per poŝlampo.

Sub la tranĉo, mia versio de aparato bazita sur ESP8266, kiu kalkulas pulsojn de akvomezuriloj kaj sendas legadojn al la inteligenta hejma servilo per MQTT. Ni programos en micropython uzante la uasyncio-bibliotekon. Kreante la firmware, mi renkontis plurajn interesajn malfacilaĵojn, kiujn mi ankaŭ diskutos en ĉi tiu artikolo. Iru!

Skemo

Ni konektas la akvomezurilon al la inteligenta hejmo

La koro de la tuta cirkvito estas modulo sur la mikroregilo ESP8266. ESP-12 estis origine planita, sed mia montriĝis misa. Mi devis kontentiĝi kun la ESP-07-modulo, kiu estis disponebla. Feliĉe, ili estas samaj kaj laŭ konkludoj kaj funkcioj, la sola diferenco estas en la anteno - la ESP-12 havas ĝin enkonstruita, dum la ESP-07 havas eksteran. Tamen, eĉ sen WiFi-anteno, la signalo en mia banĉambro estas kaptita normale.

La ligado de la modulo estas norma:

  • restarigi butonon kun tirilo kaj kondensilo (kvankam ambaŭ jam estas ene de la modulo)
  • La ebliga signalo (CH_PD) estas tirita supren al potenco
  • GPIO15 tirita al tero. Ĉi tio necesas nur ĉe la komenco, sed mi ankoraŭ ne bezonas alkroĉiĝi al ĉi tiu kruro plu

Por translokigi la modulon al la firmware-reĝimo, vi devas fermi GPIO2 al la grundo, kaj por igi ĝin pli oportuna, mi disponigis la Butonon Boot. En la normala stato, ĉi tiu pinglo estas tirita supren al potenco.

La stato de la linio GPIO2 estas kontrolita nur komence de operacio - kiam potenco estas aplikata aŭ tuj post restarigo. Do la modulo aŭ ekfunkciigas kiel kutime, aŭ iras en firmware-reĝimon. Post kiam ŝarĝita, ĉi tiu pinglo povas esti uzata kiel regula GPIO. Nu, ĉar jam estas butono tie, vi povas pendigi sur ĝi ian utilan funkcion.

Por programado kaj senararigado, mi uzos la UART, kiun mi alportis al la kombilo. Kiam necese, mi simple konektas tie USB-UART-adaptilon. Vi nur bezonas memori, ke la modulo funkcias per 3.3V. Se vi forgesas ŝanĝi la adaptilon al ĉi tiu tensio kaj apliki 5V, tiam la modulo plej verŝajne forbrulos.

Mi ne havas problemojn pri elektro en la banĉambro - la ellasejo situas proksimume unu metron de la metroj, do mi elektigos ĝin de 220V. Kiel energifonto, mi havos malgrandan bloko HLK-PM03 de Tenstar Roboto. Persone, mi malfacilas kun analoga kaj elektra elektroniko, kaj jen preta elektroprovizo en malgranda kazo.

Por signali la operaciajn modojn, mi disponigis LED-on konektitan al GPIO2. Tamen mi ne soldis ĝin, ĉar. la ESP-07-modulo jam havas LED-on konektitan al la sama GPIO2. Sed lasu ĝin esti sur la tabulo - subite mi volas alporti ĉi tiun LED al la kazo.

Ni transiru al la plej interesa. Akvomezuriloj ne havas logikon, oni ne povas peti ilin pri aktualaj legaĵoj. La nura aĵo, kiu estas disponebla por ni, estas impulsoj - fermante la kontaktojn de la junŝaltilo ĉiun litron. Mi havas la reed-ŝaltilojn en GPIO12 / GPIO13. Mi enŝaltos la tirreziston programe ene de la modulo.

Komence, mi forgesis provizi rezistilojn R8 kaj R9 kaj ili ne estas en mia versio de la tabulo. Sed ĉar mi jam elmetas la skemon por ke ĉiuj vidu, indas korekti ĉi tiun superrigardon. Rezistoj estas bezonataj por ne bruligi la havenon se la firmvaro estas buga kaj metas unuon sur la pinglo, kaj la junŝaltilo mallongigas ĉi tiun linion al grundo (kun rezistilo, maksimume 3.3V / 1000Ω = 3.3mA fluos) .

Estas tempo pensi pri kion fari se la elektro malŝaltas. La unua opcio estas peti la servilon pri la komencaj valoroj de la nombriloj ĉe la komenco. Sed ĉi tio postulus gravan komplikaĵon de la interŝanĝa protokolo. Krome, la agado de la aparato en ĉi tiu kazo dependas de la stato de la servilo. Se post malŝalto de la lumo la servilo ne komenciĝus (aŭ komenciĝis poste), tiam la akvomezurilo ne povus peti la komencajn valorojn kaj funkcius malĝuste.

Tial mi decidis efektivigi la stokadon de nombrilaj valoroj en memorpeceto konektita per I2C. Mi ne havas specialajn postulojn por la grandeco de fulmmemoro - vi devas konservi nur 2 nombrojn (la nombro da litroj laŭ varma kaj malvarma akvomezuriloj). Eĉ la plej malgranda modulo faros. Sed vi devas atenti la nombron da skribcikloj. Por plej multaj moduloj, ĉi tio estas 100 mil cikloj, por iuj ĝis miliono.

Ŝajnus, ke miliono estas multe. Sed dum 4 jaroj da loĝado en mia loĝejo, mi konsumis iom pli ol 500 kubmetrojn da akvo, tio estas 500 mil litroj! Kaj 500 mil diskoj en fulmo. Kaj tio estas nur malvarma akvo. Vi povas, kompreneble, reludi la blaton ĉiun duan jaron, sed montriĝis, ke ekzistas FRAM-blatoj. El programa vidpunkto, ĉi tiu estas la sama I2C EEPROM, nur kun tre granda nombro da reverkaj cikloj (cent milionoj). Tio estas nur ĝis mi ankoraŭ ne povas atingi vendejon kun tiaj mikrocirkvitoj, do nuntempe la kutima 24LC512 staros.

Presita cirkvito

Komence, mi planis fari tabulon hejme. Tial, la tabulo estis desegnita kiel unuflanka. Sed pasiginte unu horon kun lasera fero kaj lutmasko (ĝi iel ne estas comme il faut sen ĝi), mi tamen decidis mendi tabulojn de la ĉinoj.

Ni konektas la akvomezurilon al la inteligenta hejmo

Preskaŭ antaŭ ol mendi la tabulon, mi rimarkis, ke krom la fulmmemora blato, vi povas kunligi ion alian utilan al la I2C-buso, ekzemple ekranon. Kion precize eligi ĝin ankoraŭ estas demando, sed vi devas bredi ĝin sur la tabulo. Nu, ĉar mi intencis mendi tabulojn ĉe la fabriko, tute ne servis min limigi al unuflanka tabulo, do la I2C-linioj estas la solaj sur la malantaŭo de la tabulo.

Unu granda jambo ankaŭ estis ligita kun la unudirekta drataro. Ĉar la tabulo estis desegnita unuflanka, tiam la spuroj kaj SMD-komponentoj estis planitaj por esti metitaj unuflanke, kaj la eligkomponentoj, konektiloj kaj elektroprovizo sur la alia. Kiam mi ricevis la tabulojn monaton poste, mi forgesis pri la originala plano kaj lutis ĉiujn komponantojn ĉe la antaŭa flanko. Kaj nur kiam temis pri lutado de la nutrado, montriĝis, ke la pluso kaj minus eksedziĝis inverse. Mi devis farmi per saltantoj. En la supra bildo, mi jam ŝanĝis la kablon, sed la grundo estas translokigita de unu parto de la tabulo al alia tra la pingloj de la butono Boot (kvankam eblus desegni trakon sur la dua tavolo).

Ĝi rezultis tiel

Ni konektas la akvomezurilon al la inteligenta hejmo

Loĝado

La sekva paŝo estas la korpo. Se vi havas 3D-presilon, ĉi tio ne estas problemo. Mi ne multe ĝenis - mi nur desegnis skatolon de la ĝusta grandeco kaj faris eltranĉaĵojn en la ĝustaj lokoj. La kovrilo estas fiksita al la korpo per malgrandaj memfrapaj ŝraŭboj.

Ni konektas la akvomezurilon al la inteligenta hejmo

Mi jam menciis, ke la butono Boot povas esti uzata kiel ĝeneraluzebla butono - do ni alportu ĝin al la antaŭa panelo. Por fari tion, mi desegnis specialan "puton" kie la butono vivas.

Ni konektas la akvomezurilon al la inteligenta hejmo

Estas ankaŭ stumpoj ene de la kazo, sur kiuj la tabulo estas instalita kaj fiksita per ununura M3-ŝraŭbo (ne estis pli da spaco sur la tabulo)

La ekrano jam estis elektita kiam mi presis la unuan taŭgan version de la kazo. Norma dulinia presilo ne konvenis al ĉi tiu kazo, sed en la fundo de la barelo estis OLED-ekrano SSD1306 128 × 32. Ĝi estas malgranda, sed mi ne rigardas lin ĉiutage - ĝi ruliĝos.

Taksante ĉi tiel kaj tiel, kiel la dratoj estos metitaj de ĝi, mi decidis alglui la ekranon en la mezo de la kazo. Ergonomio, kompreneble, sub la soklo - la butono estas supre, la ekrano estas malsupre. Sed mi jam diris, ke la ideo ŝraŭbi la ekranon venis tro malfrue kaj mi estis tro mallaborema rekabligi la tabulon por movi la butonon.

Kunvenita aparato. La montra modulo estas gluita al la muko per varma gluo

Ni konektas la akvomezurilon al la inteligenta hejmo

Ni konektas la akvomezurilon al la inteligenta hejmo

La fina rezulto videblas ĉe KDPV

Firmaro

Ni transiru al la programaro parto. Por tiaj malgrandaj metioj, mi tre ŝatas uzi la Python-lingvon (mikropitono) - la kodo estas tre kompakta kaj komprenebla. Feliĉe, ne necesas malsupreniri al la nivelo de registroj por elpremi mikrosekundojn - ĉio povas esti farita de python.

Ŝajnas, ke ĉio estas simpla, sed ne tre - la aparato havas plurajn sendependajn funkciojn:

  • La uzanto frapetas butonon kaj rigardas la ekranon
  • Litroj markas kaj ĝisdatigas valorojn en fulmmemoro
  • La modulo monitoras la WiFi-signalon kaj rekonektas se necese
  • Nu, sen palpebruma ampolo, vi tute ne povas

Vi ne povas supozi, ke unu funkcio ne funkciis se la alia ial malsukcesas. Mi jam manĝis kaktojn en aliaj projektoj kaj nun mi ankoraŭ vidas erarojn kiel "maltrafis alian litron ĉar la ekrano ĝisdatiĝis en tiu momento" aŭ "la uzanto ne povas fari ion ajn dum la modulo konektas al WiFi". Kompreneble, iuj aferoj povas esti faritaj per interrompoj, sed vi povas renkonti limigon pri daŭro, nestado de vokoj aŭ neatoma ŝanĝo de variabloj. Nu, la kodo, kiu faras ĉion kaj tuj rapide fariĝas malordo.

В pli serioza projekto Mi uzis klasikan preventan multitasking kaj FreeRTOS, sed en ĉi tiu kazo, la modelo montriĝis multe pli taŭga. coroutines kaj uasync bibliotekoj . Plie, la efektivigo de Python de korutinoj estas nur bombo - ĉio estas farita simple kaj oportune por la programisto. Nur skribu vian propran logikon, nur diru al mi kie vi povas ŝanĝi inter fadenoj.

Mi proponas studi la diferencojn inter preventa kaj konkurenciva multitasking kiel laŭvola. Nun ni finfine atingu la kodon.

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

Ĉiu nombrilo estas pritraktata de okazo de la klaso Counter. Antaŭ ĉio, la komenca valoro de la nombrilo estas subtrahita de la EEPROM (value_storage) - jen kiel reakiro post elektropaneo estas efektivigita.

La stifto estas pravigita per enkonstruita tiriĝo al la elektroprovizo: se la kanŝaltilo estas fermita, la linio estas nul, se la linio estas malfermita, ĝi estas tirita supren al la elektroprovizo kaj la regilo legas unu.

Ankaŭ, aparta tasko estas lanĉita ĉi tie, kiu balotos la pinglo. Ĉiu nombrilo plenumos sian propran taskon. Jen ŝia kodo

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

Prokrasto de 25ms necesas por filtri la resalton de kontaktoj, kaj samtempe ĝi reguligas kiom ofte la tasko vekiĝas (dum ĉi tiu tasko dormas, aliaj taskoj funkcias). Ĉiu 25ms, la funkcio vekiĝas, kontrolas la pinglo, kaj se la kanaj ŝaltilkontaktoj estas fermitaj, tiam alia litro pasis tra la vendotablo kaj ĉi tio devas esti prilaborita.

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

        self._value_storage.write(self._value)

Prilaborado de la sekva litro estas bagatela - la nombrilo nur pliiĝas. Nu, estus bone skribi novan valoron al USB-memorilo.

Por facileco de uzo, "akcesoraĵoj" estas provizitaj.

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

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

Nu, nun ni uzu la ĉarmojn de python kaj la uasync-bibliotekon kaj faru la nombrilon atendebla (kiel mi povas traduki ĝin en la rusan? Tiun, kiun oni povas atendi?)

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

        return self.value()

    __iter__ = __await__  

Ĉi tio estas tiel oportuna funkcio, kiu atendas ĝis la nombrilo valoro estas ĝisdatigita - la funkcio vekiĝas de tempo al tempo kaj kontrolas la _value_changed flago. La lertaĵo de ĉi tiu funkcio estas, ke la alvokodo povas endormiĝi dum voko al ĉi tiu funkcio kaj dormi ĝis nova valoro estas ricevita.

Sed kio pri interrompoj?Jes, ĉi-momente vi povas troli min, dirante, ke li mem diris pri interrompoj, sed fakte li aranĝis stultan pinglobon. Fakte interrompoj estas la unua afero, kiun mi provis. En la ESP8266, vi povas organizi interrompon ĉe la fronto, kaj eĉ skribi interrompan prizorganton por ĉi tiu interrompo en python. En ĉi tiu interrompo, vi povas ĝisdatigi la valoron de variablo. Verŝajne, ĉi tio sufiĉus, se la nombrilo estus sklava aparato - tiu, kiu atendas ĝis oni petas ĝin pri ĉi tiu valoro.

Bedaŭrinde (aŭ feliĉe?), mia aparato estas aktiva, ĝi mem sendu mesaĝojn per la protokolo MQTT kaj skribu datumojn al EEPROM. Kaj ĉi tie jam venas limigoj - vi ne povas asigni memoron en interrompoj kaj uzi grandan stakon, kio signifas, ke vi povas forgesi sendi mesaĝojn tra la reto. Estas bulkoj kiel micropython.schedule (), kiuj ebligas al vi ruli ian funkcion "tuj kaj tuj", sed la demando estiĝas "kio estas la signifo?". Subite ni sendas ian mesaĝon nun, kaj tiam interrompo ekas kaj ruinigas la valorojn de variabloj. Aŭ, ekzemple, nova nombrila valoro alvenis de la servilo dum ni ankoraŭ ne registris la malnovan. Ĝenerale, vi devas bloki sinkronigon aŭ eliri iel alimaniere.

Kaj de tempo al tempo RuntimeError: plani stakon plenajn kraŝojn kaj kiu scias kial?

Kun eksplicita balotado kaj uasync, en ĉi tiu kazo, ĝi iel montriĝas pli bela kaj pli fidinda.

Mi elprenis laboron kun EEPROM en malgranda klaso

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)

Estas malfacile labori kun bajtoj rekte en Python, kaj estas bajtoj kiuj estas skribitaj en memoron. Mi devis bari la konvertiĝon inter entjero kaj bajtoj uzante la ustruct-bibliotekon.

Por ne ĉiufoje transdoni la I2C-objekton kaj la adreson de la memorĉelo, mi envolvis ĉion en malgranda kaj oportuna klasikaĵo.

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)

La I2C objekto mem estas kreita kun ĉi tiuj parametroj

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

Ni alproksimiĝas al la plej interesa - la efektivigo de komunikado kun la servilo per MQTT. Nu, vi ne bezonas efektivigi la protokolon mem - mi trovis ĝin en la interreto preta nesinkrona efektivigo. Ĉi tie ni uzos ĝin.

Ĉio plej interesa estas kolektita en la klaso CounterMQTTClient, kiu baziĝas sur la biblioteko MQTTClient. Ni komencu per la periferio

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

Ĉi tie oni kreas kaj agordas ampolon kaj butonpinglojn, same kiel objektojn de malvarma kaj varma akvomezurilo.

Kun komencado, ne ĉio estas tiel bagatela

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

Por agordi la parametrojn de la biblioteko mqtt_as, granda vortaro de malsamaj agordoj estas uzata - config. La plej multaj el la defaŭltaj agordoj funkcias por ni, sed multaj agordoj devas esti agordita eksplicite. Por ne preskribi la agordojn rekte en la kodo, mi konservas ilin en tekstdosiero config.txt. Ĉi tio ebligas al vi ŝanĝi la kodon sendepende de la agordoj, kaj ankaŭ niti plurajn identajn aparatojn kun malsamaj parametroj.

La lasta bloko de kodo komencas plurajn korutinojn por servi diversajn sistemajn funkciojn. Jen ekzemplo de korutino, kiu servas nombrilojn

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

La korutino atendas en buklo novan nombrilon, kaj tuj kiam ĝi aperas, ĝi sendas mesaĝon per la MQTT-protokolo. La unua peco de kodo sendas la komencan valoron eĉ se ne estas akvo fluanta tra la nombrilo.

La baza klaso MQTTClient servas sin, iniciatas WiFi-konekton kaj rekonektas kiam la konekto estas perdita. Kiam la stato de la WiFi-konekto ŝanĝiĝas, la biblioteko informas nin vokante 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)

La funkcio estas honeste lekita de la ekzemploj. En ĉi tiu kazo, ĝi kalkulas la nombron da malfunkcioj (internet_outages) kaj ilian daŭron. Kiam la konekto estas restarigita, malaktiva tempo estas sendita al la servilo.

Cetere, la lasta dormo estas bezonata nur por ke la funkcio fariĝu nesinkrona - en la biblioteko ĝi estas vokita per await, kaj nur funkcioj en kies korpo estas alia await povas esti vokita.

Krom konekti al WiFi, vi ankaŭ devas establi konekton kun la MQTT-broker (servilo). Ĉi tion ankaŭ faras la biblioteko, kaj ni ricevas la ŝancon fari ion utilan kiam la konekto estas establita

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

Ĉi tie ni abonas plurajn mesaĝojn - la servilo nun havas la kapablon agordi la aktualajn valorojn de la nombriloj sendante la taŭgan mesaĝon.

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

Ĉi tiu funkcio prilaboras envenantajn mesaĝojn, kaj depende de la temo (mesaĝnomo), la valoroj de unu el la nombriloj estas ĝisdatigitaj.

Paro da helpaj funkcioj

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

Ĉi tiu funkcio respondecas pri sendado de mesaĝo se la konekto estas establita. Se ne ekzistas konekto, la mesaĝo estas ignorita.

Kaj ĉi tio estas nur oportuna funkcio, kiu generas kaj sendas sencimigajn mesaĝojn.

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

Tiom da teksto kaj ni ankoraŭ ne palpebrumis la LED. Jen

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

Mi disponigis 2 reĝimojn de palpebrumado. Se la konekto estas perdita (aŭ ĝi ĵus estas establita), tiam la aparato palpebrumas rapide. Se la konekto estas establita, la aparato palpebrumas ĉiujn 5 sekundojn. Se necese, aliaj reĝimoj de palpebrumado povas esti efektivigitaj ĉi tie.

Sed la LED estas tiel, dorloto. Ni ankaŭ svingiĝis ĉe la ekrano.

    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)

Jen kion mi parolis - kiel simple kaj oportune ĝi estas kun korutinoj. Ĉi tiu eta funkcio priskribas ĈIUJN uzantinterago. La korutino nur atendas, ke la butono estu premata kaj ŝaltas la ekranon dum 3 sekundoj. La ekrano montras la aktualajn mezurilojn.

Restas ankoraŭ kelkaj etaj aferoj. Jen la funkcio, kiu (re)starigas ĉi tiun tutan ekonomion. La ĉefa buklo nur zorgas pri sendado de diversaj sencimigaj informoj unufoje ĉiuminute. Ĝenerale, mi donas ĝin kiel ĝi estas - mi ne bezonas komenti specife, mi pensas

   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)

Nu, kelkaj pliaj agordoj kaj konstantoj por kompleteco de priskribo

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

Ĉio komenciĝas tiel

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

Io okazis al mia memoro

Do, la tuta kodo estas tie. Mi alŝutis la dosierojn per la ampy ilo - ĝi permesas al vi alŝuti ilin al la interna (tiu en la ESP-07 mem) poŝmemoro kaj poste aliri ĝin de la programo kiel normalaj dosieroj. Tie mi ankaŭ alŝutis la bibliotekojn mqtt_as, uasyncio, ssd1306 kaj kolektojn, kiujn mi uzis (uzis ene de mqtt_as).

Ni komencas kaj... Ni ricevas MemoryError. Cetere, ju pli mi provis kompreni precize kie la memoro likas, des pli mi sencimis la presaĵojn, des pli frue ĉi tiu eraro okazis. Mallonga guglo igis min kompreni, ke en la mikroregilo principe estas nur 30 kb da memoro, en kiu 65 kb da kodo (kune kun bibliotekoj) neniel taŭgas.

Sed estas elirejo. Rezultas, ke mikropython ne efektivigas kodon rekte de .py-dosiero - ĉi tiu dosiero unue estas kompilita. Krome, ĝi estas kompilita rekte sur la mikroregilo, iĝas bajtokodo, kiu tiam estas konservita en memoro. Nu, la kompililo ankaŭ bezonas certan kvanton da RAM por funkcii.

La lertaĵo estas savi la mikroregilon de rimed-intensa kompilo. Vi povas kompili dosierojn sur granda komputilo, kaj alŝuti pretan bajtkodon al la mikroregilo. Por fari tion, vi devas elŝuti la mikropytonan firmware kaj konstrui mpy-cross utileco.

Mi ne skribis Makefile, sed mane trarigardis kaj kompilis ĉiujn necesajn dosierojn (inkluzive de bibliotekoj) tiel.

mpy-cross water_counter.py

Restas nur plenigi la dosierojn kun la etendo .mpy, memorante unue forigi la respondajn dosierojn .py el la dosiersistemo de la aparato.

Mi faris la tutan evoluon en la programo (IDE?) ESPlorer. Ĝi permesas al vi alŝuti skriptojn al la mikroregilo kaj tuj ekzekuti ilin. En mia kazo, la tuta logiko kaj la kreado de ĉiuj objektoj troviĝas en la dosiero water_counter.py (.mpy). Sed por ke ĉio ĉi komenciĝu aŭtomate ĉe la komenco, ankaŭ devas esti dosiero nomata main.py. Plie, ĝi devas esti ĝuste .py, kaj ne antaŭkompilita .mpy. Jen ĝia bagatela enhavo

import water_counter

Ni komencas - ĉio funkcias. Sed libera memoro estas minace malgranda - ĉirkaŭ 1kb. Mi ankoraŭ havas planojn vastigi la funkciecon de la aparato, kaj ĉi tiu kilobajto evidente ne sufiĉos por mi. Sed montriĝis, ke estas elirejo.

La punkto estas ĉi tio. Kvankam la dosieroj estas kompilitaj en bajtkodon kaj loĝas en la interna dosiersistemo, ili estas fakte ŝarĝitaj en RAM kaj ekzekutitaj de tie ĉiuokaze. Sed rezultas, ke mikropitono povas ekzekuti bajtokodon rekte el fulmmemoro, sed por tio vi devas enkonstrui ĝin rekte en la firmware. Ne malfacilas, kvankam necesis deca tempo ĉe mia netbook (nur tie mi havis Linukson).

La algoritmo estas kiel sekvas:

  • Elŝutu kaj instalu ESP Open SDK. Ĉi tiu afero konstruas kompililon kaj bibliotekojn por programoj sub la ESP8266. Ĝi estas kunvenita laŭ la instrukcioj en la ĉefpaĝo de la projekto (mi elektis la agordon STANDALONE=jes)
  • Elŝuti mikropitonaj specoj
  • Ĵetu la necesajn bibliotekojn en ports/esp8266/modules en la mikropitonan arbon
  • Ni kolektas la firmware laŭ la instrukcioj en la dosiero ports/esp8266/README.md
  • Alŝutu la firmvaro al la mikroregilo (mi faras ĝin en Vindozo uzante la programojn ESP8266Flasher aŭ la esptool de Python)

Ĉio, nun 'import ssd1306' levos la kodon rekte de la firmvaro kaj RAM ne estos elspezita por ĉi tio. Kun ĉi tiu lertaĵo, mi alŝutis nur la bibliotekan kodon al la firmvaro, dum la ĉefa programkodo estas ekzekutita de la dosiersistemo. Ĉi tio faciligas modifi la programon sen rekompili la firmware. Nuntempe, mi havas ĉirkaŭ 8.5kb da RAM libera. Ĉi tio permesos al ni efektivigi sufiĉe multajn malsamajn utilajn funkciojn en la estonteco. Nu, se tute ne estas sufiĉe da memoro, tiam vi povas puŝi la ĉefan programon en la firmware.

Kaj kion fari kun ĝi nun?

Bone, la ferpeco estas lutita, la firmvaro estas skribita, la skatolo estas presita, la aparato estas fiksita sur la muro kaj la lumo palpebrumas feliĉe. Sed ĝis nun ĉi ĉio estas nigra skatolo (laŭvorte kaj figure) kaj estas ankoraŭ malmulte da senco de ĝi. Estas tempo fari ion kun la MQTT-mesaĝoj, kiuj estas senditaj al la servilo.

Mia "saĝa hejmo" turniĝas Majordomo sistemo. La MQTT-modulo estas aŭ el la skatolo, aŭ facile instalita de la aldonaĵa merkato - mi ne memoras de kie ĝi venis. MQTT ne estas memsufiĉa afero - vi bezonas tn. broker - servilo kiu akceptas, ordigas kaj plusendas mesaĝojn al MQTT-klientoj. Mi uzas moskiton, kiu (kiel majordomo) funkcias per la sama netbook.

Post kiam la aparato sendas mesaĝon almenaŭ unufoje, la valoro tuj aperos en la listo.

Ni konektas la akvomezurilon al la inteligenta hejmo

Ĉi tiuj valoroj nun povas esti asociitaj kun sistemaj objektoj, ili povas esti uzataj en aŭtomatigaj skriptoj kaj submetitaj al diversaj analizoj - ĉio ĉi estas ekster la amplekso de ĉi tiu artikolo. Kiu interesiĝas pri la majordomo-sistemo, mi povas rekomendi Kanala Elektroniko En Lenso - amiko ankaŭ konstruas inteligentan hejmon kaj kompreneble parolas pri agordo de la sistemo.

Mi nur montros al vi kelkajn grafikaĵojn. Ĉi tio estas simpla grafiko de valoroj tage

Ni konektas la akvomezurilon al la inteligenta hejmo
Oni povas vidi, ke preskaŭ neniu uzis la akvon nokte. Kelkfoje iu iris al la necesejo, kaj ŝajnas, ke la inversa osmoza filtrilo suĉas kelkajn litrojn nokte. Matene la konsumo signife pliiĝas. Mi kutime uzas akvon el la kaldrono, sed tiam mi volis bani min kaj provizore ŝanĝis al urba varma akvo - tio ankaŭ estas klare videbla en la malsupra grafikaĵo.

De ĉi tiu diagramo, mi eksciis, ke iri al la necesejo estas 6-7 litroj da akvo, duŝo estas 20-30 litroj, lavi telerojn estas ĉirkaŭ 20 litroj, kaj baniĝi postulas 160 litrojn. Dum la tago, mia familio konsumas ie ĉirkaŭ 500-600l.

Por tiuj, kiuj estas precipe scivolemaj, vi povas rigardi la rekordojn por ĉiu individua valoro.

Ni konektas la akvomezurilon al la inteligenta hejmo

De ĉi tie mi eksciis, ke kiam la krano estas malfermita, akvo fluas kun rapideco de ĉirkaŭ 1 litro en 5 sekundoj.

Sed en ĉi tiu formo, la statistikoj verŝajne ne estas tre oportune rigardi. majordomo ankaŭ havas la kapablon vidi konsumdiagramojn laŭ tago, semajno kaj monato. Jen, ekzemple, grafikaĵo de konsumo en kolumnoj

Ni konektas la akvomezurilon al la inteligenta hejmo

Ĝis nun mi havas nur unu semajnon da datumoj. Post unu monato, ĉi tiu grafiko estos pli malkaŝa - aparta kolumno respondos al ĉiu tago. La bildo estas iomete difektita de la ĝustigoj de la valoroj, kiujn mi enigas permane (la plej granda kolumno). Kaj ankoraŭ ne estas klare, ĉu mi malĝuste fiksis la plej unuajn valorojn preskaŭ unu kubon malpli, aŭ ĉu ĉi tio estas cimo en la firmware kaj ne ĉiuj litroj estis konsiderataj. Bezonas pli da tempo.

Super la grafikaĵoj mem, vi ankoraŭ bezonas elvoki, blankigi, pentri. Eble mi ankaŭ konstruos grafeon de memorkonsumo por sencimigaj celoj - subite io likas tie. Eble mi iel montros la periodojn, kiam ne ekzistis Interreto. Dum ĉio ĉi turniĝas je la nivelo de la ideo.

konkludo

Hodiaŭ mia loĝejo fariĝis iom pli inteligenta. Kun tia malgranda aparato, estos pli oportune por mi kontroli la akvokonsumon en la domo. Se pli frue mi indignis "denove multe da akvo estis konsumita en monato", nun mi povas trovi la fonton de ĉi tiu konsumo.

Ŝajnos strange al iu rigardi la legaĵojn sur la ekrano se ĝi estas metro de la metro mem. Sed en ne tro malproksima estonteco, mi planas translokiĝi al alia apartamento, kie estos pluraj akvomontriloj, kaj la mezuriloj mem, plej verŝajne, situos sur la surteriĝo. Do fora leganta aparato estus tre oportuna.

Mi ankaŭ planas vastigi la funkciecon de la aparato. Mi jam rigardas motorizitajn valvojn. Nun, por ŝanĝi la kaldrono-urba akvon, mi devas turni 3 kranojn en malfacile atingebla niĉo. Estus multe pli oportune fari tion per unu butono kun la responda indiko. Nu, kompreneble, indas efektivigi protekton kontraŭ likoj.

En la artikolo, mi rakontis mian version de la aparato bazita sur ESP8266. Miaopinie, mi ricevis tre interesan version de la mikropitono-firmvaro uzante korutinojn - simplan kaj belan. Mi provis priskribi la multajn nuancojn kaj jambojn, kiujn mi renkontis dum la kampanjo. Eble mi priskribis ĉion tro detale, por mi persone, kiel leganto, estas pli facile malŝpari la troon ol pensi pri tio, kio poste restis nedirita.

Kiel ĉiam, mi estas malfermita al konstrua kritiko.

Fontkodo
Skemo kaj tabulo
Kaza Modelo

fonto: www.habr.com

Aldoni komenton