Mēs savienojam ūdens skaitītāju ar viedo māju

Kādreiz mājas automatizācijas sistēmas jeb “gudrās mājas”, kā tās mēdza dēvēt, bija šausmīgi dārgas, un tās varēja atļauties tikai bagātie. Mūsdienās tirgū var atrast diezgan lētus komplektus ar sensoriem, pogām/slēdžiem un izpildmehānismiem apgaismojuma, kontaktligzdu, ventilācijas, ūdens apgādes un citu patērētāju kontrolei. Un pat visgreizākais DIY cilvēks var iesaistīties skaistumkopšanā un salikt ierīces gudrai mājai par lētu cenu.

Mēs savienojam ūdens skaitītāju ar viedo māju

Parasti piedāvātās ierīces ir vai nu sensori, vai izpildmehānismi. Tie atvieglo tādu scenāriju ieviešanu kā "kad tiek iedarbināts kustības sensors, ieslēdziet apgaismojumu" vai "slēdzis netālu no izejas izslēdz apgaismojumu visā dzīvoklī". Bet kaut kā ar telemetriju viss neizdevās. Labākajā gadījumā tas ir temperatūras un mitruma vai momentānās jaudas grafiks noteiktā kontaktligzdā.

Nesen uzstādīju ūdens skaitītājus ar impulsa izvadi. Katram litram, kas iziet cauri skaitītājam, tiek aktivizēts niedru slēdzis un aizver kontaktu. Atliek tikai pieķerties vadiem un mēģināt no tā gūt labumu. Piemēram, analizējiet ūdens patēriņu pēc stundas un nedēļas dienas. Nu, ja dzīvoklī ir vairāki ūdens stāvvadi, tad ērtāk ir redzēt visus strāvas rādītājus vienā ekrānā, nekā ar lukturīti kāpt grūti aizsniedzamās nišās.

Zem griezuma ir mana versija par ierīci, kuras pamatā ir ESP8266, kas skaita impulsus no ūdens skaitītājiem un nosūta rādījumus, izmantojot MQTT, uz viedās mājas serveri. Mēs programmēsim micropython, izmantojot uasyncio bibliotēku. Veidojot programmaparatūru, es saskāros ar vairākām interesantām grūtībām, kuras arī aplūkošu šajā rakstā. Aiziet!

Shēma

Mēs savienojam ūdens skaitītāju ar viedo māju

Visas ķēdes sirds ir ESP8266 mikrokontrollera modulis. Sākotnēji tika plānots ESP-12, bet manējais izrādījās bojāts. Mums bija jāapmierinās ar ESP-07 moduli, kas bija pieejams. Par laimi, tie ir vienādi gan tapu, gan funkcionalitātes ziņā, atšķirība ir tikai antenā - ESP-12 ir iebūvēta, savukārt ESP-07 ir ārējā. Taču arī bez WiFi antenas signāls manā vannas istabā tiek uztverts normāli.

Standarta moduļa vadi:

  • atiestatīšanas poga ar uzvilkšanu un kondensatoru (lai gan abi jau atrodas modulī)
  • Iespējošanas signāls (CH_PD) tiek iedarbināts
  • GPIO15 ir novilkts uz zemes. Tas ir vajadzīgs tikai sākumā, bet man joprojām nav ko piestiprināt pie šīs kājas; man tas vairs nav vajadzīgs

Lai moduli pārslēgtu programmaparatūras režīmā, GPIO2 ir jāsavieno ar zemi, un, lai tas būtu ērtāk, es nodrošināju sāknēšanas pogu. Normālā stāvoklī šī tapa tiek pievilkta pie strāvas.

GPIO2 līnijas stāvoklis tiek pārbaudīts tikai darbības sākumā - pieslēdzot strāvu vai tūlīt pēc atiestatīšanas. Tātad modulis vai nu sāk darboties kā parasti, vai arī pāriet programmaparatūras režīmā. Pēc ielādes šo tapu var izmantot kā parastu GPIO. Nu, tā kā tur jau ir poga, varat tai pievienot kādu noderīgu funkciju.

Programmēšanai un atkļūdošanai izmantošu UART, kas tiek izvadīts uz ķemmi. Ja nepieciešams, es tur vienkārši pievienoju USB-UART adapteri. Jums tikai jāatceras, ka modulis tiek darbināts ar 3.3 V. Ja aizmirstat pārslēgt adapteri uz šo spriegumu un pievadīt 5V, modulis, visticamāk, izdegs.

Man vannasistabā ar elektrību nav problēmu - izvads atrodas apmēram metru no skaitītājiem, tāpēc barošu ar 220V. Kā barošanas avots man būs mazs bloks HLK-PM03 autors Tenstar Robot. Man personīgi ir grūti ar analogo un spēka elektroniku, bet šeit ir gatavs barošanas bloks mazā korpusā.

Lai signalizētu darbības režīmus, es nodrošināju LED, kas savienots ar GPIO2. Tomēr es to neatlodēju, jo... ESP-07 modulim jau ir LED, un tas ir savienots arī ar GPIO2. Bet lai tas ir uz tāfeles, ja es vēlos šo LED izvadīt korpusā.

Pāriesim pie interesantākās daļas. Ūdens skaitītājiem nav loģikas, jūs nevarat no tiem pieprasīt pašreizējos rādījumus. Vienīgais, kas mums ir pieejams, ir impulsi - niedres slēdža kontaktu aizvēršana ik pēc litra. Manas niedres slēdža izejas ir savienotas ar GPIO12/GPIO13. Es programmatiski iespējošu pievilkšanas rezistoru modulī.

Sākotnēji es aizmirsu nodrošināt rezistorus R8 un R9, un manā plates versijā to nav. Bet, tā kā es jau ievietoju diagrammu, lai visi to redzētu, ir vērts labot šo kļūdu. Rezistori ir nepieciešami, lai nesadedzinātu pieslēgvietu, ja programmaparatūra satricina un iestata tapu uz vienu, un niedres slēdzis saīsina šo līniju ar zemi (ar rezistoru plūdīs ne vairāk kā 3.3 V/1000 Ohm = 3.3 mA).

Laiks domāt, ko darīt, ja pazūd elektrība. Pirmā iespēja ir sākumā pieprasīt no servera sākotnējās skaitītāja vērtības. Bet tas prasītu ievērojamu apmaiņas protokola sarežģījumu. Turklāt ierīces veiktspēja šajā gadījumā ir atkarīga no servera stāvokļa. Ja serveris nesāktos pēc strāvas izslēgšanas (vai palaists vēlāk), ūdens skaitītājs nevarēs pieprasīt sākotnējās vērtības un nedarbosies pareizi.

Tāpēc es nolēmu ieviest skaitītāja vērtību saglabāšanu atmiņas mikroshēmā, kas savienota ar I2C. Man nav īpašu prasību attiecībā uz zibatmiņas izmēru - jums ir jāsaglabā tikai 2 cipari (litru skaits atbilstoši karstā un aukstā ūdens skaitītājiem). Pat mazākais modulis derēs. Bet jums jāpievērš uzmanība ierakstīšanas ciklu skaitam. Lielākajai daļai moduļu tas ir 100 tūkstoši ciklu, dažiem līdz miljonam.

Šķiet, ka miljons ir daudz. Bet 4 gadu laikā, dzīvojot savā dzīvoklī, es patērēju nedaudz vairāk par 500 kubikmetru ūdens, tas ir 500 tūkstoši litru! Un 500 tūkstoši ierakstu zibspuldzē. Un tas ir tikai auksts ūdens. Jūs, protams, varat pārlodēt mikroshēmu ik pēc pāris gadiem, bet izrādās, ka ir FRAM mikroshēmas. No programmēšanas viedokļa tas ir tas pats I2C EEPROM, tikai ar ļoti lielu pārrakstīšanas ciklu skaitu (simtiem miljonu). Vienkārši ar tādām mikroshēmām līdz veikalam vēl nevaru tikt, tāpēc pagaidām parastais 24LC512 stāvēs.

Iespiedshēmas plate

Sākotnēji plānoju dēli izgatavot mājās. Tāpēc dēlis tika veidots kā vienpusējs. Bet pēc stundas pavadīšanas ar lāzera gludekli un lodēšanas masku (bez tās kaut kā nav comme il faut), es tomēr nolēmu pasūtīt dēļus no ķīniešiem.

Mēs savienojam ūdens skaitītāju ar viedo māju

Gandrīz pirms plates pasūtīšanas sapratu, ka papildus zibatmiņas mikroshēmai I2C kopnei var pieslēgt vēl ko noderīgu, piemēram, displeju. Ko tieši tajā izvadīt, tas vēl ir jautājums, bet tas ir jāmaršrutē uz tāfeles. Nu, tā kā es gatavojos pasūtīt dēļus no rūpnīcas, nebija jēgas aprobežoties ar vienpusēju dēli, tāpēc I2C līnijas ir vienīgās plāksnes aizmugurē.

Bija arī viena liela problēma ar vienvirziena vadu. Jo Plāksne tika zīmēta kā vienpusēja, tāpēc vienā pusē bija paredzēts novietot sliežu ceļus un SMD komponentus, bet otrā - izejas komponentus, savienotājus un barošanas bloku. Kad pēc mēneša saņēmu dēļus, aizmirsu par sākotnējo plānu un pielodēju visas sastāvdaļas priekšpusē. Un tikai tad, kad runa bija par barošanas bloka lodēšanu, izrādījās, ka pluss un mīnuss tika savienoti pretēji. Nācās saimniekot ar džemperiem. Augšējā bildē jau esmu nomainījis vadus, bet zeme tiek pārnesta no vienas dēļa daļas uz otru caur Boot pogas tapām (lai gan būtu iespēja uzzīmēt trasi uz otro slāni).

Tas izrādījās šādi

Mēs savienojam ūdens skaitītāju ar viedo māju

Корпус

Nākamais solis ir ķermenis. Ja jums ir 3D printeris, tā nav problēma. Es pārāk neuztraucos - vienkārši uzzīmēju vajadzīgā izmēra kastīti un izveidoju izgriezumus pareizajās vietās. Vāks ir piestiprināts pie korpusa ar mazām pašvītņojošām skrūvēm.

Mēs savienojam ūdens skaitītāju ar viedo māju

Es jau minēju, ka sāknēšanas pogu var izmantot kā vispārējas nozīmes pogu, tāpēc mēs to parādīsim priekšējā panelī. Lai to izdarītu, es uzzīmēju īpašu “aku”, kurā atrodas poga.

Mēs savienojam ūdens skaitītāju ar viedo māju

Korpusa iekšpusē ir arī tapas, uz kurām tāfele ir uzstādīta un nostiprināta ar vienu M3 skrūvi (uz tāfeles vairs nebija vietas)

Displeju izvēlējos jau tad, kad izdrukāju pirmo korpusa parauga versiju. Standarta divu līniju lasītājs šajā korpusā neiederējās, bet apakšā bija OLED displejs SSD1306 128×32. Tas ir mazliet mazs, bet man nav katru dienu uz to jāskatās — man tas ir par daudz.

Izdomājot šo ceļu un to, kā no tā tiks izvilkti vadi, es nolēmu ielīmēt displeju korpusa vidū. Ergonomika, protams, ir zem nominālvērtības – poga ir augšpusē, displejs apakšā. Bet es jau teicu, ka doma piestiprināt displeju radās pārāk vēlu un man bija slinkums pārslēgt dēli, lai pārvietotu pogu.

Ierīce ir samontēta. Displeja modulis ir pielīmēts pie puņķa ar karsto līmi

Mēs savienojam ūdens skaitītāju ar viedo māju

Mēs savienojam ūdens skaitītāju ar viedo māju

Gala rezultātu var redzēt KDPV

Programmaparatūras

Pāriesim pie programmatūras daļas. Tādiem maziem amatniecības darbiem kā šis man ļoti patīk izmantot Python (mikropitons) - kods izrādās ļoti kompakts un saprotams. Par laimi, nav nepieciešams nolaisties līdz reģistra līmenim, lai izspiestu mikrosekundes - visu var izdarīt no Python.

Šķiet, ka viss ir vienkārši, bet ne ļoti vienkārši - ierīcei ir vairākas neatkarīgas funkcijas:

  • Lietotājs nospiež pogu un skatās uz displeju
  • Litri atzīmē un atjaunina vērtības zibatmiņā
  • Modulis uzrauga WiFi signālu un vajadzības gadījumā atjauno savienojumu
  • Nu, bez mirgojošas spuldzes tas nav iespējams

Jūs nevarat pieņemt, ka viena funkcija nedarbojās, ja kāda cita funkcija ir iestrēgusi. Man jau ir piesātināti kaktusi citos projektos un tagad joprojām redzu kļūmes stilā “pakavēju vēl vienu litru, jo tajā brīdī tika atjaunināts displejs” vai “lietotājs neko nevar darīt, kamēr modulis pieslēdzas Bezvadu internets." Protams, dažas darbības var veikt, izmantojot pārtraukumus, taču jūs varat saskarties ar ilguma ierobežojumiem, zvanu ligzdošanu vai mainīgo lielumu ne-atomiskām izmaiņām. Nu kods, kas visu dara, ātri pārvēršas par putru.

В nopietnāks projekts Es izmantoju klasisko preemptive multitasking un FreeRTOS, taču šajā gadījumā modelis izrādījās daudz piemērotāks korutīnas un uasync bibliotēkas . Turklāt Python korutīnu ieviešana ir vienkārši pārsteidzoša - viss tiek darīts vienkārši un programmētājam ērti. Vienkārši uzrakstiet savu loģiku, vienkārši pasakiet man, kurās vietās varat pārslēgties starp straumēm.

Es iesaku izpētīt atšķirības starp preventīvo un konkurētspējīgo daudzuzdevumu veikšanu kā izvēles priekšmetu. Tagad beidzot pāriesim pie koda.

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

Katru skaitītāju apstrādā klases Counter gadījums. Pirmkārt, sākotnējā skaitītāja vērtība tiek atņemta no EEPROM (value_storage) - šādi tiek realizēta atkopšana pēc strāvas padeves pārtraukuma.

Taps tiek inicializēts ar iebūvētu pievilkšanos pie barošanas avota: ja niedres slēdzis ir aizvērts, līnija ir nulle, ja līnija ir atvērta, tā tiek pievilkta līdz barošanas avotam un kontrolleris nolasa vienu.

Šeit tiek palaists arī atsevišķs uzdevums, kas aptaujās tapu. Katrs skaitītājs veiks savu uzdevumu. Šeit ir viņas kods

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

Lai filtrētu kontakta atlēcienu, ir nepieciešama 25 ms aizkave, un tajā pašā laikā tiek regulēts, cik bieži uzdevums pamostas (kamēr šis uzdevums guļ, citi uzdevumi darbojas). Ik pēc 25 ms funkcija pamostas, pārbauda tapu un ja niedres slēdža kontakti ir aizvērti, tad skaitītājam ir izgājis vēl viens litrs un tas ir jāapstrādā.

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

        self._value_storage.write(self._value)

Nākamā litra apstrāde ir triviāla - skaitītājs vienkārši palielinās. Būtu jauki ierakstīt jauno vērtību zibatmiņas diskā.

Lietošanas ērtībai tiek nodrošināti “piederumi”.

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

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

Nu, tagad izmantosim Python un uasync bibliotēkas sniegtās priekšrocības un izveidosim gaidāmu skaitītāju objektu (kā mēs to varam pārtulkot krievu valodā? To, kādu jūs varat sagaidīt?)

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

        return self.value()

    __iter__ = __await__  

Šī ir tik ērta funkcija, kas gaida, līdz tiek atjaunināta skaitītāja vērtība - funkcija ik pa laikam pamostas un pārbauda karogu _value_changed. Šīs funkcijas foršais aspekts ir tas, ka zvanīšanas kods var aizmigt šīs funkcijas izsaukšanas laikā un gulēt, līdz tiek saņemta jauna vērtība.

Kā ar pārtraukumiem?Jā, šajā brīdī vari mani troļļot, sakot, ka pats teici par pārtraukumiem, bet patiesībā tu veici stulbu pin aptauju. Patiesībā pārtraukumi ir pirmā lieta, ko mēģināju. Programmā ESP8266 varat organizēt malas pārtraukumu un pat uzrakstīt šī pārtraukuma apstrādātāju programmā Python. Šajā pārtraukumā mainīgā vērtību var atjaunināt. Droši vien ar to pietiktu, ja skaitītājs būtu vergu ierīce – tāda, kas gaida, līdz tiek prasīta šī vērtība.

Diemžēl (vai par laimi?) mana ierīce ir aktīva, tai pašai jāsūta ziņojumi caur MQTT protokolu un jāraksta dati uz EEPROM. Un šeit stājas spēkā ierobežojumi - jūs nevarat piešķirt atmiņu pārtraukumos un izmantot lielu steku, kas nozīmē, ka varat aizmirst par ziņojumu sūtīšanu tīklā. Ir tādas bulciņas kā micropython.schedule(), kas ļauj palaist kādu funkciju “cik drīz vien iespējams”, taču rodas jautājums: “kāda jēga?” Ko darīt, ja mēs šobrīd nosūtām kādu ziņojumu, un tad ienāk pārtraukums un sabojā mainīgo vērtības. Vai, piemēram, no servera atnāca jauna skaitītāja vērtība, kamēr mēs vēl nebijām pierakstījuši veco. Kopumā jums ir jābloķē sinhronizācija vai jāiziet no tās kaut kā savādāk.

Un laiku pa laikam RuntimeError: ieplānojiet pilnas steka avārijas un kas zina, kāpēc?

Izmantojot skaidru aptauju un uasync, šajā gadījumā tas kaut kā izrādās skaistāks un uzticamāks

Es atnesu darbu ar EEPROM uz mazo klasi

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)

Python ir grūti strādāt tieši ar baitiem, bet tieši baiti tiek ierakstīti atmiņā. Man bija jānovērš konvertēšana starp veseliem skaitļiem un baitiem, izmantojot ustruct bibliotēku.

Lai katru reizi nepārnestu I2C objektu un atmiņas šūnas adresi, to visu ietinu mazā un ērtā klasikā

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)

Pats I2C objekts tiek izveidots ar šiem parametriem

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

Mēs nonākam pie interesantākās daļas - komunikācijas ar serveri ieviešanas caur MQTT. Nu nav jārealizē pats protokols - atradu internetā gatava asinhronā ieviešana. Tas ir tas, ko mēs izmantosim.

Visas interesantākās lietas ir apkopotas CounterMQTTClient klasē, kuras pamatā ir bibliotēka MQTTClient. Sāksim no perifērijas

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

Šeit var izveidot un konfigurēt spuldžu tapas un pogas, kā arī aukstā un karstā ūdens skaitītāju objektus.

Ar inicializāciju ne viss ir tik triviāls

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

Lai iestatītu bibliotēkas mqtt_as darbības parametrus, tiek izmantota liela dažādu iestatījumu vārdnīca - config. Lielākā daļa noklusējuma iestatījumu mums ir piemēroti, taču daudzi iestatījumi ir jāiestata tieši. Lai uzstādījumus nerakstītu tieši kodā, saglabāju tos teksta failā config.txt. Tas ļauj mainīt kodu neatkarīgi no iestatījumiem, kā arī kniedēt vairākas identiskas ierīces ar dažādiem parametriem.

Pēdējais koda bloks sāk vairākas korutīnas, kas apkalpo dažādas sistēmas funkcijas. Piemēram, šeit ir korutīna, kas apkalpo skaitītājus

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

Korutīna cilpā gaida jaunu skaitītāja vērtību un, tiklīdz tā parādās, nosūta ziņojumu, izmantojot MQTT protokolu. Pirmais koda fragments nosūta sākotnējo vērtību pat tad, ja caur skaitītāju neplūst ūdens.

Bāzes klase MQTTClient apkalpo sevi, iniciē WiFi savienojumu un atjauno savienojumu, kad savienojums tiek zaudēts. Ja notiek izmaiņas WiFi savienojuma stāvoklī, bibliotēka mūs informē, zvanot uz 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)

Funkcija ir godīgi kopēta no piemēriem. Šajā gadījumā tas uzskaita pārtraukumu skaitu (internet_outages) un to ilgumu. Kad savienojums tiek atjaunots, serverim tiek nosūtīts dīkstāves laiks.

Starp citu, pēdējais miegs ir nepieciešams tikai, lai funkciju padarītu asinhronu - bibliotēkā tā tiek izsaukta via await, un var izsaukt tikai tās funkcijas, kuru ķermenī ir vēl viens await.

Papildus savienojuma izveidei ar WiFi, jums ir arī jāizveido savienojums ar MQTT brokeri (serveri). Bibliotēka arī to dara, un mēs iegūstam iespēju darīt kaut ko noderīgu, kad savienojums ir izveidots

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

Šeit mēs abonējam vairākus ziņojumus - serverim tagad ir iespēja iestatīt pašreizējās skaitītāja vērtības, nosūtot atbilstošo ziņojumu.

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

Šī funkcija apstrādā ienākošos ziņojumus un atkarībā no tēmas (ziņojuma nosaukuma) tiek atjauninātas viena skaitītāja vērtības

Pāris palīgfunkcijas

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

Šī funkcija nosūta ziņojumu, ja savienojums ir izveidots. Ja nav savienojuma, ziņojums tiek ignorēts.

Un šī ir tikai ērta funkcija, kas ģenerē un nosūta atkļūdošanas ziņojumus.

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

Tik daudz teksta, un mēs vēl neesam mirgojuši LED. Šeit

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

Esmu nodrošinājis 2 mirgošanas režīmus. Ja savienojums tiek zaudēts (vai tas tikai tiek izveidots), ierīce ātri mirgo. Ja savienojums ir izveidots, ierīce mirgo reizi 5 sekundēs. Ja nepieciešams, šeit var ieviest citus mirgošanas režīmus.

Bet gaismas diode tikai lutina. Mēs arī mērķējām uz displeju.

    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)

Tas ir tas, par ko es runāju - cik vienkārši un ērti tas ir ar korutīnām. Šī mazā funkcija apraksta VISU lietotāja pieredzi. Korutīna vienkārši gaida, līdz tiek nospiesta poga, un ieslēdz displeju uz 3 sekundēm. Displejs parāda pašreizējos skaitītāja rādījumus.

Vēl palikuši pāris sīkumi. Šeit ir funkcija, kas (atkārtoti) sāk visu šo uzņēmumu. Galvenā cilpa tikai reizi minūtē nosūta dažādu atkļūdošanas informāciju. Kopumā es citēju to tādu, kāds tas ir - manuprāt, nav nepieciešams pārāk daudz komentēt

   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, vēl pāris iestatījumi un konstantes, lai pabeigtu aprakstu

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

Viss sākas šādi

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

Kaut kas notika ar manu atmiņu

Tātad viss kods ir tur. Es augšupielādēju failus, izmantojot utilītu ampy - tas ļauj augšupielādēt tos iekšējā (pašā ESP-07) zibatmiņas diskā un pēc tam piekļūt tam no programmas kā parastiem failiem. Tur es arī augšupielādēju mqtt_as, uasyncio, ssd1306 un kolekciju bibliotēkas, kuras izmantoju (izmantotas mqtt_as iekšpusē).

Mēs palaižam un... Mēs saņemam MemoryError. Turklāt, jo vairāk mēģināju saprast, kur tieši noplūst atmiņa, jo vairāk atkļūdošanas izdruku ievietoju, jo agrāk šī kļūda parādījās. Īsa Google meklēšana lika man saprast, ka mikrokontrolleram principā ir tikai 30 kB atmiņa, kurā 65 kB koda (ieskaitot bibliotēkas) vienkārši nevar ievietot.

Bet ir izeja. Izrādās, micropython neizpilda kodu tieši no .py faila – šis fails tiek kompilēts pirmais. Turklāt tas tiek apkopots tieši uz mikrokontrollera, pārvērsts baitkodā, kas pēc tam tiek saglabāts atmiņā. Nu, lai kompilators darbotos, ir nepieciešams arī noteikts RAM apjoms.

Triks ir glābt mikrokontrolleri no resursietilpīgas kompilācijas. Jūs varat apkopot failus uz liela datora un augšupielādēt gatavo baitkodu mikrokontrollerī. Lai to izdarītu, jums ir jālejupielādē micropython programmaparatūra un jāizveido mpy-cross utilīta.

Es nerakstīju Makefile, bet manuāli izgāju cauri un apkopoju visus nepieciešamos failus (ieskaitot bibliotēkas) kaut ko līdzīgu šim

mpy-cross water_counter.py

Atliek tikai augšupielādēt failus ar paplašinājumu .mpy, neaizmirstot vispirms izdzēst atbilstošo .py no ierīces failu sistēmas.

Es veicu visu izstrādi programmā (IDE?) ESPlorer. Tas ļauj augšupielādēt skriptus mikrokontrollerī un nekavējoties tos izpildīt. Manā gadījumā visa loģika un visu objektu izveide atrodas failā water_counter.py (.mpy). Bet, lai tas viss sāktos automātiski, sākumā ir jābūt arī failam ar nosaukumu main.py. Turklāt tam jābūt tieši .py, nevis iepriekš kompilētam .mpy. Šeit ir tā triviālais saturs

import water_counter

Mēs to palaižam - viss darbojas. Taču brīvā atmiņa ir satraucoši maza – apmēram 1kb. Man joprojām ir plāni paplašināt ierīces funkcionalitāti, un ar šo kilobaitu man acīmredzami nepietiek. Taču izrādījās, ka arī šai lietai ir izeja.

Lūk, lieta. Lai gan faili tiek apkopoti baitkodā un atrodas iekšējā failu sistēmā, patiesībā tie joprojām tiek ielādēti RAM un tiek izpildīti no turienes. Bet izrādās, ka micropython var izpildīt baitkodu tieši no zibatmiņas, taču šim nolūkam tas ir jāiebūvē tieši programmaparatūrā. Tas nav grūti, lai gan manā netbook tas prasīja diezgan ilgu laiku (tikai tur man bija Linux).

Algoritms ir šāds:

  • Lejupielādējiet un instalējiet ESP Open SDK. Šī lieta apkopo ESP8266 programmu kompilatoru un bibliotēkas. Samontēts saskaņā ar norādījumiem projekta galvenajā lapā (izvēlējos iestatījumu STANDALONE=yes)
  • lejuplādēt mikropitona veidi
  • Ievietojiet vajadzīgās bibliotēkas portos/esp8266/modules micropython koka iekšpusē
  • Mēs saliekam programmaparatūru saskaņā ar failā sniegtajiem norādījumiem ports/esp8266/README.md
  • Mēs augšupielādējam programmaparatūru mikrokontrollerī (es to daru operētājsistēmā Windows, izmantojot ESP8266Flasher programmas vai Python esptool)

Tas arī viss, tagad 'importēt ssd1306' pacels kodu tieši no programmaparatūras, un RAM šim nolūkam netiks patērēta. Izmantojot šo triku, es programmaparatūrā augšupielādēju tikai bibliotēkas kodu, savukārt galvenais programmas kods tiek izpildīts no failu sistēmas. Tas ļauj viegli modificēt programmu, nepārkompilējot programmaparatūru. Šobrīd man ir aptuveni 8.5 kb brīvas RAM. Tas ļaus mums nākotnē ieviest diezgan daudz dažādu noderīgu funkcionalitāti. Nu, ja vispār nav pietiekami daudz atmiņas, varat iespiest galveno programmu programmaparatūrā.

Tātad, kas mums tagad ar to būtu jādara?

Ok, aparatūra pielodēta, programmaparatūra uzrakstīta, kaste izdrukāta, aparāts pielipis pie sienas un priecīgi mirkšķina spuldzīti. Bet pagaidām tas viss ir melnā kaste (tiešā un pārnestā nozīmē), un tas joprojām ir maz noderīgs. Ir pienācis laiks kaut ko darīt ar MQTT ziņojumiem, kas tiek nosūtīti uz serveri.

Mana “viedā māja” griežas Majordomo sistēma. MQTT modulis tiek izņemts no kastes vai ir viegli uzstādīts no papildinājumu tirgus — es neatceros, no kurienes to ieguvu. MQTT nav pašpietiekama lieta – vajag t.s. brokeris - serveris, kas saņem, kārto un pārsūta MQTT ziņojumus klientiem. Es izmantoju mosquitto, kas (tāpat kā majordomo) darbojas tajā pašā netbook.

Pēc tam, kad ierīce vismaz vienu reizi nosūtīs ziņojumu, vērtība nekavējoties parādīsies sarakstā.

Mēs savienojam ūdens skaitītāju ar viedo māju

Šīs vērtības tagad var saistīt ar sistēmas objektiem, tās var izmantot automatizācijas skriptos un pakļaut dažādām analīzēm - tas viss ir ārpus šī raksta darbības jomas. Varu ieteikt majordomo sistēmu ikvienam interesentam kanāls Electronics In Lens — draugs arī būvē viedo māju un skaidri runā par sistēmas uzstādīšanu.

Es jums parādīšu tikai pāris grafikus. Šis ir vienkāršs ikdienas vērtību grafiks

Mēs savienojam ūdens skaitītāju ar viedo māju
Redzams, ka pa nakti ūdeni gandrīz neviens neizmantoja. Pāris reizes kāds gāja uz tualeti, un šķiet, ka reversās osmozes filtrs iesūc pāris litrus uz nakti. No rīta patēriņš ievērojami palielinās. Parasti izmantoju ūdeni no boilera, bet tad sagribējās vannā un uz laiku pārgāju uz pilsētas karsto ūdeni - tas arī labi redzams apakšējā grafikā.

No šī grafika uzzināju, ka, lai dotos uz tualeti, nepieciešami 6-7 litri ūdens, dušā – 20-30 litri, trauku mazgāšanai – aptuveni 20 litri, vannā – 160 litri. Mana ģimene patērē kaut kur 500-600 litrus dienā.

Īpaši ziņkārīgie var apskatīt ierakstus par katru atsevišķu vērtību

Mēs savienojam ūdens skaitītāju ar viedo māju

No šejienes es uzzināju, ka, ja krāns ir atvērts, ūdens plūst ar ātrumu aptuveni 1 litrs 5 s.

Bet šādā formā statistiku droši vien nav īpaši ērti apskatīt. Majordomo ir arī iespēja skatīt patēriņa diagrammas pēc dienas, nedēļas un mēneša. Šeit, piemēram, ir patēriņa grafiks joslās

Mēs savienojam ūdens skaitītāju ar viedo māju

Pagaidām man ir dati tikai par nedēļu. Pēc mēneša šis grafiks būs orientējošāks – katrai dienai būs atsevišķa kolonna. Attēlu nedaudz sabojā manuāli ievadīto vērtību pielāgojumi (lielākā kolonna). Un vēl nav skaidrs, vai es nepareizi iestatīju pašas pirmās vērtības, gandrīz par kubu mazāk, vai arī tā ir programmaparatūras kļūda un visi litri nav saskaitīti. Vajag vairāk laika.

Pašiem grafikiem vēl ir vajadzīga maģija, balināšana, krāsošana. Varbūt es arī izveidošu atmiņas patēriņa grafiku atkļūdošanas nolūkos - ja tur kaut kas noplūst. Varbūt es kaut kā parādīšu periodus, kad nebija interneta. Pagaidām tas viss ir ideju līmenī.

Secinājums

Šodien mans dzīvoklis ir kļuvis mazliet gudrāks. Ar tik mazu ierīci man būs ērtāk uzraudzīt ūdens patēriņu mājā. Ja agrāk biju sašutis par “mēnesī atkal patērējām daudz ūdens”, tad tagad varu atrast šī patēriņa avotu.

Dažiem var šķist dīvaini skatīties uz rādījumiem ekrānā, ja tas atrodas metra attālumā no paša skaitītāja. Bet ne pārāk tālā nākotnē plānoju pārvākties uz citu dzīvokli, kur būs vairāki ūdens stāvvadi, un paši skaitītāji, visticamāk, atradīsies uz nolaišanās. Tātad attālās nolasīšanas ierīce būs ļoti noderīga.

Tāpat plānoju paplašināt ierīces funkcionalitāti. Es jau skatos uz motorizētiem vārstiem. Tagad, lai pārslēgtu katlu uz pilsētas ūdeni, man ir jāpagriež 3 krāni grūti sasniedzamā nišā. Daudz ērtāk to būtu izdarīt ar vienu pogu ar atbilstošu norādi. Protams, ir vērts ieviest aizsardzību pret noplūdēm.

Rakstā es aprakstīju savu ierīces versiju, kuras pamatā ir ESP8266. Manuprāt, es izdomāju ļoti interesantu micropython programmaparatūras versiju, izmantojot korutīnas - vienkāršu un jauku. Mēģināju aprakstīt daudzas nianses un nepilnības, ar kurām saskāros kampaņas laikā. Iespējams, es visu aprakstīju pārāk detalizēti; personīgi man kā lasītājam ir vieglāk pārlaist nevajadzīgās lietas, nekā vēlāk izdomāt, kas palika nepateikts.

Kā vienmēr, esmu atvērts konstruktīvai kritikai.

Pirmkods
Shēma un plate
Korpusa modelis

Avots: www.habr.com

Pievieno komentāru