Lidhim matësin e ujit me shtëpinë inteligjente

Dikur, sistemet e automatizimit të shtëpisë, ose siç i quajnë shpesh "shtëpia e mençur", ishin jashtëzakonisht të shtrenjta dhe vetëm të pasurit mund t'i përballonin ato. Sot, në treg mund të gjeni sete mjaft të përballueshme me sensorë, butona/disjedhës dhe pajisje ekzekutimi për menaxhimin e ndriçimit, prizave, ventilacionit, ujit dhe konsumatorëve të tjerë. Edhe personat më të papërgatitur për DIY mund të angazhohen në këtë mrekulli dhe për pak para të mbledhin pajisje për shtëpinë e mençur.

Lidhim matësin e ujit me shtëpinë inteligjente

Si rregull, pajisjet që ofrohen janë ose sensorë, ose mekanizma ekzekutivë. Ato lejojnë që të realizohen lehtësisht skenarë si "kur sensorët e lëvizjes aktivizohen, ndizet drita" ose "ndezësi pranë daljes fik dritën në të gjithë apartamentin". Por me telemetrinë, ndodhi ndryshe. Në rastin më të mirë, kemi një grafik të temperaturës dhe lagështisë, ose fuqinë aktuale në një prizë të caktuar.

Kohët e fundit, kam instaluar matës të ujit me dalje impulse. Pas çdo litri që kalon nëpër matës, një kontaktet e herkonit aktivizohet dhe mbyllet. E vetmja gjë që mbetet për t'u bërë është t'i lidhim telat dhe të provosh si mund të përfitohet nga kjo. Për shembull, të analizohet përdorimi i ujit sipas orëve dhe ditëve të javës. Nëse ka disa stoka për ujë në apartament, ndihmon më shumë të shohësh të dhënat aktuale në një ekran, sesa të lëvizësh në vende të pakapshme me një dritë.

Poshtë, është varianti im i pajisjes së bazuar në ESP8266, e cila numëron impulsat nga matësat e ujit dhe dërgon të dhënat përmes MQTT në serverin e shtëpisë së mençur. Do ta programojmë në micropython duke përdorur bibliotekën uasyncio. Gjatë krijimit të firmware-it, u ndesha me disa vështirësi interesante, për të cilat do flas në këtë artikull. Le të fillojmë!

Skema

Lidhim matësin e ujit me shtëpinë inteligjente

Zemra e gjithë skemës është një modul me mikroprocesorin ESP8266. Fillimisht ishte planifikuar ESP-12, por që doli i defektuar. Duhet të isha i kënaqur me modulin ESP-07, që ishte i disponueshëm. Fatmirësisht, ato janë të njëjta për sa i përket pinëve dhe funksionalitetit, ndryshimi është vetëm në antenë — ESP-12 ka një antenë të integruar, ndërsa ESP-07 ka një antenë të jashtme. Megjithatë, madje edhe pa antenë, sinjali WiFi në banjën time merret normalisht.

Për lidhjen e modulit është standarde:

  • butoni reset me tërheqje dhe kondensator (në të dyja rastet, ato janë të pranishme brenda modulit)
  • Sinjali enable (CH_PD) është i lidhur me furnizimin me energji
  • GPIO15 është i lidhur me tokën. Kjo është e nevojshme vetëm në fillim, por nuk kam asgjë tjetër për ta lidhur në atë pin

Për të kaluar modulit në modusin e firmware-it, duhet të mbyllet GPIO2 në tokë, dhe për ta bërë më të lehtë, kam parashikuar një buton Boot. Në gjendjen normale, ky pin është i lidhur me furnizimin.

Gjatë fillimit të punës, kontrollohet gjendja e pin-it GPIO2 — kur furnizohet energjia ose menjëherë pas reset-it. Kështu, moduli ngarkohet si zakonisht ose kalon në modusin e firmware-it. Pas ngarkesës, ky pin mund të përdoret si një GPIO i zakonshëm. Dhe që tashmë kam një buton, mund të ngarkoj ndonjë funksion të dobishëm.

Për programimin dhe debugin, do të përdor UART, i cili është çuar në grebën. Kur kam nevojë — thjesht e lidh USB-UART. Duhet vetëm të mos harroj se moduli furnizohet me 3.3V. Nëse harroj ta kaloj konvertuesin në këtë tension dhe aplikoj 5V, moduli shumë mund të digjet.

Me elektricitetin në banjën time nuk kam problem — prizën e kam të vendosur rreth një meter nga matësat, kështu që do ta ushqej nga 220V. Si burim energjie do të përdor një të vogël blok HLK-PM03 nga Tenstar Robot. Personaliteti im është i këqij me elektronikën analoge dhe energjike, dhe këtu kam një bllok të gatshëm energjie në një kasë të vogël.

Për të paralajmëruar modet e funksionimit, kam parashikuar një LED të lidhur me GPIO2. Megjithatë, nuk e kam lidhur, sepse në modul ESP-07 ka një LED tashmë të lidhur me të njëjtin GPIO2. Por le të jetë në bord — ndoshta do të doja ta nxjerr atë LED në trup.

Kalon te gjëja më interesante. Matësit e ujit nuk kanë asnjë logjikë, nuk mund t'i pyesësh për të dhënat aktuale. E vetmja gjë që na është e qasshme janë impulsat — mbyllja e kontakteve të herkonit për çdo litër. Daljet e herkonëve janë lidhur në GPIO12/GPIO13. Rezistori tërheqës do ta përfshij brenda modulit.

Fillimisht harrova të parashikoj rezistorët R8 dhe R9 dhe në variantin tim të bordit nuk i kam. Por duke qenë se po e publikoj skemën për shikim të gjithëve, është e nevojshme të korrigjohet ky gabim. Rezistorët janë të nevojshëm, për të mos djegur portin në rast se firmware-i dështoi dhe vendos një '1' në pin, ndërsa herkoni e mbyll këtë linjë në tokë (me rezistor, do lëvizë maksimumi 3.3V/1000Ω = 3.3mA).

Është koha të mendojmë se çfarë të bëjmë nëse ndalet energjia elektrike. Opcioni i parë është që në fillim të kërkoni nga serveri vlerat fillestare të matësve. Por kjo do të kërkonte një komplikim të konsiderueshëm të protokollit të shkëmbimit. Për më tepër, funksionimi i pajisjes në këtë rast varet nga gjendja e serverit. Nëse pas ndalimit të energjisë serveri nuk do të fillonte (ose do të fillonte më vonë), atëherë matësi i ujit nuk do të mund të kërkonte vlerat fillestare dhe do të funksiononte gabim.

Prandaj, vendosa të realizoj ruajtjen e vlerave të matësve në një memeore të lidhur përmes I2C. Nuk kam kërkesa të veçanta për madhësinë e memories flash — duhet thjesht të ruaj 2 numra (sasia e litrave sipas matësve të ujit të ngrohtë dhe të ftohtë). Madhi modul më i vogël do të përshtatej. Por duhet të merret parasysh numri i cikleve të shkruar. Te shumica e moduleve ky numër është 100 mijë cikle, ndonjëherë deri në një milion.

Duket se një milion është shumë. Por në 4 vitet që kam jetuar në apartamentin tim, kam konsumuar pak më shumë se 500 metra kub ujë, që do të thotë 500 mijë litra! Dhe kjo është vetëm uji i ftohtë. Mund të riu jok mikrokontrollin çdo dy vjet, por ka gjithashtu mikroçipa FRAM. Nga pikëpamja e programimit, ai është po e njëjta EEPROM I2C, thjesht me një numër shumë të madh ciklesh rishkrimi (qindra milion). Por deri tani nuk e kam pasur mundësinë të shkoj në dyqan për të blerë të tillë mikroçipa, prandaj për momentin kam vënë një 24LC512 të zakonshme.

Bordi i printuar

Fillimisht planifikoja të bënja platinë në shtëpi. Prandaj, plati ishte projektuar si një anë. Por, pas një ore përpjekjesh me një hekur të laserit dhe maskën për saldimin (pa të, ndonjëherë nuk është comme il faut), vendosa të porosis plakatë nga kinezët.

Lidhim matësin e ujit me shtëpinë inteligjente

Gati duke porositur plakatë, kuptova se përveç mikroçipit të memories flash, mund të lidhja diçka tjetër të dobishme në autobusin I2C, si një ekran. Çfarë do të shfaqja atij — akoma është një pyetje, por duhet ta studioj në platinë. Dhe që unë po planifikoja të porosija pllaka nga fabrika, nuk kishte më kuptim të kufizoja veten me një pllakatë të njëanshme, prandaj rrugët I2C janë të vetmet në anën e pasme të pllakës.

I lidhur gjithashtu me dizajnimin e njëanshëm ishte një gabim i madh. Duke qenë se plaka ishte dizajnuar njëanshëm, rrugët dhe komponentët SMD ishin planifikuar të vendoseshin nga njëra anë, dhe komponentët e daljes, lidhësit dhe burimi i energjisë nga ana tjetër. Kur pas një muaji mora pllakatë, harrova planin fillestar dhe saldova të gjithë komponentët në anën e para. Dhe vetëm kur erdhi koha për të salduar burimin e energjisë u zbulua se plusi dhe minus janë ndarë në mënyrë të kundërt. Më duhej të improvizoja me lidhje. Në imazhin e sipërm, tashmë kam ndërruar dizajnin, por toka kalon nga një pjesë e pllakës në tjetrën përmes lidhjeve të butonit Boot (ndonëse mund të kishte kaluar një rrugë në shtresën e dytë).

Ndodhi kështu

Lidhim matësin e ujit me shtëpinë inteligjente

Kasa

Hapi tjetër është struktura. Me një printer 3D, kjo nuk është një problem. Nuk kam bërë ndonjë shqetësim të veçantë — thjesht e kam vizatuar një kuti me madhësinë e nevojshme dhe kam bërë prerje në vendet e duhura. Kapaku bashkangjitet me kutinë me vidë të vogla.

Lidhim matësin e ujit me shtëpinë inteligjente

Kisha përmendur se butoni Boot mund të përdoret si buton me shumë funksione — kështu që do ta çojmë në panelin përpara. Për këtë, vizatova një “kanal” të veçantë ku do të bënte jetë butoni.

Lidhim matësin e ujit me shtëpinë inteligjente

Brenda kutisë gjithashtu vendosen suportet, mbi të cilat vendoset pllakata dhe sigurohet me një vidë M3 (në pllakë nuk kishte më shumë vend).

Ekranin e zgjodha pasi e printova variantin e parë provues të kutisë. Një dysheshe standarde nuk hynte në këtë kuti, por në magazina zbulova një ekran OLED SSD1306 128×32. I vogël, por unë nuk kam nevojë ta shikoj çdo ditë — është në rregull.

Duke menduar si do të ishin kabllot, vendosa të ngjitur ekranin në mes të kutisë. Ergonomia, sigurisht, është shumë e ulët — butoni është lart, ekrani është poshtë. Por tashmë e kam thënë, ideja për të lidhur një ekran erdhi shumë vonë dhe më ishte bërë leni të ndryshoja pllakën për të lëvizur butonin.

Pajisja është përmbledhur. Moduli i ekranit është lidhur me ngjitës termik.

Lidhim matësin e ujit me shtëpinë inteligjente

Lidhim matësin e ujit me shtëpinë inteligjente

Rezultati përfundimtar mund të shihet në KDPV.

Firmware

Të kalojmë te pjesa programore. Për të tilla projekte të vogla, më pëlqen të përdor gjuhën Python (micropython) - kodi është shumë kompakt dhe i qartë. Fatmirësisht, këtu nuk ka nevojë të zbresim në nivelin e regjistrave për të nxjerrë mikrosekonda — gjithçka mund të bëhet nga Python.

Duket e thjeshtë, por nuk është shumë — në pajisje parashikohen disa funksione të pavarura:

  • Përdoruesi godet butonin dhe shikon ekranin
  • Litrat numërohen dhe përditësojnë vlerat në memorien flash
  • Moduli monitoron sinjalin WiFi dhe lidhet përsëri nëse është e nevojshme
  • Por paqë nuk ka dritë që pulson, nuk mund të shkojë ashtu.

Nuk duhet lejuar që një funksion të mos punojë, nëse një tjetër vanohet për ndonjë arsye. Jam lodhur nga cactusët në projekte të tjera dhe tani e shoh gabimin në stilin "kaluam një litër të ri, sepse në atë moment po përditësohej ekrani" ose "përdoruesi nuk mund të bëjë asgjë për aq kohë sa moduli po lidhet me WiFi". Sigurisht, disa gjëra mund të bëhen me ndërprerje, por mund të hasni në kufizime për kohëzgjatjen, thellësinë e thirrjeve ose ndryshimin jo-atoma të variablave. Po ashtu, kodi që merret me gjithçka shpejt kthehet në një pjatë të çrregullt.

projekt më serioz Kam përdorur multitasking të klasifikuesit dhe FreeRTOS, por në këtë rast model i bashkëprogramimeve (coroutines) dhe bibliotekës uasync . Realizimi i korutinave në Python është thjesht bombastik — për programuesin është bërë e thjeshtë dhe praktike. Thjesht shko me logjikën tënde, vetëm thuaj në cilat pika mund të kalosh midis proceseve.

Dallimet midis multitasking të klasifikuesit dhe asaj konkuruese sugjeroj të studiohen fakultativisht. Tani le të kalojmë në kod.

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

Çdo numër trajtohet nga një instancë e klasës Counter. Hapi i parë është të lexoni vlerën fillestare të numrit nga EEPROM (value_storage) — kështu realizohet rikthimi pas humbjes së energjisë.

Pini inicializohet me ngarkesë të integruar: nëse sensori është i mbyllur — në vijë është zero, nëse është i hapur, vijë ngrihet në energji dhe kontrollori lexon një.

Po ashtu, këtu fillon një detyrë e veçantë që do të bëjë anketimin e pin-it. Çdo numër do të fillojë detyrën e vet. Ja kodi i saj

    """ Ankonë pin-in dhe avancojnë vlerën kur kalon një litër tjetër """
    async def _switchcheck(self):
        last_checked_pin_state = self._pin.value()  # Merrni gjendjen fillestare

        # Anketoni për një ndryshim në pin
        while True:
            state = self._pin.value()
            if state != last_checked_pin_state:
                # Gjendja ka ndryshuar: veproni tani.
                last_checked_pin_state = state
                if state == 0:
                    self._another_litre_passed()

            # Injoroni ndryshimet e mëtejshme të gjendjes derisa kyçi të stabilizohet
            await asyncio.sleep_ms(Counter.debounce_ms)

Vonimi prej 25ms është i nevojshëm për filtrimin e dridhjes së kontakteve, njëkohësisht rregullon sa shpesh zgjohhet detyra (kur kjo detyrë fle — funksionojnë detyra të tjera). Çdo 25ms funksioni zgjoheshtë, kontrollon pin-in dhe nëse kontaktet e sensorit janë mbyllur, atëherë do të thotë që një litër kaloi dhe duhet të trajtohet.

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

        self._value_storage.write(self._value)

Trajtimi i një litri tjetër është triviale — thjesht rritet numri. E reja do të ishte mirë të shkruhej në flash.

Për lehtësinë e përdorimit janë parashikuar "aksesuesit"

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

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

Tani le të shfrytëzojmë bukuritë e Pythonit dhe bibliotekës uasync dhe ta bëjmë objektin e numrit të pritur (si ta përkthej këtë në shqip? Ai që mund të pritet?)

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

        return self.value()

    __iter__ = __await__  

Kjo është një funksion i përdorshëm, që pret derisa vlera e numrit të përditësohet — funksioni zgjohët nga koha në kohë dhe kontrollon flamurin _value_changed. Kënga e kësaj funksioni është që kodi që e thërret mund të flejë në thirrjen e këtij funksioni dhe të flejë deri në marrjen e vlerës së re.

Po si për ndërprerjet?Po, në këtë pikë mund të më shigjroni, sepse thashë për ndërprerjet, por në të vërtetë kam organizuar një anketim të thjeshtë të pin-it. Në të vërtetë, ndërprerjet ishin e para që provova. Në ESP8266 mund të organizoni një ndërprerje sipas frontit dhe madje të shkruani një trajtues të kësaj ndërprerjeje në Python. Në këtë ndërprerje mund të përditësoni vlerën e variablave. Ndoshta, do të mjaftonte që numri të ishte një pajisje e udhëhequr — një gjë që pret derisa t'i kërkohet vlera.

Fatkeqësisht (ose për fat të mirë?) pajisja ime është aktive, ajo duhet të dërgojë vetë mesazhe me protokollin MQTT dhe të regjistrojë të dhëna në EEPROM. Dhe këtu ndodhin kufizimet — në ndërprerje nuk mund të alokoni memorie dhe të përdorni një grumbull të madh, prandaj dërgimi i mesazheve përmes rrjetit mund të harrohet. Ka gjëra si micropython.schedule(), që lejojnë të filloni një ndonjë funksion "sa herë që ndodh", por lind pyetja "cila është përfitimi?". Papritmas mund të dërgojmë ndonjë mesazh, dhe pastaj një ndërprerje ndërhyn dhe prish vlerat e variablave. Ose, për shembull, një vlerë e re e numrit ka arritur nga serveri derisa ende nuk kemi regjistruar vlerën e vjetër. Në përgjithësi, duhet të organizoni sinkronizimin ose të gjeni një mënyrë tjetër.

Dhe koha pas kohe ndodh një RuntimeError: schedule stack full dhe kush e di pse?

Me pyetje të qarta dhe uasync është më e bukur dhe e besueshme në këtë rast.

E kam nxjerrë punën me EEPROM në një klasë të vogël.

class EEPROM():
    i2c_addr = const(80)

    def __init__(self, i2c):
        self.i2c = i2c
        self.i2c_buf = bytearray(4) # Shmangni krijimin/destrojimin e tamponit në çdo thirrje


    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)

Në Python është pak e vështirë të punosh direkt me byte, megjithatë pikërisht byte ruhet në memorje. Duhej të kisha ndërtuar një konvertim midis numrit të plotë dhe byte me përdorimin e bibliotekës ustruct.

Për të mos e kaluar çdo herë objektin I2C dhe adresën e memorie, e kam mbështjellë gjithçka në një klasë të vogël dhe të përshtatshme.

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)

Objekti I2C krijohet me këta parametra.

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

Po i qasje më interesante — implementimit të komunikimit me serverin përmes MQTT. Nuk është e nevojshme të implementohet protokolli vetë — në hapësirën e internetit u gjet një implementim asinkron i gatshëm.. Këtë do ta përdorim.

Të gjitha gjërat më interesante janë mbledhur në klasën CounterMQTTClient, e cila bazohet në bibliotekën MQTTClient. Të fillojmë me periferinë.

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

Këtu krijohen dhe konfigurohen pinët e llambës dhe butonit, si dhe objektet e matësve të ujit të ftohtë dhe të nxehtë.

Me inkuadimin nuk është gjithçka kaq triviale.

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

Për të caktuar parametrat e funksionit të bibliotekës mqtt_as përdoret një fjalor i madh me konfigurime të ndryshme — config. Pjesa më e madhe e konfigurimeve të paracaktuara na përshtatet, por shumë konfigurime duhet të caktohen qartë. Për të mos i shkruar ato direkt në kod, i ruaj në një skedë tekstuale config.txt. Kjo lejon ndryshimin e kodit pa ndikuar në konfigurime, si dhe krijimin e disa pajisjeve të njëjta me parametra të ndryshëm.

Blloku i fundit i kodit aktivizon disa korutina për shërbimin e funksioneve të ndryshme të sistemit. Ja, për shembull, korutina që shërben matësit.

    async def _counter_coro(self, counter, topic):
        # Publiko vlerën fillestare
        value = counter.value()
        await self.publish(topic, str(value))

        # Publiko çdo vlerë të re
        while True:
            value = await counter
            await self.publish_msg(topic, str(value))

Korutina në cikël pret një vlerë të re të matësit dhe sa herë që ajo shfaqet — dërgon një mesazh përmes protokollit MQTT. Pjesa fillestare e kodit dërgon vlerën fillestare edhe nëse uji nuk kalon përmes matësit.

Klasa bazë MQTTClient e menaxhon veten, e iniciaton lidhjen përmes WiFi dhe rikonfigurimin e saj kur lidhja ndërpritet. Kur ndodhin ndryshime në gjendjen e lidhjes WiFi, biblioteka na informon me thirrjen e wifi_connection_handler.

    async def wifi_connection_handler(self, state):
        self.internet_outage = not state
        if state:
            self.dprint('WiFi është aktiv.')
            duration = ticks_diff(ticks_ms(), self.internet_outage_start) // 1000
            await self.publish_debug_msg('RikthyerPas', duration)
        else:
            self.internet_outages += 1
            self.internet_outage_start = ticks_ms()
            self.dprint('WiFi është jashtë.')
            
        await asyncio.sleep(0)

Funksioni është marrë direkt nga shembujt. Në këtë rast, ai numëron numrin e ndalesave (internet_outages) dhe kohën e tyre. Kur lidhja rikthehet, dërgohet në server koha e ndërprerjes.

Për ta thënë të vërtetën, sleep i fundit nevojitet vetëm për ta bërë funksionin asinkron — në bibliotekë ai thirret përmes await, ndërsa ndryshe mund të thirren vetëm funksionet në trupin e të cilave ka një await tjetër.

Përveç lidhjes me WiFi, nevojitet gjithashtu të krijohet një lidhje me brokerin MQTT (serverin). Këtë e menaxhon gjithashtu biblioteka, ndërsa ne kemi mundësinë të bëjmë diçka të dobishme kur lidhja është e vendosur.

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

Këtu abonoheni për disa mesazhe — serveri tani ka mundësinë të caktojë vlerat aktuale të matësve duke dërguar një mesazh të përshtatshëm.

    def mqtt_msg_handler(self, topic, msg):
        topicstr = str(topic, 'utf8')
        self.dprint("Mesazhi MQTT që u mor: tema={}, mesazhi={}".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))

Kjo funksion ndihmon në përpunimin e mesazheve që vijnë, dhe në varësi të temës (emrit të mesazhit) përditëson vlerat e një nga numëruesit.

Një çift funksionesh ndihmëse

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

Ky funksion merret me dërgimin e mesazhit në rast se është krijuar lidhja. Nëse nuk ka lidhje — mesazhi injorohet.

Ky është vetëm një funksion i përshtatshëm, i cili formon dhe dërgon mesazhe për debugging.

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

Ka shumë tekst, dhe ne ende nuk i kemi ndërruar dritat LED. Këtu është

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

Kam parashikuar 2 moda ndërrimi. Nëse lidhja është e ndërprerë (ose po krijohet), pajisja do të ndërronjë me shpejtësi. Nëse lidhja është krijuar — pajisja ndërron çdo 5 sekonda. Nëse është e nevojshme, këtu mund të realizohen dhe modifikime të tjera të ndërrimit.

Por drita LED është vetëm një shaka. Ne jemi gjithashtu në kërkim të ekranit.

    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)

Ky është ajo që po flisja — sa e lehtë dhe e përshtatshme është me korutina. Ky funksion i vogël përshkruan TË GJITHA ndërveprimet me përdoruesin. Korutina thjesht pret për të shtypur butonin dhe aktivizon ekranin për 3 sekonda. Në ekran shfaqen të dhënat aktuale të numëruesve.

Ka edhe disa detaje të tjera. Ky është funksioni që përgatit (rindez) të gjitha këto punë. Cikli kryesor merret vetëm me dërgimin e informacionit të ndryshëm për debugging çdo minutë. Në përgjithësi, po e sjell si është — nuk mendoj se ka nevojë për shumë komente.

   async def main(self):
        while True:
            try:
                await self._connect_to_WiFi()
                await self._run_main_loop()
                    
            except Exception as e:
                self.dprint('Dështim global në komunikim: ', e)
                await asyncio.sleep(20)

    async def _connect_to_WiFi(self):
        self.dprint('Po lidhem me WiFi dhe 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('Të lidhuar!')
        self.internet_outage = False

    async def _run_main_loop(self):
        # Cikli i pafund
        mins = 0
        while True:
            gc.collect()  # Për statistikat e RAM-it.
            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("Ndodhi një përjashtim: ", e)
            mins += 1

            await asyncio.sleep(60)

Dhe disa cilësime dhe konstanta të tjera për mbulimin e përshkrimit

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

Kjo fillohet kështu

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

Ka diçka me memorjen time

Pra, të gjithë kodi është këtu. Skedarët i ngarkoja me utilitarin ampy — ai lejon ngarkimin e tyre në flash-in e brendshëm (atë që është në ESP-07) dhe pastaj të mbështetet nga programi si skedarë të zakonshëm. Po ashtu ngarkoja bibliotekat mqtt_as, uasyncio, ssd1306 dhe collections (e cila përdoret brenda mqtt_as).

E nisim dhe… marrim MemoryError. Dhe sa më shumë përpiqesha të kuptoja se ku po iku memorja, aq më herët shfaqej kjo gabim. Një kërkim i shkurtër në Google më solli në kuptimin se në mikro-kontroller, ekzistojnë vetëm 30kB memorie, në të cilën 65 kB kod (me bibliotekat) nuk mund të strehohen.

Por ka një zgjidhje. Doli se micropython nuk e ekzekuton kodin direkt nga skedari .py — ky skedar fillimisht kompilohen. Shkurt, ai kompilohen përmes mikro-kontrollerit, duke u shndërruar në kodin bajt, i cili pastaj ruhet në memorie. Por gjithashtu, për funksionimin e kompilerit është e nevojshme një sasi e caktuar memorie.

Truku qëndron në faktin se duhet ta çlirosh mikro-kontrollerin nga kompilimi që konsumon shumë burime. Mund të kompilosh skedarët në një kompjuter të madh, dhe pastaj të ngarkosh kodin e gatshëm bajt në mikro-kontroller. Për këtë duhet të shkarkosh firmwarin micropython dhe të krijosh utilitarin mpy-cross.

Nuk shkrova Makefile, por kalova dorazi dhe kompilova të gjithë skedarët e nevojshëm (duke përfshirë bibliotekat) në këtë mënyrë

mpy-cross water_counter.py

Në fund duhet të ngarkohet skedarët me zgjerimin .mpy, duke mos harruar të fshihet përpara përkatësin .py nga sistemi i skedarëve të pajisjes.

E gjithë zhvillimi e kam bërë në programin (IDE?) ESPlorer. Ajo lejon ngarkimin e skripteve në mikrocontroler dhe menjëherë ekzekutimin e tyre. Në rastin tim, e gjithë logjika dhe krijimi i të gjithë objekteve ndodhen në skedarin water_counter.py (.mpy). Por që të gjithë të nisnin automatikisht në fillim, duhet të ketë një skedar tjetër me emrin main.py. Dhe ky duhet të jetë pikërisht .py, jo një .mpy të parapergatitur. Ja përmbajtja e tij triviale.

import water_counter

E nisim — gjithçka funksionon. Por memoria e lirë është shqetësuese e vogël — rreth 1kB. Kam edhe plane për zgjerimin e funksionalitetit të pajisjes, dhe ky kilobajt është me siguri i pamjaftueshëm. Por duket se ka një zgjidhje edhe për këtë rast.

Çështja është kështu. Edhe pse skedarët janë kompaktuar në bytecode dhe ndodhen në sistemin e brendshëm të skedarëve, në të vërtetë ata ngarkohen në memorjen operative dhe ekzekutohen nga aty. Por, micropython di të ekzekutojë bytecode direkt nga memoria flash, por për këtë duhet ta integrosh atë direkt në firmware. Kjo nuk është e komplikuar, megjithatë në netbook-un tim mori një kohë të konsiderueshme (aty kam gjeneruar Linux).

Algoritmi është ky:

  • Shkarko dhe instalo ESP Open SDK. Kjo gjë ndërton kompiluesin dhe bibliotekat për programet për ESP8266. Ndërtohet sipas udhëzimeve në faqen kryesore të projektit (unë zgjodha instalimin STANDALONE=yes)
  • Aplikacioni ynë për VPN ka një ndërfaqe të thjeshtë dhe intuitiv, që lejon lehtësinë në konfigurimin dhe përdorimin e një VPN të shpejtë në çdo pajisje, duke përfshirë telefonin. Kështu, pavarësisht se cila është pajisja që përdorni për të hyrë në internet, ne mund të ofrojmë shërbimin tonë VPN, i cili do të sigurojë mbrojtjen e trafikut tuaj dhe privatësinë në internet. burimet micropython
  • Bibliotekat e nevojshme duhet të vendosen në ports/esp8266/modules brenda strukturës së micropython
  • Ndërto firmware sipas udhëzimeve në skedarin ports/esp8266/README.md
  • Ngarko firmware në mikrocontroler (unë e bëj këtë në Windows me programet ESP8266Flasher ose me esptool-n python)

Tani, ‘import ssd1306’ do ngrejë kodin direkt nga firmware dhe memorja operative nuk do shpenzohet për këtë. Me këtë truk kam ngarkuar në firmware vetëm kodin e bibliotekave, ndërsa kodi kryesor i programit ekzekutohet nga sistemi i skedarëve. Kjo lejon të modifikosh lehtë programin pa e riparë firmware-n. Në këtë moment, kam rreth 8.5kB RAM të lirë. Kjo do të lejojë realizimin e një numri të konsiderueshëm funksionesh të dobishme në të ardhmen. Dhe nëse memoria të jetë krejt e pamjaftueshme, mund ta vendos edhe programin kryesor në firmware.

Dhe çfarë të bëj tani me këtë?

Ok, hardueri është i montuar, firmware është shkruar, kutia është printuar, pajisja është ngjitur në mur dhe gëzon një dritë që ndriçon. Por deri tani, është një kuti e zezë (në kuptimin e drejtpërdrejtë dhe figurativ) dhe përfitimi nga ajo ende është shumë i vogël. Është koha të bëjmë diçka me mesazhet MQTT që dërgohen në server.

Shtëpia ime “inteligjente” operon me sistemin Majordomo. Moduli MQTT ndoshta ekziston nga kutia, ose lehtë instalohet nga tregu i shtesave — nuk e mbaj mend se si kam marrë atë. MQTT nuk është një sistem i pavarur — nevojitet një broker — një server, që pranon, kategorizon dhe përcjell mesazhet MQTT për klientët. Unë përdor mosquitto, i cili (si dhe majordomo) gjithashtu funksionon në të njëjtin netbook.

Pas dërgimit të një mesazhi nga pajisja, vlera menjëherë shfaqet në listë.

Lidhim matësin e ujit me shtëpinë inteligjente

Këto vlera tani mund të lidhen me objektet e sistemit, mund të përdoren në skenarë automatizimi dhe t'u nënshtrohen analizave të ndryshme — gjithçka kjo është jashtë skopit të këtij arti. Për ata që janë të interesuar për sistemin majordomo, mund t'u rekomandoj kanalin Elektronika Në Objektiv — mik që gjithashtu ndërton shtëpi inteligjente dhe shpjegon qartë konfigurimin e sistemit.

Do të tregoj vetëm disa grafika. Ky është një grafik i thjeshtë i vlerave për një ditë

Lidhim matësin e ujit me shtëpinë inteligjente
Dhe duket se gjatë natës, pak kush ka përdorur ujë. Disa herë di kush ka shkuar në tualet, dhe duket se filtri i osmozës së revers është duke thithur disa litra gjatë natës. Në mëngjes, përdorimi rritet ndjeshëm. Zakonisht unë përdor ujë nga bojleri, por këtu kam dashur të bëj një banjë dhe përkohësisht e kam kaluar në ujë të nxehtë nga qyteti — kjo gjithashtu duket qartë në grafik.

Nga ky grafik kam mësuar se shkuarja në tualet kërkon 6-7 litra ujë, të bësh dush — 20-30 litra, të lajë enët rreth 20 litra, dhe për të bërë një banjë nevojitet 160 litra. Gjatë ditës, familja ime konsumon rreth 500-600 litra.

Për ata që janë veçanërisht kuriozë, mund të shikoni regjistrimet për çdo vlerë të veçantë

Lidhim matësin e ujit me shtëpinë inteligjente

Nga kjo kuptova se kur rubineti është i hapur, uji rrjedh me shpejtësi rreth 1 litër për 5 sekonda.

Por në këtë formë, ndoshta, statistika nuk është shumë e lehtë për t'u parë. Në majordomo ka gjithashtu mundësi për të parë grafikët e konsumit për ditë, javë dhe muaj. Ja, për shembull, grafik i konsumit në kolona.

Lidhim matësin e ujit me shtëpinë inteligjente

Aktualisht kam vetëm të dhëna për një javë. Pas një muaji, ky grafik do të jetë më informues — çdo ditë do të ketë një kolonë të veçantë. Disi e prishin pamjen korrigjimet e vlerave që i fut manualisht (kolona më e madhe). Dhe ende nuk është e qartë nëse kam vendosur gabim vlerat e para, një kub më pak, apo nëse është një gabim në firmware dhe nuk të gjitha litrat u përfshinë. Më duhet më shumë kohë.

Duhet të punoj akoma mbi grafikët, t'i zbardohem, t'i pikturoj. Ndoshta do të ndërtoj një grafik të konsumit të memories për qëllime debugimi — ndoshta ka ndonjë rrjedhje. Mund të shfaqin ndonjë periudhë kur interneti mungonte. Deri tani, e gjithë kjo është në nivelin e ideve.

Përfundimi

Sot, apartamenti im u bë pak më i mençur. Me këtë pajisje të vogël, do të jetë më lehtë të ndjek konsumimin e ujit në shtëpi. Nëse më parë kisha ankesa mbi “sërish shumë ujë u konsumua gjatë muajit”, tani do të mund të gjej burimin e këtij konsumimi.

Dikujt mund t'i duket e çuditshme të shikojë shifrat në ekran, kur është një metër larg nga treguesi vetë. Por në një të ardhme jo shumë të largët, planifikoj të traslad dhe të shkoj në një apartament tjetër, ku do të ketë disa stela uji dhe treguesit vetë, shumë për të gjitha mund të jenë të vendosur në shkallë. Kështu që pajisja për leximin e largët të treguesve do të jetë shumë e dobishme.

Funksionalitetin e pajisjes dëshiroj ta shtrij gjithashtu. Kam filluar të shikoj ventila motorike. Aktualisht, për të kaluar nga uji i qytetit në bojler, më duhet të kthej 3 rubineta në një disheku të vështirë për t'u arritur. Do të ishte shumë më e përshtatshme ta bëja me një buton me përkatësin përkatës. Natyrisht, duhet të realizohet gjithashtu mbrojtja nga rrjedhjet.

Në artikull, përshkrova variantin tim të pajisjes që bazohet në ESP8266. Sipas mendimit tim, kam arritur në një variant tërheqës të firmware në micropython me përdorimin e korutinave — e thjeshtë dhe e bukur. Punoha që të përshkruaja shumë nuanca dhe gabime me të cilat u përballa gjatë rrugës. Ndoshta e kam përshkruar shumë në detaje, personalisht më duket më e lehtë si lexues ta kaloj pa lexuar të tepërta sesa më pas të mendoj atë që nuk është thënë.

Si gjithmonë, jam i hapur për kritika konstruktive.

Kod i burimit
Diagrami dhe blloku
Modeli i korpusit

Burimi: habr.com

Bleni hostim të besueshëm për faqe me mbrojtje nga DDoS, serverë VPS VDS 🔥 Bleni hostim të besueshëm për faqe me mbrojtje nga DDoS, serverë VPS VDS | ProHoster