Kami menghubungkan meteran air ke rumah pintar

Dahulu kala, sistem otomasi rumah, atau sering disebut “rumah pintar”, sangat mahal dan hanya orang kaya yang mampu membelinya. Saat ini di pasaran Anda dapat menemukan kit yang cukup murah dengan sensor, tombol/sakelar, dan aktuator untuk mengontrol pencahayaan, soket, ventilasi, pasokan air, dan konsumen lainnya. Dan bahkan orang DIY yang paling licik pun bisa terlibat dalam kecantikan dan merakit perangkat untuk rumah pintar dengan harga murah.

Kami menghubungkan meteran air ke rumah pintar

Biasanya, perangkat yang diusulkan berupa sensor atau aktuator. Mereka memudahkan penerapan skenario seperti “saat sensor gerak terpicu, nyalakan lampu” atau “saklar di dekat pintu keluar mematikan lampu di seluruh apartemen”. Tapi entah kenapa semuanya tidak berjalan baik dengan telemetri. Paling banter, ini adalah grafik suhu dan kelembapan, atau daya sesaat di stopkontak tertentu.

Saya baru saja memasang meter air dengan keluaran pulsa. Untuk setiap liter yang melewati meteran, saklar buluh diaktifkan dan menutup kontak. Satu-satunya hal yang harus dilakukan adalah berpegang teguh pada kabel dan mencoba memanfaatkannya. Misalnya, analisis konsumsi air berdasarkan jam dan hari dalam seminggu. Nah, jika ada beberapa penambah air di apartemen, maka akan lebih mudah untuk melihat semua indikator arus di satu layar daripada naik ke ceruk yang sulit dijangkau dengan senter.

Di bawah potongan adalah versi perangkat saya berdasarkan ESP8266, yang menghitung pulsa dari meter air dan mengirimkan pembacaan melalui MQTT ke server rumah pintar. Kami akan memprogram dalam micropython menggunakan perpustakaan uasyncio. Saat membuat firmware, saya menemukan beberapa kesulitan menarik, yang juga akan saya bahas di artikel ini. Pergi!

Skema itu

Kami menghubungkan meteran air ke rumah pintar

Inti dari keseluruhan rangkaian adalah modul pada mikrokontroler ESP8266. ESP-12 awalnya direncanakan, tetapi milik saya ternyata rusak. Kami harus puas dengan modul ESP-07 yang tersedia. Untungnya, keduanya sama dalam hal pin dan fungsionalitas, satu-satunya perbedaan adalah pada antenanya - ESP-12 memiliki antena internal, sedangkan ESP-07 memiliki antena eksternal. Namun, meski tanpa antena WiFi, sinyal di kamar mandi saya diterima dengan normal.

Kabel modul standar:

  • tombol reset dengan pull-up dan kapasitor (walaupun keduanya sudah ada di dalam modul)
  • Sinyal pengaktifan (CH_PD) ditarik ke daya
  • GPIO15 ditarik ke tanah. Ini hanya diperlukan pada awalnya, namun saya masih belum mempunyai apa pun untuk dipasang pada kaki ini; saya tidak memerlukannya lagi

Untuk memasukkan modul ke mode firmware, Anda perlu menghubungkan GPIO2 ke ground, dan untuk membuatnya lebih nyaman, saya menyediakan tombol Boot. Dalam kondisi normal, pin ini ditarik ke listrik.

Status saluran GPIO2 diperiksa hanya pada awal pengoperasian - saat daya dialirkan atau segera setelah reset. Jadi modul akan melakukan booting seperti biasa atau masuk ke mode firmware. Setelah dimuat, pin ini dapat digunakan sebagai GPIO biasa. Karena sudah ada tombol di sana, Anda dapat melampirkan beberapa fungsi berguna ke dalamnya.

Untuk pemrograman dan debugging saya akan menggunakan UART, yang dikeluarkan ke sisir. Bila perlu, saya cukup menghubungkan adaptor USB-UART di sana. Anda hanya perlu mengingat bahwa modul ini ditenagai oleh 3.3V. Jika Anda lupa mengganti adaptor ke tegangan ini dan menyuplai 5V, kemungkinan besar modul akan terbakar.

Saya tidak punya masalah dengan listrik di kamar mandi - stopkontak terletak sekitar satu meter dari meter, jadi saya akan mendapat daya dari 220V. Sebagai sumber listrik saya akan punya yang kecil blok HLK-PM03 oleh Robot Tenstar. Secara pribadi, saya mengalami kesulitan dengan analog dan elektronika daya, tetapi ini adalah catu daya siap pakai dalam wadah kecil.

Untuk memberi sinyal mode operasi, saya menyediakan LED yang terhubung ke GPIO2. Namun, saya tidak melepas soldernya, karena... Modul ESP-07 sudah memiliki LED dan juga terhubung dengan GPIO2. Tapi biarkan saja di papan, kalau-kalau saya ingin menampilkan LED ini ke casing.

Mari beralih ke bagian yang paling menarik. Meter air tidak memiliki logika; Anda tidak dapat menanyakan pembacaan terkini pada meter tersebut. Satu-satunya hal yang tersedia bagi kita adalah impuls - menutup kontak saklar buluh setiap liter. Output saklar buluh saya terhubung ke GPIO12/GPIO13. Saya akan mengaktifkan resistor pull-up secara terprogram di dalam modul.

Awalnya, saya lupa menyediakan resistor R8 dan R9 dan versi board saya tidak memilikinya. Namun karena saya sudah memposting diagramnya agar dapat dilihat semua orang, ada baiknya memperbaiki kesalahan ini. Resistor diperlukan agar tidak membakar port jika firmware mengalami gangguan dan menyetel pin ke satu, dan saklar buluh memendekkan saluran ini ke ground (dengan resistor maksimum 3.3V/1000Ohm = 3.3mA akan mengalir).

Saatnya memikirkan apa yang harus dilakukan jika listrik padam. Opsi pertama adalah meminta nilai penghitung awal dari server di awal. Namun hal ini memerlukan kerumitan protokol pertukaran yang signifikan. Selain itu, kinerja perangkat dalam hal ini bergantung pada status server. Jika server tidak memulai setelah listrik dimatikan (atau dimulai kemudian), meteran air tidak akan dapat meminta nilai awal dan tidak akan berfungsi dengan benar.

Oleh karena itu, saya memutuskan untuk menerapkan nilai penghitung penyimpanan dalam chip memori yang terhubung melalui I2C. Saya tidak memiliki persyaratan khusus untuk ukuran memori flash - Anda hanya perlu menyimpan 2 angka (jumlah liter menurut meteran air panas dan dingin). Bahkan modul terkecil pun bisa digunakan. Namun Anda perlu memperhatikan jumlah siklus perekaman. Untuk sebagian besar modul, ini adalah 100 ribu siklus, untuk beberapa modul mencapai satu juta.

Tampaknya satu juta itu banyak. Tapi selama 4 tahun tinggal di apartemen saya, saya mengonsumsi lebih dari 500 meter kubik air, itu 500 ribu liter! Dan 500 ribu rekaman dalam sekejap. Dan itu hanya air dingin. Anda tentu saja dapat menyolder ulang chip tersebut setiap beberapa tahun, tetapi ternyata ada chip FRAM. Dari sudut pandang pemrograman, ini adalah EEPROM I2C yang sama, hanya saja dengan jumlah siklus penulisan ulang yang sangat besar (ratusan juta). Hanya saja saya masih belum bisa ke toko dengan sirkuit mikro seperti itu, jadi untuk saat ini 24LC512 biasa akan bertahan.

Papan sirkuit

Awalnya saya berencana membuat papan itu di rumah. Oleh karena itu, papan dirancang satu sisi. Namun setelah menghabiskan satu jam dengan setrika laser dan masker solder (entah bagaimana rasanya tanpa itu), saya tetap memutuskan untuk memesan papan tersebut dari Cina.

Kami menghubungkan meteran air ke rumah pintar

Hampir sebelum memesan papan, saya menyadari bahwa selain chip memori flash, saya dapat menghubungkan hal lain yang berguna ke bus I2C, seperti layar. Apa sebenarnya yang harus dihasilkan masih merupakan sebuah pertanyaan, namun perlu diarahkan ke papan tulis. Nah, karena saya akan memesan papan dari pabrik, tidak ada gunanya membatasi diri pada papan satu sisi, jadi hanya jalur I2C yang ada di sisi belakang papan.

Ada juga satu masalah besar dengan kabel satu arah. Karena Papan digambar satu sisi, sehingga track dan komponen SMD direncanakan ditempatkan di satu sisi, dan komponen keluaran, konektor, dan catu daya di sisi lain. Ketika saya menerima papannya sebulan kemudian, saya lupa rencana awal dan menyolder semua komponen di sisi depan. Dan hanya ketika menyolder catu daya, ternyata plus dan minusnya disambungkan secara terbalik. Saya harus bertani dengan jumper. Pada gambar di atas, saya telah mengganti kabel, tetapi ground dipindahkan dari satu bagian papan ke bagian lain melalui pin tombol Boot (walaupun dimungkinkan untuk menggambar trek pada lapisan kedua).

Ternyata seperti ini

Kami menghubungkan meteran air ke rumah pintar

Perumahan

Langkah selanjutnya adalah tubuh. Jika Anda memiliki printer 3D, hal ini tidak menjadi masalah. Saya tidak terlalu repot - saya hanya menggambar sebuah kotak dengan ukuran yang tepat dan membuat potongan di tempat yang tepat. Penutup dipasang ke badan dengan sekrup kecil yang dapat disadap sendiri.

Kami menghubungkan meteran air ke rumah pintar

Saya telah menyebutkan bahwa tombol Boot dapat digunakan sebagai tombol serba guna - jadi kami akan menampilkannya di panel depan. Untuk melakukan ini, saya menggambar "sumur" khusus tempat tombol itu berada.

Kami menghubungkan meteran air ke rumah pintar

Di dalam casing juga terdapat tiang tempat papan dipasang dan diamankan dengan satu sekrup M3 (tidak ada lagi ruang di papan)

Saya sudah memilih tampilan ketika saya mencetak versi sampel pertama dari case ini. Pembaca dua baris standar tidak cocok dengan casing ini, tetapi di bagian bawah terdapat layar OLED SSD1306 128x32. Memang agak kecil, tapi saya tidak perlu melihatnya setiap hari—itu terlalu berat bagi saya.

Mencari tahu cara ini dan bagaimana kabel akan dirutekan darinya, saya memutuskan untuk menempelkan layar di tengah casing. Ergonominya tentu saja di bawah standar - tombolnya di atas, layarnya di bawah. Tapi saya sudah mengatakan bahwa ide untuk memasang layar datang terlambat dan saya terlalu malas untuk memasang kembali papan untuk memindahkan tombol.

Perangkat sudah dirakit. Modul tampilan direkatkan ke ingus dengan lem panas

Kami menghubungkan meteran air ke rumah pintar

Kami menghubungkan meteran air ke rumah pintar

Hasil akhirnya bisa dilihat di KDPV

Firmware

Mari beralih ke bagian perangkat lunak. Untuk kerajinan kecil seperti ini, saya sangat suka menggunakan Python (mikropython) - kodenya ternyata sangat ringkas dan mudah dimengerti. Untungnya, tidak perlu turun ke level register untuk memeras mikrodetik - semuanya bisa dilakukan dari Python.

Tampaknya semuanya sederhana, tetapi tidak terlalu sederhana - perangkat ini memiliki beberapa fungsi independen:

  • Pengguna menekan tombol dan melihat ke layar
  • Liter centang dan perbarui nilai dalam memori flash
  • Modul memonitor sinyal WiFi dan menghubungkan kembali jika perlu
  • Ya, tanpa bola lampu yang berkedip, hal itu tidak mungkin

Anda tidak dapat berasumsi bahwa satu fungsi tidak berfungsi jika fungsi lainnya terhenti karena alasan tertentu. Saya sudah mengisi kaktus di proyek lain dan sekarang saya masih melihat gangguan seperti "ketinggalan satu liter lagi karena tampilan sedang diperbarui pada saat itu" atau "pengguna tidak dapat melakukan apa pun saat modul terhubung ke Wifi." Tentu saja, beberapa hal dapat dilakukan melalui interupsi, tetapi Anda mungkin mengalami batasan pada durasi, kumpulan panggilan, atau perubahan non-atomik pada variabel. Nah, kode yang melakukan segalanya dengan cepat berubah menjadi bubur.

В proyek yang lebih serius Saya menggunakan multitasking preemptive klasik dan FreeRTOS, namun dalam kasus ini modelnya ternyata jauh lebih cocok perpustakaan coroutine dan uasync . Selain itu, implementasi coroutine dengan Python sungguh luar biasa - semuanya dilakukan dengan sederhana dan nyaman bagi pemrogram. Tulis saja logika Anda sendiri, beri tahu saya di tempat mana Anda dapat beralih antar aliran.

Saya menyarankan mempelajari perbedaan antara multitasking preemptive dan kompetitif sebagai mata pelajaran opsional. Sekarang mari kita beralih ke kode.

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

Setiap counter ditangani oleh sebuah instance dari kelas Counter. Pertama-tama, nilai penghitung awal dikurangi dari EEPROM (value_storage) - ini adalah bagaimana pemulihan setelah listrik mati terjadi.

Pin diinisialisasi dengan pull-up bawaan ke catu daya: jika saklar buluh ditutup, salurannya nol, jika saluran terbuka, ditarik ke catu daya dan pengontrol membaca satu.

Tugas terpisah juga diluncurkan di sini, yang akan melakukan polling pin. Setiap counter akan menjalankan tugasnya masing-masing. Ini kodenya

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

Penundaan 25 ms diperlukan untuk memfilter pentalan kontak, dan pada saat yang sama mengatur seberapa sering tugas dibangunkan (saat tugas ini dalam mode tidur, tugas lain sedang berjalan). Setiap 25ms fungsinya aktif, periksa pin dan jika kontak saklar buluh ditutup, maka satu liter lagi telah melewati meteran dan ini perlu diproses.

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

        self._value_storage.write(self._value)

Memproses liter berikutnya itu sepele - penghitungnya bertambah. Nah, alangkah baiknya untuk menulis nilai baru ke dalam flash drive.

Untuk kemudahan penggunaan, "aksesor" disediakan

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

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

Nah, sekarang mari kita manfaatkan keunggulan Python dan pustaka uasync dan buat objek penghitung yang dapat ditunggu (bagaimana kita bisa menerjemahkannya ke dalam bahasa Rusia? Yang bisa Anda harapkan?)

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

        return self.value()

    __iter__ = __await__  

Ini adalah fungsi praktis yang menunggu hingga nilai penghitung diperbarui - fungsi tersebut aktif dari waktu ke waktu dan memeriksa tanda _value_changed. Hal yang keren tentang fungsi ini adalah kode pemanggil bisa tertidur saat memanggil fungsi ini dan tidur hingga nilai baru diterima.

Bagaimana dengan interupsi?Ya, saat ini Anda dapat menjebak saya dengan mengatakan bahwa Anda sendiri yang mengatakan tentang interupsi, tetapi kenyataannya Anda melakukan polling pin yang bodoh. Sebenarnya interupsi adalah hal pertama yang saya coba. Di ESP8266, Anda dapat mengatur interupsi tepi, dan bahkan menulis penangan untuk interupsi ini dengan Python. Dalam interupsi ini, nilai suatu variabel dapat diperbarui. Mungkin, ini akan cukup jika penghitungnya adalah perangkat budak - perangkat yang menunggu hingga nilai ini diminta.

Sayangnya (atau untungnya?) perangkat saya aktif, ia harus mengirim pesan sendiri melalui protokol MQTT dan menulis data ke EEPROM. Dan di sini pembatasan mulai berlaku - Anda tidak dapat mengalokasikan memori dalam interupsi dan menggunakan tumpukan besar, yang berarti Anda bisa melupakan pengiriman pesan melalui jaringan. Ada fitur seperti micropython.schedule() yang memungkinkan Anda menjalankan beberapa fungsi "sesegera mungkin", tetapi muncul pertanyaan, "apa gunanya?" Bagaimana jika kita sedang mengirimkan semacam pesan sekarang, dan kemudian interupsi masuk dan merusak nilai variabel. Atau, misalnya, nilai penghitung baru datang dari server sementara kami belum menuliskan nilai yang lama. Secara umum, Anda perlu memblokir sinkronisasi atau keluar dari situ dengan cara yang berbeda.

Dan dari waktu ke waktu RuntimeError: schedule stack full crash dan entah kenapa?

Dengan polling eksplisit dan uasync, dalam hal ini ternyata lebih indah dan dapat diandalkan

Saya membawa pekerjaan dengan EEPROM ke kelas kecil

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)

Di Python, sulit untuk bekerja secara langsung dengan byte, tetapi bytelah yang ditulis ke memori. Saya harus memagari konversi antara integer dan byte menggunakan perpustakaan ustruct.

Agar tidak mentransfer objek I2C dan alamat sel memori setiap saat, saya membungkus semuanya dalam bentuk klasik yang kecil dan nyaman

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)

Objek I2C sendiri dibuat dengan parameter ini

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

Kita sampai pada bagian yang paling menarik - implementasi komunikasi dengan server melalui MQTT. Ya, tidak perlu mengimplementasikan protokol itu sendiri - saya menemukannya di Internet implementasi asinkron yang sudah jadi. Inilah yang akan kami gunakan.

Semua hal paling menarik dikumpulkan di kelas CounterMQTTClient, yang didasarkan pada perpustakaan MQTTClient. Mari kita mulai dari pinggiran

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

Di sini Anda dapat membuat dan mengonfigurasi pin dan tombol bola lampu, serta objek meteran air dingin dan panas.

Dengan inisialisasi, tidak semuanya sepele

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

Untuk mengatur parameter operasi perpustakaan mqtt_as, kamus besar dengan pengaturan berbeda digunakan - config. Sebagian besar pengaturan default baik-baik saja bagi kami, tetapi banyak pengaturan yang perlu diatur secara eksplisit. Agar tidak menulis pengaturan langsung di kode, saya menyimpannya di file teks config.txt. Ini memungkinkan Anda untuk mengubah kode apa pun pengaturannya, serta memusatkan beberapa perangkat identik dengan parameter berbeda.

Blok kode terakhir memulai beberapa coroutine untuk menjalankan berbagai fungsi sistem. Misalnya, berikut adalah coroutine yang melayani penghitung

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

Coroutine menunggu dalam satu lingkaran untuk nilai penghitung baru dan, segera setelah nilai tersebut muncul, mengirimkan pesan melalui protokol MQTT. Potongan kode pertama mengirimkan nilai awal meskipun tidak ada air yang mengalir melalui penghitung.

Kelas dasar MQTTClient melayani dirinya sendiri, memulai koneksi WiFi dan menyambung kembali ketika koneksi terputus. Ketika ada perubahan status koneksi WiFi, perpustakaan memberi tahu kami dengan memanggil 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)

Fungsi ini disalin dengan jujur ​​​​dari contoh. Dalam hal ini, ini menghitung jumlah pemadaman (internet_outages) dan durasinya. Ketika koneksi dipulihkan, waktu idle dikirim ke server.

Omong-omong, tidur terakhir hanya diperlukan untuk membuat fungsi menjadi asinkron - di perpustakaan ini dipanggil melalui menunggu, dan hanya fungsi yang tubuhnya berisi menunggu lain yang dapat dipanggil.

Selain menghubungkan ke WiFi, Anda juga perlu membuat koneksi ke broker (server) MQTT. Perpustakaan juga melakukan hal ini, dan kami mendapat kesempatan untuk melakukan sesuatu yang berguna saat koneksi terjalin

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

Di sini kami berlangganan beberapa pesan - server sekarang memiliki kemampuan untuk mengatur nilai penghitung saat ini dengan mengirimkan pesan yang sesuai.

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

Fungsi ini memproses pesan masuk, dan bergantung pada topik (judul pesan), nilai salah satu penghitung diperbarui

Beberapa fungsi pembantu

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

Fungsi ini mengirimkan pesan jika koneksi telah terjalin. Jika tidak ada koneksi, pesan akan diabaikan.

Dan ini hanyalah fungsi praktis yang menghasilkan dan mengirim pesan debug.

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

Begitu banyak teks, dan kami belum mengedipkan LED. Di Sini

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

Saya telah menyediakan 2 mode kedipan. Jika koneksi terputus (atau baru saja dibuat), perangkat akan berkedip cepat. Jika sambungan berhasil dibuat, perangkat akan berkedip setiap 5 detik sekali. Jika perlu, mode kedipan lainnya dapat diterapkan di sini.

Tapi LEDnya hanya memanjakan. Kami juga membidik tampilan.

    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)

Inilah yang saya bicarakan - betapa sederhana dan nyamannya dengan coroutine. Fungsi kecil ini menggambarkan SELURUH pengalaman pengguna. Coroutine hanya menunggu tombol ditekan dan menyalakan tampilan selama 3 detik. Layar menunjukkan pembacaan meter saat ini.

Masih ada beberapa hal kecil yang tersisa. Inilah fungsi yang (kembali) memulai seluruh perusahaan ini. Loop utama hanya mengirimkan berbagai informasi debug satu menit sekali. Secara umum, saya mengutip apa adanya - menurut saya tidak perlu terlalu banyak berkomentar

   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)

Nah, beberapa pengaturan dan konstanta lagi untuk melengkapi deskripsi

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

Semuanya dimulai seperti ini

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

Sesuatu terjadi pada ingatanku

Jadi, semua kodenya ada di sana. Saya mengunggah file menggunakan utilitas ampy - ini memungkinkan Anda mengunggahnya ke flash drive internal (yang ada di ESP-07 itu sendiri) dan kemudian mengaksesnya dari program sebagai file biasa. Di sana saya juga mengunggah perpustakaan mqtt_as, uasyncio, ssd1306 dan koleksi yang saya gunakan (digunakan di dalam mqtt_as).

Kami meluncurkan dan... Kami mendapatkan MemoryError. Selain itu, semakin saya mencoba memahami di mana tepatnya memori bocor, semakin banyak cetakan debug yang saya tempatkan, semakin awal kesalahan ini muncul. Pencarian singkat di Google membawa saya pada pemahaman bahwa mikrokontroler, pada prinsipnya, hanya memiliki 30 kB memori, yang mana 65 kB kode (termasuk perpustakaan) tidak dapat ditampung.

Tapi ada jalan keluarnya. Ternyata micropython tidak mengeksekusi kode langsung dari file .py - file ini dikompilasi terlebih dahulu. Apalagi dikompilasi langsung di mikrokontroler, diubah menjadi bytecode, yang kemudian disimpan di memori. Agar kompiler dapat berfungsi, Anda juga memerlukan sejumlah RAM.

Caranya adalah dengan menyelamatkan mikrokontroler dari kompilasi yang intensif sumber daya. Anda dapat mengkompilasi file di komputer besar dan mengunggah bytecode yang sudah jadi ke mikrokontroler. Untuk melakukan ini, Anda perlu mengunduh firmware micropython dan membangun utilitas mpy-cross.

Saya tidak menulis Makefile, tetapi secara manual memeriksa dan mengkompilasi semua file yang diperlukan (termasuk perpustakaan) seperti ini

mpy-cross water_counter.py

Yang tersisa hanyalah mengunggah file dengan ekstensi .mpy, jangan lupa untuk menghapus terlebih dahulu .py yang sesuai dari sistem file perangkat.

Saya melakukan semua pengembangan di program (IDE?) ESPlorer. Ini memungkinkan Anda mengunggah skrip ke mikrokontroler dan segera menjalankannya. Dalam kasus saya, semua logika dan pembuatan semua objek terletak di file water_counter.py (.mpy). Tetapi agar semua ini dimulai secara otomatis, harus ada file bernama main.py di awal. Selain itu, itu harus persis .py, dan bukan .mpy yang telah dikompilasi sebelumnya. Berikut isinya yang sepele

import water_counter

Kami meluncurkannya - semuanya berfungsi. Namun memori bebasnya sangat kecil - sekitar 1kb. Saya masih memiliki rencana untuk memperluas fungsionalitas perangkat, dan kilobyte ini jelas tidak cukup bagi saya. Namun ternyata kasus ini juga ada jalan keluarnya.

Inilah masalahnya. Meskipun file dikompilasi menjadi bytecode dan berada di sistem file internal, kenyataannya file tersebut masih dimuat ke dalam RAM dan dieksekusi dari sana. Namun ternyata micropython dapat mengeksekusi bytecode langsung dari memori flash, namun untuk itu Anda perlu membangunnya langsung ke dalam firmware. Tidak sulit, walaupun memakan waktu cukup lama di netbook saya (hanya di sana kebetulan saya punya Linux).

Algoritmanya adalah sebagai berikut:

  • Unduh dan pasang ESP Buka SDK. Hal ini merakit kompiler dan perpustakaan untuk program untuk ESP8266. Dirakit sesuai petunjuk di halaman utama proyek (saya memilih pengaturan STANDALONE=yes)
  • Unduh jenis mikropython
  • Tempatkan perpustakaan yang diperlukan di ports/esp8266/modules di dalam pohon mikropython
  • Kami merakit firmware sesuai dengan instruksi di file port/esp8266/README.md
  • Kami mengunggah firmware ke mikrokontroler (saya melakukan ini di Windows menggunakan program ESP8266Flasher atau Python esptool)

Itu saja, sekarang 'import ssd1306' akan mengambil kode langsung dari firmware dan RAM tidak akan digunakan untuk ini. Dengan trik ini, saya hanya mengunggah kode perpustakaan ke dalam firmware, sedangkan kode program utama dijalankan dari sistem file. Ini memungkinkan Anda memodifikasi program dengan mudah tanpa mengkompilasi ulang firmware. Saat ini saya memiliki sekitar 8.5kb RAM kosong. Ini akan memungkinkan kami untuk mengimplementasikan banyak fungsi berguna yang berbeda di masa depan. Nah, jika memori tidak cukup sama sekali, maka Anda bisa memasukkan program utama ke dalam firmware.

Jadi apa yang harus kita lakukan sekarang?

Oke, perangkat keras sudah disolder, firmware sudah ditulis, kotak sudah dicetak, perangkat menempel di dinding dan bola lampu berkedip dengan gembira. Namun untuk saat ini semuanya hanya berupa kotak hitam (secara harfiah dan kiasan) dan masih sedikit gunanya. Saatnya melakukan sesuatu dengan pesan MQTT yang dikirim ke server.

“Rumah pintar” saya terus berputar Sistem Majordomo. Modul MQTT sudah dikeluarkan dari kotaknya, atau mudah dipasang dari pasar tambahan - saya tidak ingat dari mana saya mendapatkannya. MQTT bukanlah hal yang mandiri - Anda memerlukan apa yang disebut. broker - server yang menerima, mengurutkan, dan meneruskan pesan MQTT ke klien. Saya menggunakan Mosquitto, yang (seperti Majordomo) berjalan di netbook yang sama.

Setelah perangkat mengirim pesan setidaknya satu kali, nilainya akan langsung muncul di daftar.

Kami menghubungkan meteran air ke rumah pintar

Nilai-nilai ini sekarang dapat dikaitkan dengan objek sistem, dapat digunakan dalam skrip otomatisasi dan menjalani berbagai analisis - yang semuanya berada di luar cakupan artikel ini. Saya dapat merekomendasikan sistem mayordomo kepada siapa pun yang tertarik saluran Elektronik Dalam Lensa — seorang teman juga sedang membangun rumah pintar dan dengan jelas berbicara tentang pengaturan sistem.

Saya hanya akan menunjukkan beberapa grafik. Ini adalah grafik sederhana nilai harian

Kami menghubungkan meteran air ke rumah pintar
Terlihat hampir tidak ada orang yang menggunakan air tersebut pada malam hari. Beberapa kali seseorang pergi ke toilet, dan sepertinya filter reverse osmosis menyedot beberapa liter per malam. Di pagi hari, konsumsi meningkat secara signifikan. Saya biasanya menggunakan air dari ketel, tetapi kemudian saya ingin mandi dan untuk sementara beralih ke air panas kota - ini juga terlihat jelas pada grafik di bawah.

Dari grafik tersebut saya mengetahui bahwa untuk ke toilet dibutuhkan air sebanyak 6-7 liter, untuk mandi sebanyak 20-30 liter, untuk mencuci piring sebanyak 20 liter, dan untuk mandi sebanyak 160 liter. Keluarga saya mengkonsumsi sekitar 500-600 liter per hari.

Bagi mereka yang sangat penasaran, Anda dapat melihat catatan masing-masing nilai

Kami menghubungkan meteran air ke rumah pintar

Dari sini saya mengetahui bahwa ketika keran dibuka, air mengalir dengan kecepatan kurang lebih 1 liter per 5 detik.

Namun dalam bentuk ini, statistiknya mungkin tidak mudah untuk dilihat. Majordomo juga memiliki kemampuan untuk melihat grafik konsumsi berdasarkan hari, minggu dan bulan. Di sini, misalnya, adalah grafik konsumsi dalam bentuk batangan

Kami menghubungkan meteran air ke rumah pintar

Sejauh ini saya hanya punya data selama seminggu. Dalam sebulan, grafik ini akan lebih bersifat indikatif - setiap hari akan memiliki kolom terpisah. Gambarnya sedikit dirusak oleh penyesuaian nilai yang saya masukkan secara manual (kolom terbesar). Dan belum jelas apakah saya salah menetapkan nilai pertama, hampir satu kubus lebih kecil, atau apakah ini bug di firmware dan tidak semua liter dihitung. Perlu lebih banyak waktu.

Grafiknya sendiri masih membutuhkan keajaiban, pengapuran, dan pengecatan. Mungkin saya juga akan membuat grafik konsumsi memori untuk tujuan debugging - jika ada yang bocor di sana. Mungkin saya akan menampilkan periode ketika tidak ada Internet. Untuk saat ini, semua itu masih pada level ide.

Kesimpulan

Hari ini apartemen saya menjadi sedikit lebih pintar. Dengan alat sekecil itu, saya akan lebih mudah memantau konsumsi air di dalam rumah. Kalau dulu saya geram dengan “sekali lagi kita konsumsi air putih banyak dalam sebulan”, kini saya bisa menemukan sumber konsumsi tersebut.

Beberapa orang mungkin merasa aneh melihat pembacaan di layar jika jaraknya satu meter dari meteran itu sendiri. Namun dalam waktu yang tidak lama lagi, saya berencana untuk pindah ke apartemen lain, di mana akan ada beberapa penambah air, dan meterannya sendiri kemungkinan besar akan berlokasi di tangga. Jadi alat pembaca jarak jauh akan sangat berguna.

Saya juga berencana untuk memperluas fungsionalitas perangkat. Saya sudah melihat katup bermotor. Sekarang, untuk mengganti boiler ke air kota, saya perlu memutar 3 keran di ceruk yang sulit dijangkau. Akan jauh lebih mudah melakukan ini dengan satu tombol dengan indikasi yang sesuai. Tentu saja, ada baiknya menerapkan perlindungan terhadap kebocoran.

Dalam artikel tersebut saya menjelaskan versi perangkat saya berdasarkan ESP8266. Menurut pendapat saya, saya membuat versi firmware mikropython yang sangat menarik menggunakan coroutine - sederhana dan bagus. Saya mencoba memaparkan banyak nuansa dan kekurangan yang saya temui selama kampanye. Mungkin saya menggambarkan semuanya dengan terlalu detail; secara pribadi, sebagai pembaca, lebih mudah bagi saya untuk melewatkan hal-hal yang tidak perlu daripada memikirkan apa yang tidak terucapkan.

Seperti biasa, saya terbuka untuk kritik yang membangun.

Sumber
Sirkuit dan papan
Model Kasus

Sumber: www.habr.com

Tambah komentar