Մենք ջրաչափը միացնում ենք խելացի տանը

Ժամանակին տնային ավտոմատացման համակարգերը կամ, ինչպես հաճախ անվանում էին «խելացի տուն», սարսափելի թանկ էին, և միայն հարուստները կարող էին իրենց թույլ տալ: Այսօր շուկայում դուք կարող եք գտնել բավականին էժան կոմպլեկտներ՝ սենսորներով, կոճակներով/անջատիչներով և շարժիչներով՝ լուսավորության, վարդակների, օդափոխության, ջրամատակարարման և այլ սպառողների կառավարման համար: Եվ նույնիսկ ամենակուռ մարդը կարող է զբաղվել գեղեցկությամբ և խելացի տան համար սարքեր հավաքել էժան գնով:

Մենք ջրաչափը միացնում ենք խելացի տանը

Սովորաբար, առաջարկվող սարքերը կամ սենսորներ են, կամ շարժիչներ: Դրանք հեշտացնում են այնպիսի սցենարների իրականացումը, ինչպիսիք են «երբ շարժման սենսորը գործարկվում է, միացրեք լույսերը» կամ «Ելքի մոտ գտնվող անջատիչը անջատում է լույսերը ամբողջ բնակարանում»: Բայց ինչ-որ կերպ ամեն ինչ չստացվեց հեռաչափությամբ: Լավագույն դեպքում դա ջերմաստիճանի և խոնավության գրաֆիկ է կամ ակնթարթային հզորությունը կոնկրետ վարդակից:

Վերջերս տեղադրեցի ջրաչափեր իմպուլսային ելքով: Հաշվիչի միջով անցնող յուրաքանչյուր լիտրի համար եղեգի անջատիչն ակտիվանում է և փակում կոնտակտը: Մնում է միայն կառչել լարերից և փորձել դրանից օգուտ քաղել։ Օրինակ՝ վերլուծեք ջրի սպառումը ըստ շաբաթվա ժամերի և օրերի: Դե, եթե բնակարանում կան մի քանի ջրի բարձրացուցիչներ, ապա ավելի հարմար է տեսնել բոլոր ընթացիկ ցուցանիշները մեկ էկրանին, քան լապտերով բարձրանալ դեպի դժվար հասանելի խորշեր:

Կտրվածքի տակ դրված է ESP8266-ի վրա հիմնված սարքի իմ տարբերակը, որը հաշվում է իմպուլսները ջրաչափերից և ընթերցումներ ուղարկում MQTT-ի միջոցով խելացի տան սերվերին: Մենք կծրագրավորենք micropython-ում՝ օգտագործելով uasyncio գրադարանը։ Որոնվածը ստեղծելիս ես հանդիպեցի մի քանի հետաքրքիր դժվարությունների, որոնք ես նույնպես կքննարկեմ այս հոդվածում: Գնա՛

Ծրագիրը

Մենք ջրաչափը միացնում ենք խելացի տանը

Ամբողջ շղթայի սիրտը մի մոդուլ է ESP8266 միկրոկոնտրոլերի վրա: ESP-12-ը ի սկզբանե նախատեսված էր, բայց իմը թերի էր: Մենք պետք է բավարարվեինք ESP-07 մոդուլով, որը հասանելի էր: Բարեբախտաբար, դրանք նույնն են և՛ քորոցների, և՛ ֆունկցիոնալության առումով, տարբերությունը միայն ալեհավաքի մեջ է՝ ESP-12-ն ունի ներկառուցված, իսկ ESP-07-ը՝ արտաքին: Այնուամենայնիվ, նույնիսկ առանց WiFi ալեհավաքի, իմ լոգարանում ազդանշանը ստացվում է նորմալ:

Ստանդարտ մոդուլի լարերը.

  • զրոյական կոճակ՝ ձգվող և կոնդենսատորով (չնայած երկուսն էլ արդեն մոդուլի ներսում են)
  • Միացման ազդանշանը (CH_PD) միացված է սնուցման
  • GPIO15-ը քաշվում է գետնին: Սա միայն սկզբում է պետք, բայց ես դեռ ոչինչ չունեմ այս ոտքին կապելու, ինձ այլևս պետք չէ

Մոդուլը որոնվածի ռեժիմի մեջ դնելու համար անհրաժեշտ է GPIO2-ը կարճ միացնել գետնին, իսկ այն ավելի հարմար դարձնելու համար ես տրամադրեցի Boot կոճակը: Նորմալ վիճակում այս քորոցը միացված է հոսանքի:

GPIO2 գծի վիճակը ստուգվում է միայն շահագործման սկզբում` հոսանքի կիրառման ժամանակ կամ վերականգնումից անմիջապես հետո: Այսպիսով, մոդուլը կա՛մ բեռնվում է սովորականի պես, կա՛մ անցնում է որոնվածի ռեժիմ: Բեռնվելուց հետո այս փին կարող է օգտագործվել որպես սովորական GPIO: Դե, քանի որ այնտեղ արդեն կա կոճակ, կարող եք դրան մի քանի օգտակար ֆունկցիա կցել։

Ծրագրավորման և վրիպազերծման համար ես կօգտագործեմ UART-ը, որը թողարկվում է սանրում: Անհրաժեշտության դեպքում ես այնտեղ պարզապես միացնում եմ USB-UART ադապտեր: Պարզապես պետք է հիշել, որ մոդուլը սնուցվում է 3.3 Վ-ով: Եթե ​​մոռանաք ադապտորը միացնել այս լարման և մատակարարել 5 Վ, մոդուլը, ամենայն հավանականությամբ, կվառվի:

Լոգարանում հոսանքի հետ խնդիր չունեմ. վարդակից գտնվում է հաշվիչների հեռավորության վրա մոտ մեկ մետր հեռավորության վրա, այնպես որ ես այն կսնուցեմ 220 Վ-ից։ Որպես էներգիայի աղբյուր ես կունենամ փոքր բլոկ HLK-PM03 Tenstar Robot-ի կողմից: Անձամբ ես դժվարանում եմ անալոգային և ուժային էլեկտրոնիկայի հետ կապված, բայց ահա պատրաստի սնուցման աղբյուրը փոքր պատյանով:

Գործառնական ռեժիմները ազդանշան տալու համար ես տրամադրեցի GPIO2-ին միացված լուսադիոդ: Այնուամենայնիվ, ես այն չզոդեցի, քանի որ... ESP-07 մոդուլն արդեն ունի լուսադիոդ, այն նաև միացված է GPIO2-ին։ Բայց թող լինի տախտակի վրա, եթե ես ուզում եմ այս լուսադիոդը դուրս բերել գործին:

Անցնենք ամենահետաքրքիր հատվածին։ Ջրաչափերը ոչ մի տրամաբանություն չունեն. Մեզ հասանելի միակ բանը իմպուլսներն են՝ եղեգի անջատիչի կոնտակտները փակել ամեն լիտրով։ Իմ եղեգի անջատիչի ելքերը միացված են GPIO12/GPIO13-ին: Ես ծրագրային կերպով միացնեմ քաշվող դիմադրությունը մոդուլի ներսում:

Սկզբում ես մոռացել էի R8 և R9 ռեզիստորներ տրամադրել, և տախտակի իմ տարբերակում դրանք չկան: Բայց քանի որ ես արդեն տեղադրում եմ դիագրամը, որպեսզի տեսնեն բոլորը, արժե շտկել այս անտեսումը: Ռեզիստորները անհրաժեշտ են, որպեսզի չվառեն պորտը, եթե որոնվածը խափանում է և պտտեցնում է մեկին, և եղեգի անջատիչը այս գիծը կտրում է գետնին (առավելագույնը 3.3 Վ/1000 Օհմ = 3.3 մԱ ռեզիստորի դեպքում կհոսի):

Ժամանակն է մտածել, թե ինչ անել, եթե հոսանքը անջատվի։ Առաջին տարբերակը սկզբում սերվերից սկզբնական հաշվիչների արժեքներ պահանջելն է: Բայց դա կպահանջի փոխանակման արձանագրության զգալի բարդացում: Ավելին, սարքի աշխատանքը տվյալ դեպքում կախված է սերվերի վիճակից։ Եթե ​​սերվերը չգործարկվեր հոսանքն անջատվելուց հետո (կամ ավելի ուշ գործարկվել), ապա ջրաչափը չէր կարողանա պահանջել նախնական արժեքները և ճիշտ չէր աշխատի:

Հետևաբար, ես որոշեցի կիրառել հաշվիչի արժեքների պահպանումը I2C-ի միջոցով միացված հիշողության չիպի մեջ: Ֆլեշ հիշողության չափի համար հատուկ պահանջներ չունեմ. անհրաժեշտ է պահպանել միայն 2 համար (լիտրերի քանակը՝ ըստ տաք և սառը ջրաչափերի): Անգամ ամենափոքր մոդուլը կարող է անել: Բայց դուք պետք է ուշադրություն դարձնեք ձայնագրման ցիկլերի քանակին: Մոդուլների մեծ մասի համար սա 100 հազար ցիկլ է, որոշների համար՝ մինչև միլիոն:

Թվում է, թե միլիոնը շատ է։ Բայց իմ բնակարանում ապրելու 4 տարիների ընթացքում ես սպառել եմ 500 խմ-ից մի փոքր ավելի ջուր, այսինքն՝ 500 հազար լիտր: Եվ 500 հազար ձայնագրություն՝ ֆլեշում։ Եվ դա պարզապես սառը ջուր է: Դուք, իհարկե, կարող եք վերազոդել չիպը երկու տարին մեկ, բայց պարզվում է, որ կան FRAM չիպեր: Ծրագրավորման տեսանկյունից սա նույն I2C EEPROM-ն է՝ միայն շատ մեծ թվով վերագրանցման ցիկլերով (հարյուր միլիոններ): Պարզապես ես դեռ չեմ կարող խանութ հասնել նման միկրոսխեմաներով, ուստի առայժմ սովորական 24LC512-ը կկանգնի:

Տպագիր տպատախտակ

Սկզբում պլանավորել էի տախտակը պատրաստել տանը։ Հետեւաբար, խորհուրդը նախագծվել է որպես միակողմանի: Բայց մեկ ժամ լազերային արդուկի և զոդման դիմակի հետ անցկացնելուց հետո (առանց դրա ինչ-որ կերպ չի ստացվում), ես դեռ որոշեցի պատվիրել տախտակները չինացիներից:

Մենք ջրաչափը միացնում ենք խելացի տանը

Գրեթե նախքան տախտակը պատվիրելը, ես հասկացա, որ բացի ֆլեշ հիշողության չիպից, կարող եմ I2C ավտոբուսին միացնել մեկ այլ օգտակար բան, օրինակ՝ դիսփլեյ: Թե կոնկրետ ինչ արդյունք տալ դրան, դեռ հարց է, բայց այն պետք է ուղղորդվի տախտակի վրա: Դե, քանի որ ես պատրաստվում էի տախտակներ պատվիրել գործարանից, իմաստ չկար սահմանափակվել միայն միակողմանի տախտակով, ուստի I2C տողերը միակն են տախտակի հետևի մասում:

Մի մեծ խնդիր կար նաև միակողմանի լարերի հետ կապված. Որովհետեւ Տախտակը գծված էր որպես միակողմանի, ուստի պլանավորվում էր, որ հետքերը և SMD բաղադրիչները տեղադրվեին մի կողմում, իսկ ելքային բաղադրիչները, միակցիչները և էլեկտրամատակարարումը մյուս կողմից: Երբ ես ստացա տախտակները մեկ ամիս անց, ես մոռացա սկզբնական պլանի մասին և զոդեցի բոլոր բաղադրիչները առջևի կողմում: Եվ միայն այն ժամանակ, երբ խոսքը գնում էր էլեկտրամատակարարման զոդման մասին, պարզվեց, որ պլյուսն ու մինուսը լարերը հակառակն էին: Ես ստիպված էի ֆերմա վարել ցատկողներով: Վերևի նկարում ես արդեն փոխել եմ լարերը, բայց գետինը տեղափոխվում է տախտակի մի մասից մյուսը Boot կոճակի քորոցների միջոցով (չնայած երկրորդ շերտի վրա հնարավոր կլիներ հետք նկարել):

Ստացվեց այսպես

Մենք ջրաչափը միացնում ենք խելացի տանը

Բնակարանային պայմանները

Հաջորդ քայլը մարմինն է: Եթե ​​դուք ունեք 3D տպիչ, դա խնդիր չէ: Ես շատ չանհանգստացա. ես պարզապես նկարեցի ճիշտ չափի տուփ և ճիշտ տեղերում կտրվածքներ արեցի: Կափարիչը ամրացվում է մարմնի վրա փոքր ինքնակպչուն պտուտակներով:

Մենք ջրաչափը միացնում ենք խելացի տանը

Ես արդեն նշեցի, որ Boot կոճակը կարող է օգտագործվել որպես ընդհանուր նշանակության կոճակ, այնպես որ մենք այն կցուցադրենք առջևի վահանակում: Դա անելու համար ես նկարեցի հատուկ «ջրհոր», որտեղ ապրում է կոճակը:

Մենք ջրաչափը միացնում ենք խելացի տանը

Գործի ներսում կան նաև գամասեղներ, որոնց վրա տախտակը տեղադրվում և ամրացվում է մեկ M3 պտուտակով (տախտակի վրա այլևս տեղ չկար)

Ես ընտրեցի էկրանը, երբ տպեցի գործի առաջին օրինակելի տարբերակը: Ստանդարտ երկտող ընթերցողը չէր տեղավորվում այս գործի մեջ, բայց ներքևում կար OLED էկրան SSD1306 128×32: Դա մի փոքր փոքր է, բայց ես չպետք է ամեն օր նայեմ դրան, դա չափազանց շատ է ինձ համար:

Պարզելով այս և այն մասին, թե ինչպես են հաղորդալարերը հեռանալու դրանից, ես որոշեցի էկրանը կպցնել պատյանի մեջտեղում: Էրգոնոմիկա, իհարկե, ցածր մակարդակի վրա է՝ կոճակը վերևում է, էկրանը՝ ներքևում: Բայց ես արդեն ասացի, որ էկրանը կցելու գաղափարը շատ ուշ եկավ, և ես չափազանց ծույլ էի տախտակը նորից լարել կոճակը տեղափոխելու համար:

Սարքը հավաքված է։ Ցուցադրման մոդուլը տաք սոսինձով կպչում է ճյուղին

Մենք ջրաչափը միացնում ենք խելացի տանը

Մենք ջրաչափը միացնում ենք խելացի տանը

Վերջնական արդյունքը կարելի է տեսնել KDPV-ում

Որոնվածը

Անցնենք ծրագրային մասին։ Նման փոքր արհեստների համար ես իսկապես սիրում եմ օգտագործել Python (միկրոպիթոն) - կոդը ստացվում է շատ կոմպակտ և հասկանալի: Բարեբախտաբար, կարիք չկա իջնել ռեգիստրի մակարդակին՝ միկրովայրկյանները սեղմելու համար. ամեն ինչ կարելի է անել Python-ից:

Թվում է, թե ամեն ինչ պարզ է, բայց ոչ շատ պարզ. սարքն ունի մի քանի անկախ գործառույթներ.

  • Օգտագործողը սեղմում է կոճակը և նայում էկրանին
  • Լիտրերը նշում և թարմացնում են արժեքները ֆլեշ հիշողության մեջ
  • Մոդուլը վերահսկում է WiFi ազդանշանը և անհրաժեշտության դեպքում նորից միանում
  • Դե, առանց թարթող լամպի դա անհնար է

Դուք չեք կարող ենթադրել, որ մի գործառույթ չի աշխատել, եթե մյուսը ինչ-ինչ պատճառներով խրված է: Ես արդեն կակտուսներով եմ լցվել այլ նախագծերում, և հիմա դեռ տեսնում եմ խափանումներ՝ «բաց թողեցի ևս մեկ լիտր, քանի որ էկրանն այդ պահին թարմացվում էր» կամ «օգտատերը չի կարող որևէ բան անել, մինչ մոդուլը միանում է»: WiFi»: Իհարկե, որոշ բաներ կարելի է անել ընդհատումների միջոցով, բայց դուք կարող եք բախվել տևողության, զանգերի տեղադրման կամ փոփոխականների ոչ ատոմային փոփոխությունների հետ կապված սահմանափակումների: Դե, ամեն ինչ կատարող կոդը արագ վերածվում է մուշի։

В ավելի լուրջ նախագիծ Ես օգտագործեցի դասական կանխարգելիչ բազմաֆունկցիոնալ աշխատանք և FreeRTOS, բայց այս դեպքում մոդելը շատ ավելի հարմար է ստացվել coroutines և uasync գրադարաններ . Ավելին, Python-ի կորուտինների իրականացումը պարզապես զարմանալի է. ամեն ինչ արվում է պարզ և հարմար ծրագրավորողի համար: Պարզապես գրեք ձեր սեփական տրամաբանությունը, պարզապես ասեք ինձ, թե որ վայրերում կարող եք անցնել հոսքերի միջև:

Առաջարկում եմ ուսումնասիրել կանխարգելիչ և մրցակցային բազմաբնույթ առաջադրանքների միջև եղած տարբերությունները որպես ընտրովի առարկա: Հիմա վերջապես անցնենք ծածկագրին։

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

Յուրաքանչյուր հաշվիչ կառավարվում է Counter դասի օրինակով: Նախ, սկզբնական հաշվիչի արժեքը հանվում է EEPROM-ից (արժեք_պահեստավորում) - այսպես է իրականացվում վերականգնումը հոսանքազրկումից հետո:

Քորոցը սկզբնավորվում է ներկառուցված հոսանքի սնուցման միջոցով. եթե եղեգի անջատիչը փակ է, գիծը զրոյական է, եթե գիծը բաց է, այն քաշվում է դեպի սնուցման աղբյուր, և վերահսկիչը կարդում է մեկը:

Այստեղ գործարկվում է նաև առանձին առաջադրանք, որը կփոշիացնի: Յուրաքանչյուր հաշվիչ կկատարի իր առաջադրանքը: Ահա նրա կոդը

    """ Poll pin and advance value when another litre passed """
    async def _switchcheck(self):
        last_checked_pin_state = self._pin.value()  # Get initial state

        # Poll for a pin change
        while True:
            state = self._pin.value()
            if state != last_checked_pin_state:
                # State has changed: act on it now.
                last_checked_pin_state = state
                if state == 0:
                    self._another_litre_passed()

            # Ignore further state changes until switch has settled
            await asyncio.sleep_ms(Counter.debounce_ms)

Կոնտակտային ցատկումը զտելու համար անհրաժեշտ է 25 մվ ուշացում, և միևնույն ժամանակ այն կարգավորում է, թե որքան հաճախ է առաջադրանքն արթնանում (մինչ այս առաջադրանքը քնած է, այլ առաջադրանքներ են աշխատում): Յուրաքանչյուր 25 ms ֆունկցիան արթնանում է, ստուգում է քորոցը և եթե եղեգի անջատիչի կոնտակտները փակ են, ապա ևս մեկ լիտր անցել է հաշվիչի միջով, և այն պետք է մշակվի:

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

        self._value_storage.write(self._value)

Հաջորդ լիտրը մշակելը աննշան է. հաշվիչը պարզապես մեծանում է: Դե, լավ կլիներ նոր արժեքը գրել ֆլեշ կրիչի վրա:

Օգտագործման հարմարավետության համար տրամադրվում են «աքսեսուարներ»:

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

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

Դե, հիմա եկեք օգտվենք Python-ի և uasync գրադարանի հաճույքներից և պատրաստենք սպասելի հաշվառման օբյեկտ (ինչպե՞ս կարող ենք սա թարգմանել ռուսերեն: Այն, ինչ կարող եք ակնկալել):

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

        return self.value()

    __iter__ = __await__  

Սա այնքան հարմար ֆունկցիա է, որը սպասում է մինչև հաշվիչը թարմացվի. ֆունկցիան ժամանակ առ ժամանակ արթնանում է և ստուգում _value_changed դրոշը: Այս ֆունկցիայի ամենալավ բանն այն է, որ զանգի կոդը կարող է քնել այս ֆունկցիան կանչելիս և քնել մինչև նոր արժեք ստանալը:

Ի՞նչ կասեք ընդհատումների մասին։Այո, այս պահին կարող ես ինձ տրոլլ անել՝ ասելով, որ դու ինքդ ասացիր ընդհատումների մասին, բայց իրականում հիմար պին-պոլլ արեցիր։ Իրականում ընդհատումները առաջին բանն են, որ ես փորձել եմ: ESP8266-ում դուք կարող եք կազմակերպել եզրային ընդհատում և նույնիսկ գրել այս ընդհատման համար կարգավորիչ Python-ում: Այս ընդհատման դեպքում փոփոխականի արժեքը կարող է թարմացվել: Հավանաբար, դա բավական կլիներ, եթե հաշվիչը լիներ ստրուկ սարք, որը սպասում է, մինչև իրեն հարցնեն այս արժեքը:

Ցավոք (կամ բարեբախտաբար) իմ սարքն ակտիվ է, այն պետք է ինքն իրեն հաղորդագրություններ ուղարկի MQTT արձանագրության միջոցով և տվյալներ գրի EEPROM-ին: Եվ այստեղ սահմանափակումները գործում են. դուք չեք կարող հիշողություն հատկացնել ընդհատումներով և օգտագործել մեծ կույտ, ինչը նշանակում է, որ կարող եք մոռանալ ցանցով հաղորդագրություններ ուղարկելու մասին: Կան buns, ինչպիսիք են micropython.schedule()-ը, որոնք թույլ են տալիս գործարկել ինչ-որ գործառույթ «որքան հնարավոր է շուտ», բայց հարց է առաջանում՝ «ի՞նչն է իմաստը»: Իսկ եթե մենք հենց հիմա ինչ-որ հաղորդագրություն ենք ուղարկում, և հետո ընդհատում է գալիս և փչացնում փոփոխականների արժեքները: Կամ, օրինակ, նոր հաշվիչի արժեք է եկել սերվերից, մինչդեռ մենք դեռ չենք գրել հինը: Ընդհանուր առմամբ, դուք պետք է արգելափակեք համաժամացումը կամ ինչ-որ կերպ այլ կերպ դուրս գաք դրանից:

Եվ ժամանակ առ ժամանակ RuntimeError. ժամանակացույցի ամբողջական խափանումներ և ո՞վ գիտի, թե ինչու:

Բացահայտ հարցումներով և uasync-ով, այս դեպքում ինչ-որ կերպ ավելի գեղեցիկ և հուսալի է ստացվում

Ես EEPROM-ի հետ աշխատանքը տարա փոքր դասարան

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-ում դժվար է ուղղակիորեն աշխատել բայթերի հետ, բայց հենց բայթերն են գրվում հիշողության մեջ։ Ես ստիպված էի ցանկապատել փոխակերպումը ամբողջ թվերի և բայթերի միջև՝ օգտագործելով ustruct գրադարանը:

Որպեսզի ամեն անգամ I2C օբյեկտը և հիշողության բջիջի հասցեն չփոխանցեմ, ես այդ ամենը փաթաթեցի փոքրիկ և հարմար դասականով:

class EEPROMValue():
    def __init__(self, i2c, eeprom_addr):
        self._eeprom = EEPROM(i2c)
        self._eeprom_addr = eeprom_addr
        

    def read(self):
        return self._eeprom.read(self._eeprom_addr)


    def write(self, value):
        self._eeprom.write(self._eeprom_addr, value)

I2C օբյեկտն ինքնին ստեղծված է այս պարամետրերով

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

Մենք հասնում ենք ամենահետաքրքիր մասին՝ MQTT-ի միջոցով սերվերի հետ կապի իրականացմանը: Դե, պրոտոկոլն ինքնին իրականացնելու կարիք չկա, ես այն գտա ինտերնետում պատրաստի ասինխրոն իրականացում. Սա այն է, ինչ մենք կօգտագործենք:

Բոլոր ամենահետաքրքիր բաները հավաքված են CounterMQTTClient դասում, որը հիմնված է MQTTClient գրադարանի վրա: Սկսենք ծայրամասից

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

Այստեղ դուք կարող եք ստեղծել և կարգավորել լույսի լամպերի միններն ու կոճակները, ինչպես նաև սառը և տաք ջրի հաշվիչի օբյեկտները:

Նախաստորագրման դեպքում ամեն ինչ այնքան էլ մանրուք չէ

    def __init__(self):
        self.internet_outage = True
        self.internet_outages = 0
        self.internet_outage_start = ticks_ms()

        with open("config.txt") as config_file:
            config['ssid'] = config_file.readline().rstrip()
            config['wifi_pw'] = config_file.readline().rstrip()
            config['server'] = config_file.readline().rstrip()
            config['client_id'] = config_file.readline().rstrip()
            self._mqtt_cold_water_theme = config_file.readline().rstrip()
            self._mqtt_hot_water_theme = config_file.readline().rstrip()
            self._mqtt_debug_water_theme = config_file.readline().rstrip()

        config['subs_cb'] = self.mqtt_msg_handler
        config['wifi_coro'] = self.wifi_connection_handler
        config['connect_coro'] = self.mqtt_connection_handler
        config['clean'] = False
        config['clean_init'] = False
        super().__init__(config)

        loop = asyncio.get_event_loop()
        loop.create_task(self._heartbeat())
        loop.create_task(self._counter_coro(self.cold_counter, self._mqtt_cold_water_theme))
        loop.create_task(self._counter_coro(self.hot_counter, self._mqtt_hot_water_theme))
        loop.create_task(self._display_coro())

mqtt_as գրադարանի գործառնական պարամետրերը սահմանելու համար օգտագործվում է տարբեր պարամետրերի մեծ բառարան՝ config: Լռելյայն կարգավորումների մեծ մասը մեզ համար լավ է, բայց շատ կարգավորումներ պետք է հստակորեն սահմանվեն: Պարամետրերը ուղղակի կոդի մեջ չգրելու համար դրանք պահում եմ տեքստային ֆայլում config.txt։ Սա թույլ է տալիս փոխել կոդը անկախ կարգավորումներից, ինչպես նաև գամել տարբեր պարամետրերով մի քանի նույնական սարքեր:

Կոդի վերջին բլոկը սկսում է մի քանի կորուտիններ՝ սպասարկելու համակարգի տարբեր գործառույթներ: Օրինակ, ահա մի կորուտին, որը ծառայությունները հակադարձում են

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

The Coroutine-ը սպասում է ցիկլով նոր հաշվիչի համար և, հենց որ այն հայտնվում է, հաղորդագրություն է ուղարկում MQTT արձանագրության միջոցով: Կոդի առաջին կտորն ուղարկում է սկզբնական արժեքը, նույնիսկ եթե ջուրը չի հոսում հաշվիչի միջով:

MQTTClient բազային դասը ծառայում է ինքն իրեն, սկսում է WiFi կապ և նորից միանում, երբ կապը կորչում է: Երբ WiFi կապի վիճակում փոփոխություններ են լինում, գրադարանը մեզ տեղեկացնում է՝ զանգահարելով 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)

Ֆունկցիան անկեղծորեն պատճենված է օրինակներից: Այս դեպքում այն ​​հաշվում է անջատումների քանակը (ինտերնետ_անջատումներ) և դրանց տևողությունը։ Երբ կապը վերականգնվում է, անգործության ժամանակ է ուղարկվում սերվերին:

Ի դեպ, վերջին քունը անհրաժեշտ է միայն ֆունկցիան ասինխրոն դարձնելու համար. գրադարանում այն ​​կոչվում է via await, և կարող են կանչվել միայն այն գործառույթները, որոնց մարմինը պարունակում է մեկ այլ սպասող:

Բացի WiFi-ին միանալուց, անհրաժեշտ է նաև կապ հաստատել MQTT բրոքերի (սերվերի) հետ: Դա անում է նաև գրադարանը, և մենք հնարավորություն ենք ստանում ինչ-որ օգտակար բան անել, երբ կապը հաստատվի

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

Այստեղ մենք բաժանորդագրվում ենք մի քանի հաղորդագրությունների. սերվերն այժմ հնարավորություն ունի սահմանելու ընթացիկ հաշվիչի արժեքները՝ ուղարկելով համապատասխան հաղորդագրություն:

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

Այս ֆունկցիան մշակում է մուտքային հաղորդագրությունները, և կախված թեմայից (հաղորդագրության վերնագիր) հաշվիչներից մեկի արժեքները թարմացվում են։

Մի քանի օգնական գործառույթներ

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

Այս գործառույթը հաղորդագրություն է ուղարկում, եթե կապը հաստատված է: Եթե ​​կապ չկա, հաղորդագրությունն անտեսվում է:

Եվ սա պարզապես հարմար գործառույթ է, որը ստեղծում և ուղարկում է վրիպազերծման հաղորդագրություններ:

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

Այնքան շատ տեքստ, և մենք դեռ չենք թարթել լուսադիոդը: Այստեղ

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

Ես տրամադրել եմ թարթելու 2 ռեժիմ: Եթե ​​կապը կորչում է (կամ այն ​​նոր է հաստատվում), սարքն արագ թարթելու է: Եթե ​​կապը հաստատվի, սարքը թարթում է 5 վայրկյանը մեկ անգամ: Անհրաժեշտության դեպքում այստեղ կարող են կիրառվել թարթման այլ ռեժիմներ:

Բայց լուսադիոդը պարզապես փայփայում է: Մենք նաև նպատակաուղղված ենք եղել ցուցադրությանը։

    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)

Ահա թե ինչի մասին էի խոսում՝ որքան պարզ և հարմար է դա կորուտինների հետ: Այս փոքրիկ ֆունկցիան նկարագրում է ՈՂՋ օգտատերերի փորձը: Կորուտինը պարզապես սպասում է կոճակը սեղմելուն և էկրանը միացնում է 3 վայրկյան: Ցուցադրումը ցույց է տալիս հաշվիչի ընթացիկ ընթերցումները:

Դեռ մի երկու մանրուք է մնացել։ Ահա այն գործառույթը, որը (վեր) սկսում է այս ամբողջ ձեռնարկությունը: Հիմնական հանգույցը րոպեն մեկ անգամ ուղարկում է տարբեր վրիպազերծման տեղեկություններ: Ընդհանրապես, ես մեջբերում եմ այնպես, ինչպես կա, չեմ կարծում, որ կարիք կա շատ մեկնաբանելու

   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)

Դե, ևս մի քանի կարգավորում և հաստատուն՝ նկարագրությունն ավարտելու համար

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

Ամեն ինչ սկսվում է այսպես

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

Հիշողությանս հետ ինչ-որ բան պատահեց

Այսպիսով, ամբողջ ծածկագիրը կա: Ես վերբեռնել եմ ֆայլերը՝ օգտագործելով ampy կոմունալը. այն թույլ է տալիս դրանք վերբեռնել ներքին (իսկ ESP-07-ում գտնվող) ֆլեշ կրիչի վրա և այնուհետև մուտք գործել ծրագրից որպես սովորական ֆայլեր: Այնտեղ ես նաև վերբեռնեցի իմ օգտագործած mqtt_as, uasyncio, ssd1306 և հավաքածուների գրադարանները (օգտագործվում էին mqtt_as-ի ներսում):

Մենք գործարկում ենք և... Ստանում ենք MemoryError: Ավելին, որքան շատ էի փորձում հասկանալ, թե կոնկրետ որտեղ է արտահոսում հիշողությունը, այնքան ավելի շատ վրիպազերծման տպումներ էի դնում, այնքան ավելի վաղ էր հայտնվում այս սխալը: Google-ի կարճ որոնումն ինձ հանգեցրեց այն հասկացողության, որ միկրոկոնտրոլերը, սկզբունքորեն, ունի ընդամենը 30 կԲ հիշողություն, որի մեջ 65 կԲ կոդ (ներառյալ գրադարանները) պարզապես չի կարող տեղավորվել:

Բայց ելք կա. Պարզվում է, որ micropython-ը չի կատարում կոդը անմիջապես .py ֆայլից. այս ֆայլը առաջինը կազմվում է: Ավելին, այն ուղղակիորեն կազմվում է միկրոկոնտրոլերի վրա, վերածվում բայթկոդի, որն այնուհետև պահվում է հիշողության մեջ։ Դե, որպեսզի կոմպիլյատորը աշխատի, անհրաժեշտ է նաև որոշակի քանակությամբ RAM:

Խնդիրն այն է, որ միկրոկոնտրոլերը փրկել ռեսուրսների ինտենսիվ հավաքումից: Դուք կարող եք ֆայլերը կազմել մեծ համակարգչի վրա և ներբեռնել պատրաստի բայթկոդը միկրոկոնտրոլերի մեջ։ Դա անելու համար անհրաժեշտ է ներբեռնել micropython որոնվածը և կառուցել mpy-cross կոմունալ.

Ես չեմ գրել Makefile, բայց ձեռքով անցել և հավաքել եմ բոլոր անհրաժեշտ ֆայլերը (ներառյալ գրադարանները) նման բան.

mpy-cross water_counter.py

Մնում է միայն վերբեռնել ֆայլեր .mpy ընդլայնմամբ՝ չմոռանալով նախ ջնջել համապատասխան .py-ը սարքի ֆայլային համակարգից։

Ես կատարել եմ բոլոր մշակումները ծրագրի (IDE?) ESPlorer-ում: Այն թույլ է տալիս վերբեռնել սկրիպտներ միկրոկոնտրոլեր և անմիջապես կատարել դրանք: Իմ դեպքում բոլոր օբյեկտների ողջ տրամաբանությունն ու ստեղծումը գտնվում է water_counter.py (.mpy) ֆայլում։ Բայց որպեսզի այս ամենն ինքնաբերաբար սկսվի, սկզբում պետք է լինի նաեւ main.py անունով ֆայլ։ Ավելին, այն պետք է լինի հենց .py, այլ ոչ թե նախապես կազմված .mpy: Ահա դրա չնչին բովանդակությունը

import water_counter

Մենք գործարկում ենք այն, ամեն ինչ աշխատում է: Բայց ազատ հիշողությունը տագնապալի փոքր է` մոտ 1կբ: Ես դեռ պլաններ ունեմ ընդլայնելու սարքի ֆունկցիոնալությունը, և այս կիլոբայթն ինձ համար ակնհայտորեն բավարար չէ: Բայց պարզվեց, որ այս դեպքի համար էլ ելք կա։

Ահա բանը. Թեև ֆայլերը կազմվում են բայթկոդի մեջ և գտնվում են ներքին ֆայլային համակարգում, իրականում դրանք դեռևս բեռնվում են RAM-ում և գործարկվում այնտեղից: Բայց պարզվում է, որ micropython-ը կարող է բայթկոդ կատարել անմիջապես ֆլեշ հիշողությունից, բայց դրա համար անհրաժեշտ է այն ուղղակիորեն տեղադրել որոնվածի մեջ: Դժվար չէ, թեև իմ նեթբուքում բավականին ժամանակ պահանջվեց (միայն այնտեղ պատահաբար ունեի Linux):

Ալգորիթմն այսպիսին է.

  • Ներբեռնեք և տեղադրեք ESP բաց SDK. Այս բանը հավաքում է կոմպիլյատոր և գրադարաններ ESP8266-ի համար նախատեսված ծրագրերի համար: Հավաքվել է նախագծի գլխավոր էջի հրահանգների համաձայն (ես ընտրել եմ STANDALONE=այո պարամետրը)
  • RЎRєR ° C ‡ P ° C SЊ micropython տեսակի
  • Տեղադրեք անհրաժեշտ գրադարանները ports/esp8266/modules-ում micropython ծառի ներսում
  • Մենք հավաքում ենք որոնվածը ըստ ֆայլի հրահանգների ports/esp8266/README.md
  • Մենք ներբեռնում ենք որոնվածը միկրոկառավարիչում (ես դա անում եմ Windows-ում՝ օգտագործելով ESP8266Flasher ծրագրերը կամ Python esptool)

Վերջ, հիմա «ներմուծում ssd1306»-ը կվերացնի կոդը անմիջապես որոնվածից, և RAM-ը դրա համար չի սպառվի: Այս հնարքով ես միայն գրադարանի կոդը վերբեռնեցի որոնվածը, մինչդեռ հիմնական ծրագրի կոդը կատարվում է ֆայլային համակարգից։ Սա թույլ է տալիս հեշտությամբ փոփոխել ծրագիրը՝ չվերակազմավորելով որոնվածը: Այս պահին ունեմ մոտ 8.5 կբ ազատ օպերատիվ հիշողություն։ Սա մեզ թույլ կտա ապագայում իրականացնել բավականին շատ տարբեր օգտակար գործառույթներ: Դե, եթե ընդհանրապես բավարար հիշողություն չկա, ապա կարող եք հիմնական ծրագիրը մղել որոնվածի մեջ:

Այսպիսով, ի՞նչ պետք է անենք դրա հետ կապված հիմա:

Ok, ապարատը զոդված է, որոնվածը գրված է, տուփը տպված է, սարքը խրված է պատին և ուրախությամբ թարթում է մի լամպ: Բայց առայժմ այս ամենը սև արկղ է (բառացի և փոխաբերական իմաստով) և դեռ քիչ օգուտ ունի: Ժամանակն է ինչ-որ բան անել MQTT հաղորդագրությունների հետ, որոնք ուղարկվում են սերվեր:

Իմ «խելացի տունը» պտտվում է Majordomo համակարգ. MQTT մոդուլը կամ դուրս է գալիս տուփից, կամ հեշտությամբ տեղադրվում է հավելումների շուկայից - չեմ հիշում, թե որտեղից եմ այն ​​ստացել: MQTT-ն ինքնաբավ բան չէ՝ պետք է այսպես կոչված. բրոքեր - սերվեր, որը ստանում, տեսակավորում և փոխանցում է MQTT հաղորդագրությունները հաճախորդներին: Ես օգտագործում եմ mosquitto, որը (ինչպես majordomo) աշխատում է նույն նեթբուքով:

Սարքը առնվազն մեկ անգամ հաղորդագրություն ուղարկելուց հետո արժեքը անմիջապես կհայտնվի ցանկում:

Մենք ջրաչափը միացնում ենք խելացի տանը

Այս արժեքներն այժմ կարող են կապված լինել համակարգի օբյեկտների հետ, դրանք կարող են օգտագործվել ավտոմատացման սկրիպտներում և ենթարկվել տարբեր վերլուծությունների, որոնք բոլորը դուրս են այս հոդվածի շրջանակներից: Ես կարող եմ խորհուրդ տալ majordomo համակարգը բոլորին, ովքեր հետաքրքրված են ալիք Electronics In Lens — ընկերը նույնպես խելացի տուն է կառուցում և հստակ խոսում է համակարգը կարգավորելու մասին:

Ես պարզապես ցույց կտամ ձեզ մի երկու գրաֆիկ: Սա ամենօրյա արժեքների պարզ գրաֆիկ է

Մենք ջրաչափը միացնում ենք խելացի տանը
Երևում է, որ գիշերը գրեթե ոչ ոք ջուր չի օգտագործել։ Մի երկու անգամ ինչ-որ մեկը գնացել է զուգարան, և թվում է, թե հակադարձ օսմոսի ֆիլտրը գիշերը մի երկու լիտր է ծծում։ Առավոտյան սպառումը զգալիորեն ավելանում է։ Ես սովորաբար օգտագործում եմ ջուրը կաթսայից, բայց հետո ցանկացա լոգանք ընդունել և ժամանակավորապես անցա քաղաքային տաք ջրին. սա նաև հստակ երևում է ներքևի գրաֆիկում:

Այս գրաֆիկից ես իմացա, որ զուգարան գնալու համար պահանջվում է 6-7 լիտր ջուր, ցնցուղ ընդունելու համար՝ 20-30 լիտր, սպասք լվանալու համար՝ մոտ 20 լիտր, իսկ լոգանք ընդունելու համար՝ 160 լիտր ջուր: Իմ ընտանիքը օրական ինչ-որ տեղ 500-600 լիտր է սպառում։

Նրանց համար, ովքեր հատկապես հետաքրքրված են, կարող եք դիտել յուրաքանչյուր առանձին արժեքի գրառումները

Մենք ջրաչափը միացնում ենք խելացի տանը

Այստեղից ես իմացա, որ երբ ծորակը բաց է, ջուրը հոսում է մոտավորապես 1 լիտր 5 վրկ արագությամբ։

Բայց այս տեսքով վիճակագրությունը հավանաբար այնքան էլ հարմար չէ նայելու համար։ Majordomo-ն նաև հնարավորություն ունի դիտելու սպառման գծապատկերներն ըստ օրվա, շաբաթվա և ամսվա: Ահա, օրինակ, սպառման գրաֆիկը ձողերով

Մենք ջրաչափը միացնում ենք խելացի տանը

Առայժմ միայն մեկ շաբաթվա տվյալներ ունեմ։ Մեկ ամսից այս գրաֆիկը ավելի ցուցիչ կլինի՝ յուրաքանչյուր օր կունենա առանձին սյունակ: Նկարը փոքր-ինչ փչացած է այն արժեքների ճշգրտումներով, որոնք ես մուտքագրում եմ ձեռքով (ամենամեծ սյունակը): Եվ դեռ պարզ չէ, թե արդյոք ես սխալ եմ դրել հենց առաջին արժեքները, գրեթե մեկ խորանարդ պակաս, թե սա ծրագրաշարի վրիպակ է, և ոչ բոլոր լիտրերն են հաշվվել: Ավելի շատ ժամանակ է պետք:

Գրաֆիկները դեռ կարիք ունեն որոշ կախարդանքի, սպիտակեցման, ներկելու: Թերևս ես կկառուցեմ նաև հիշողության սպառման գրաֆիկ՝ վրիպազերծման նպատակով, եթե այնտեղ ինչ-որ բան արտահոսի: Միգուցե ես ինչ-որ կերպ ցուցադրեմ ժամանակաշրջաններ, երբ ինտերնետ չկար։ Առայժմ այս ամենը գաղափարների մակարդակում է։

Ամփոփում

Այսօր իմ բնակարանը մի փոքր խելացի է դարձել. Նման փոքր սարքով ինձ համար ավելի հարմար կլինի տանը ջրի սպառումը վերահսկել։ Եթե ​​նախկինում ես վրդովվում էի «նորից մեկ ամսում շատ ջուր ենք խմել», հիմա կարող եմ գտնել այս սպառման աղբյուրը։

Ոմանք կարող են տարօրինակ թվալ էկրանի ցուցմունքներին նայելը, եթե այն գտնվում է բուն հաշվիչից մեկ մետր հեռավորության վրա: Բայց ոչ շատ հեռավոր ապագայում ես նախատեսում եմ տեղափոխվել մեկ այլ բնակարան, որտեղ կլինեն մի քանի ջրի բարձրացուցիչներ, և հաշվիչներն իրենք, ամենայն հավանականությամբ, կտեղակայվեն վայրէջքի վրա: Այսպիսով, հեռահար ընթերցման սարքը շատ օգտակար կլինի:

Նախատեսում եմ նաև ընդլայնել սարքի ֆունկցիոնալությունը։ Ես արդեն նայում եմ շարժիչով փականներին: Հիմա կաթսան քաղաքային ջրին անցնելու համար պետք է 3 ծորակ բացեմ դժվարամատչելի խորշում։ Շատ ավելի հարմար կլինի դա անել մեկ կոճակով՝ համապատասխան նշումով։ Դե, իհարկե, արժե իրականացնել պաշտպանություն արտահոսքից:

Հոդվածում ես նկարագրեցի ESP8266-ի վրա հիմնված սարքի իմ տարբերակը: Իմ կարծիքով, ես գտա micropython որոնվածի շատ հետաքրքիր տարբերակ՝ օգտագործելով coroutines՝ պարզ և գեղեցիկ: Փորձեցի նկարագրել բազմաթիվ նրբերանգներ ու թերություններ, որոնց հանդիպեցի քարոզարշավի ընթացքում։ Թերևս ես ամեն ինչ շատ մանրամասն նկարագրեցի, որպես ընթերցող, ինձ համար ավելի հեշտ է շրջանցել ավելորդ բաները, քան հետո մտածել, թե ինչ է մնացել չասված:

Ինչպես միշտ, ես բաց եմ կառուցողական քննադատության համար:

Աղբյուրի կոդը
Միացում և տախտակ
Գործի մոդել

Source: www.habr.com