Kết nối đồng hồ nước với ngôi nhà thông minh

Ngày xửa ngày xưa, các hệ thống tự động hóa gia đình hay còn gọi là “ngôi nhà thông minh” như chúng thường được gọi, cực kỳ đắt đỏ và chỉ những người giàu mới có thể mua được. Ngày nay trên thị trường, bạn có thể tìm thấy những bộ dụng cụ khá rẻ tiền với cảm biến, nút/công tắc và bộ truyền động để điều khiển ánh sáng, ổ cắm, thông gió, cấp nước và các thiết bị tiêu dùng khác. Và ngay cả những người DIY quanh co nhất cũng có thể tham gia làm đẹp và lắp ráp các thiết bị cho ngôi nhà thông minh với mức giá không hề đắt.

Kết nối đồng hồ nước với ngôi nhà thông minh

Thông thường, các thiết bị được đề xuất là cảm biến hoặc cơ cấu chấp hành. Chúng giúp bạn dễ dàng thực hiện các tình huống như “khi cảm biến chuyển động được kích hoạt, hãy bật đèn” hoặc “công tắc gần lối ra sẽ tắt đèn trong toàn bộ căn hộ”. Nhưng bằng cách nào đó mọi thứ đã không diễn ra với phép đo từ xa. Tốt nhất, đó là biểu đồ nhiệt độ và độ ẩm hoặc công suất tức thời tại một ổ cắm cụ thể.

Gần đây tôi đã lắp đặt đồng hồ nước có đầu ra xung. Đối với mỗi lít đi qua đồng hồ, công tắc sậy sẽ được kích hoạt và đóng tiếp điểm. Điều duy nhất còn lại phải làm là bám vào dây và cố gắng thu được lợi ích từ nó. Ví dụ: phân tích lượng nước tiêu thụ theo giờ và ngày trong tuần. Chà, nếu căn hộ có một số ống tăng nước, thì việc xem tất cả các chỉ số hiện tại trên một màn hình sẽ thuận tiện hơn là trèo vào những ngóc ngách khó tiếp cận bằng đèn pin.

Bên dưới phần cắt là phiên bản thiết bị dựa trên ESP8266 của tôi, đếm xung từ đồng hồ nước và gửi số đọc qua MQTT đến máy chủ nhà thông minh. Chúng tôi sẽ lập trình bằng micropython bằng thư viện uasyncio. Khi tạo phần sụn, tôi gặp phải một số khó khăn thú vị mà tôi cũng sẽ thảo luận trong bài viết này. Đi!

Đề án

Kết nối đồng hồ nước với ngôi nhà thông minh

Trung tâm của toàn bộ mạch là một mô-đun trên bộ vi điều khiển ESP8266. ESP-12 ban đầu được lên kế hoạch, nhưng hóa ra chiếc của tôi lại bị lỗi. Chúng tôi phải hài lòng với mô-đun ESP-07 hiện có. May mắn thay, chúng giống nhau cả về chân cắm và chức năng, điểm khác biệt duy nhất là ở ăng-ten - ESP-12 có ăng-ten tích hợp, trong khi ESP-07 có ăng-ten bên ngoài. Tuy nhiên, ngay cả khi không có ăng-ten WiFi, tín hiệu trong phòng tắm của tôi vẫn nhận được bình thường.

Hệ thống dây điện mô-đun tiêu chuẩn:

  • nút đặt lại có kéo lên và tụ điện (mặc dù cả hai đều đã có trong mô-đun)
  • Tín hiệu kích hoạt (CH_PD) được kéo lên nguồn
  • GPIO15 bị kéo xuống đất. Cái này chỉ cần lúc đầu thôi, nhưng tôi vẫn chưa có gì để gắn vào chân này, tôi không cần nữa

Để đưa mô-đun vào chế độ phần sụn, bạn cần nối đất GPIO2 và để thuận tiện hơn, tôi đã cung cấp nút Khởi động. Trong điều kiện bình thường, chân này được kéo lên nguồn.

Trạng thái của đường GPIO2 chỉ được kiểm tra khi bắt đầu vận hành - khi cấp nguồn hoặc ngay sau khi đặt lại. Vì vậy, mô-đun khởi động như bình thường hoặc chuyển sang chế độ phần sụn. Sau khi được tải, mã pin này có thể được sử dụng như một GPIO thông thường. Chà, vì đã có sẵn một nút ở đó nên bạn có thể đính kèm một số chức năng hữu ích vào nó.

Để lập trình và gỡ lỗi, tôi sẽ sử dụng UART, được xuất ra lược. Khi cần, tôi chỉ cần kết nối bộ chuyển đổi USB-UART ở đó. Bạn chỉ cần nhớ rằng mô-đun được cấp nguồn 3.3V. Nếu bạn quên chuyển bộ chuyển đổi sang điện áp này và cấp nguồn 5V, rất có thể mô-đun sẽ bị cháy.

Tôi không gặp vấn đề gì với điện trong phòng tắm - ổ cắm nằm cách đồng hồ khoảng một mét nên tôi sẽ dùng điện 220V. Là một nguồn năng lượng tôi sẽ có một nhỏ khối HLK-PM03 của Tenstar Robot. Cá nhân tôi gặp khó khăn với các thiết bị điện tử tương tự và công suất, nhưng đây là bộ nguồn làm sẵn trong một hộp nhỏ.

Để báo hiệu các chế độ hoạt động, tôi cung cấp một đèn LED kết nối với GPIO2. Tuy nhiên, tôi đã không hàn nó lại, bởi vì... Mô-đun ESP-07 đã có đèn LED và nó cũng được kết nối với GPIO2. Nhưng để nó ở trên board, phòng trường hợp mình muốn xuất cái LED này ra case.

Hãy chuyển sang phần thú vị nhất. Đồng hồ nước không có logic; bạn không thể yêu cầu họ đo dòng điện. Điều duy nhất có sẵn cho chúng ta là sự thôi thúc - đóng các tiếp điểm của công tắc sậy mỗi lít. Đầu ra công tắc sậy của tôi được kết nối với GPIO12/GPIO13. Tôi sẽ kích hoạt điện trở kéo lên theo chương trình bên trong mô-đun.

Ban đầu, tôi quên cung cấp điện trở R8 và R9 và phiên bản bo mạch của tôi không có chúng. Nhưng vì tôi đã đăng sơ đồ cho mọi người xem nên cần sửa lại sự sơ suất này. Cần có điện trở để không làm cháy cổng nếu phần sụn trục trặc và đặt chân thành một, đồng thời công tắc sậy rút ngắn đường dây này xuống đất (với điện trở tối đa là 3.3V/1000Ohm = 3.3mA sẽ chảy).

Đã đến lúc phải suy nghĩ xem phải làm gì nếu mất điện. Tùy chọn đầu tiên là yêu cầu các giá trị bộ đếm ban đầu từ máy chủ khi bắt đầu. Nhưng điều này sẽ đòi hỏi sự phức tạp đáng kể của giao thức trao đổi. Hơn nữa, hiệu suất của thiết bị trong trường hợp này phụ thuộc vào trạng thái của máy chủ. Nếu máy chủ không khởi động sau khi tắt nguồn (hoặc khởi động muộn hơn), đồng hồ nước sẽ không thể yêu cầu các giá trị ban đầu và không hoạt động chính xác.

Vì vậy, tôi quyết định triển khai việc lưu các giá trị bộ đếm trong chip bộ nhớ được kết nối qua I2C. Tôi không có yêu cầu đặc biệt nào về kích thước của bộ nhớ flash - bạn chỉ cần lưu 2 con số (số lít theo đồng hồ nước nóng lạnh). Ngay cả mô-đun nhỏ nhất cũng sẽ làm được. Nhưng bạn cần chú ý đến số chu kỳ ghi. Đối với hầu hết các mô-đun, đây là 100 nghìn chu kỳ, đối với một số mô-đun lên tới một triệu.

Có vẻ như một triệu là rất nhiều. Nhưng trong 4 năm sống trong căn hộ của mình, tôi đã tiêu thụ hơn 500 mét khối nước một chút, tức là 500 nghìn lít! Và 500 nghìn bản ghi trong flash. Và đó chỉ là nước lạnh. Tất nhiên, bạn có thể hàn lại chip vài năm một lần, nhưng hóa ra vẫn có chip FRAM. Từ quan điểm lập trình, đây là cùng một EEPROM I2C, chỉ có số chu kỳ viết lại rất lớn (hàng trăm triệu). Chỉ là tôi vẫn chưa thể mang những vi mạch như vậy đến cửa hàng nên hiện tại, 24LC512 thông thường sẽ vẫn hoạt động.

Bảng mạch in

Ban đầu tôi định làm bảng ở nhà. Vì vậy, bảng được thiết kế ở dạng một mặt. Nhưng sau khi dành một giờ với bàn ủi laser và mặt nạ hàn (không hiểu sao không có nó), tôi vẫn quyết định đặt mua các tấm ván từ Trung Quốc.

Kết nối đồng hồ nước với ngôi nhà thông minh

Hầu như trước khi đặt mua bo mạch, tôi nhận ra rằng ngoài chip bộ nhớ flash, tôi có thể kết nối một thứ khác hữu ích với bus I2C, chẳng hạn như màn hình. Chính xác những gì để xuất ra nó vẫn còn là một câu hỏi, nhưng nó cần phải được định tuyến trên bảng. Chà, vì tôi định đặt mua bo mạch từ nhà máy nên chẳng ích gì khi giới hạn bản thân ở bo mạch một mặt, vì vậy các đường I2C là những đường duy nhất ở mặt sau của bo mạch.

Ngoài ra còn có một vấn đề lớn với hệ thống dây điện một chiều. Bởi vì Bảng mạch được vẽ ở dạng một mặt, do đó, các đường ray và thành phần SMD được lên kế hoạch đặt ở một bên, còn các thành phần đầu ra, đầu nối và nguồn điện ở bên kia. Một tháng sau, khi tôi nhận được bo mạch, tôi quên mất kế hoạch ban đầu và hàn tất cả các bộ phận ở mặt trước. Và chỉ khi hàn bộ nguồn thì hóa ra điểm cộng và điểm trừ được nối ngược lại. Tôi đã phải trang trại với người nhảy. Trong hình trên, tôi đã thay đổi hệ thống dây điện, nhưng mặt đất được chuyển từ phần này sang phần khác của bảng thông qua các chân của nút Khởi động (mặc dù có thể vẽ đường trên lớp thứ hai).

Hóa ra như thế này

Kết nối đồng hồ nước với ngôi nhà thông minh

Nhà ở

Bước tiếp theo là cơ thể. Nếu bạn có máy in 3D thì đây không phải là vấn đề. Tôi không bận tâm quá nhiều - tôi chỉ vẽ một chiếc hộp có kích thước phù hợp và cắt bỏ đúng chỗ. Nắp được gắn vào thân máy bằng vít tự khai thác nhỏ.

Kết nối đồng hồ nước với ngôi nhà thông minh

Tôi đã đề cập rằng nút Khởi động có thể được sử dụng làm nút có mục đích chung - vì vậy chúng tôi sẽ hiển thị nó trên bảng mặt trước. Để làm điều này, tôi đã vẽ một cái “giếng” đặc biệt nơi có nút.

Kết nối đồng hồ nước với ngôi nhà thông minh

Bên trong hộp cũng có các đinh tán để lắp bo mạch và cố định bằng một vít M3 duy nhất (không còn chỗ trống trên bo mạch)

Tôi đã chọn màn hình hiển thị khi in phiên bản mẫu đầu tiên của hộp đựng. Đầu đọc hai dòng tiêu chuẩn không vừa với hộp đựng này, nhưng ở phía dưới có màn hình OLED SSD1306 128×32. Nó hơi nhỏ nhưng tôi không cần phải nhìn chằm chằm vào nó hàng ngày - nó quá sức đối với tôi.

Đang tìm cách đi dây từ nó, tôi quyết định dán màn hình vào giữa hộp. Tất nhiên, công thái học là dưới mức bình thường - nút ở trên cùng, màn hình ở phía dưới. Nhưng tôi đã nói rồi rằng ý tưởng gắn màn hình đến quá muộn và tôi cũng lười đi dây lại bảng để di chuyển nút.

Thiết bị đã được lắp ráp. Mô-đun hiển thị được dán vào lỗ mũi bằng keo nóng

Kết nối đồng hồ nước với ngôi nhà thông minh

Kết nối đồng hồ nước với ngôi nhà thông minh

Kết quả cuối cùng có thể được nhìn thấy trên KDPV

Chương trình cơ sở

Hãy chuyển sang phần phần mềm. Đối với những nghề thủ công nhỏ như thế này, tôi thực sự thích sử dụng Python (cuộc chạy bộ) - mã hóa ra rất nhỏ gọn và dễ hiểu. May mắn thay, không cần phải xuống cấp độ đăng ký để giảm bớt micro giây - mọi thứ đều có thể được thực hiện từ Python.

Có vẻ như mọi thứ đều đơn giản nhưng không hề đơn giản - thiết bị có một số chức năng độc lập:

  • Người dùng nhấn nút và nhìn vào màn hình
  • Lít đánh dấu và cập nhật giá trị trong bộ nhớ flash
  • Mô-đun giám sát tín hiệu WiFi và kết nối lại nếu cần thiết
  • Chà, không có bóng đèn nhấp nháy thì không thể

Bạn không thể cho rằng một chức năng không hoạt động nếu một chức năng khác bị kẹt vì lý do nào đó. Tôi đã lấp đầy xương rồng trong các dự án khác và bây giờ tôi vẫn thấy trục trặc kiểu “bỏ lỡ một lít nữa vì màn hình đang cập nhật vào thời điểm đó” hoặc “người dùng không thể làm gì trong khi mô-đun đang kết nối với Wifi." Tất nhiên, một số việc có thể được thực hiện thông qua các ngắt, nhưng bạn có thể gặp phải các hạn chế về thời lượng, lồng các cuộc gọi hoặc các thay đổi phi nguyên tử đối với các biến. Chà, đoạn mã thực hiện mọi thứ nhanh chóng trở nên lộn xộn.

В dự án nghiêm túc hơn Tôi đã sử dụng tính năng đa nhiệm ưu tiên cổ điển và FreeRTOS, nhưng trong trường hợp này, mô hình này hóa ra lại phù hợp hơn nhiều thư viện coroutine và uasync . Hơn nữa, việc triển khai coroutines bằng Python thật sự rất tuyệt vời - mọi thứ đều được thực hiện đơn giản và thuận tiện cho người lập trình. Chỉ cần viết logic của riêng bạn, chỉ cần cho tôi biết bạn có thể chuyển đổi giữa các luồng ở những vị trí nào.

Tôi đề nghị nghiên cứu sự khác biệt giữa đa nhiệm ưu tiên và đa nhiệm cạnh tranh như một chủ đề tùy chọn. Bây giờ cuối cùng chúng ta hãy chuyển sang mã.

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

Mỗi bộ đếm được xử lý bởi một thể hiện của lớp Counter. Trước hết, giá trị bộ đếm ban đầu được trừ khỏi EEPROM (value_storage) - đây là cách thực hiện quá trình phục hồi sau khi mất điện.

Chân này được khởi tạo bằng một tín hiệu kéo lên nguồn điện tích hợp: nếu công tắc sậy đóng, đường dây bằng XNUMX, nếu đường dây mở, nó được kéo lên nguồn điện và bộ điều khiển sẽ đọc một.

Một nhiệm vụ riêng biệt cũng được đưa ra ở đây, nhiệm vụ này sẽ thăm dò mã pin. Mỗi bộ đếm sẽ thực hiện nhiệm vụ riêng của mình. Đây là mã của cô ấy

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

Cần có độ trễ 25ms để lọc độ nảy của tiếp điểm, đồng thời nó điều chỉnh tần suất thức dậy của tác vụ (trong khi tác vụ này đang ngủ, các tác vụ khác đang chạy). Cứ sau 25ms, chức năng sẽ hoạt động, kiểm tra chân cắm và nếu các tiếp điểm của công tắc sậy đóng thì có nghĩa là một lít khác đã đi qua đồng hồ và việc này cần được xử lý.

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

        self._value_storage.write(self._value)

Việc xử lý lít tiếp theo là chuyện nhỏ - bộ đếm chỉ đơn giản là tăng lên. Chà, thật tuyệt nếu ghi giá trị mới vào ổ đĩa flash.

Để dễ sử dụng, các “bộ truy cập” được cung cấp

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

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

Chà, bây giờ chúng ta hãy tận dụng những điểm thú vị của Python và thư viện uasync và tạo một đối tượng truy cập có thể chờ đợi (làm cách nào chúng ta có thể dịch nó sang tiếng Nga? Đối tượng mà bạn có thể mong đợi?)

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

        return self.value()

    __iter__ = __await__  

Đây là một hàm tiện lợi chờ cho đến khi giá trị bộ đếm được cập nhật - hàm này thỉnh thoảng thức dậy và kiểm tra cờ _value_changed. Điều thú vị về hàm này là mã gọi có thể chuyển sang chế độ ngủ trong khi gọi hàm này và ngủ cho đến khi nhận được giá trị mới.

Còn sự gián đoạn thì sao?Vâng, tại thời điểm này, bạn có thể troll tôi, nói rằng chính bạn đã nói về sự gián đoạn, nhưng trên thực tế, bạn đã thực hiện một cuộc thăm dò ghim ngu ngốc. Trên thực tế, ngắt là điều đầu tiên tôi đã thử. Trong ESP8266, bạn có thể tổ chức ngắt cạnh và thậm chí viết trình xử lý cho ngắt này bằng Python. Trong ngắt này, giá trị của một biến có thể được cập nhật. Có lẽ, điều này là đủ nếu bộ đếm là một thiết bị phụ - một thiết bị đợi cho đến khi được yêu cầu giá trị này.

Thật không may (hoặc may mắn thay?) thiết bị của tôi đang hoạt động, nó phải tự gửi tin nhắn qua giao thức MQTT và ghi dữ liệu vào EEPROM. Và ở đây các hạn chế có hiệu lực - bạn không thể phân bổ bộ nhớ theo các ngắt và sử dụng một ngăn xếp lớn, điều đó có nghĩa là bạn có thể quên việc gửi tin nhắn qua mạng. Có những buntu như micropython.schedule() cho phép bạn chạy một số chức năng “càng sớm càng tốt”, nhưng câu hỏi đặt ra là “để làm gì?” Điều gì sẽ xảy ra nếu chúng ta đang gửi một loại tin nhắn nào đó ngay bây giờ và sau đó có một ngắt xuất hiện và làm hỏng giá trị của các biến. Hoặc, ví dụ: một giá trị bộ đếm mới được gửi đến từ máy chủ trong khi chúng tôi chưa ghi lại giá trị cũ. Nói chung, bạn cần chặn đồng bộ hóa hoặc thoát khỏi nó bằng cách nào đó.

Và thỉnh thoảng RuntimeError: lịch trình ngăn xếp đầy sự cố và ai biết tại sao?

Với bỏ phiếu rõ ràng và uasync, trong trường hợp này bằng cách nào đó nó trở nên đẹp hơn và đáng tin cậy hơn

Tôi đã mang bài tập về EEPROM đến một lớp học nhỏ

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)

Trong Python, rất khó để làm việc trực tiếp với byte, nhưng chính byte mới được ghi vào bộ nhớ. Tôi đã phải thực hiện chuyển đổi giữa số nguyên và byte bằng thư viện ustruct.

Để không phải chuyển đối tượng I2C và địa chỉ của ô nhớ mỗi lần, tôi gói gọn tất cả trong một gói cổ điển nhỏ gọn và tiện lợi

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)

Bản thân đối tượng I2C được tạo với các tham số này

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

Chúng ta đến phần thú vị nhất - việc thực hiện giao tiếp với máy chủ thông qua MQTT. Chà, không cần phải tự thực hiện giao thức - tôi đã tìm thấy nó trên Internet thực hiện không đồng bộ làm sẵn. Đây là những gì chúng tôi sẽ sử dụng.

Tất cả những điều thú vị nhất được thu thập trong lớp CounterMQTTClient, dựa trên thư viện MQTTClient. Hãy bắt đầu từ ngoại vi

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

Tại đây, bạn có thể tạo và định cấu hình các chân và nút bóng đèn cũng như các đối tượng đồng hồ đo nước nóng và lạnh.

Với việc khởi tạo, không phải mọi thứ đều tầm thường

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

Để đặt các tham số vận hành của thư viện mqtt_as, một từ điển lớn gồm các cài đặt khác nhau được sử dụng - config. Hầu hết các cài đặt mặc định đều phù hợp với chúng tôi, nhưng nhiều cài đặt cần được đặt rõ ràng. Để không viết cài đặt trực tiếp vào mã, tôi lưu trữ chúng trong tệp văn bản config.txt. Điều này cho phép bạn thay đổi mã bất kể cài đặt nào, cũng như tán một số thiết bị giống hệt nhau với các thông số khác nhau.

Khối mã cuối cùng khởi động một số coroutine để phục vụ các chức năng khác nhau của hệ thống. Ví dụ: đây là một coroutine phục vụ bộ đếm

    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 chờ trong vòng lặp để tìm giá trị bộ đếm mới và ngay khi nó xuất hiện, nó sẽ gửi tin nhắn qua giao thức MQTT. Đoạn mã đầu tiên gửi giá trị ban đầu ngay cả khi không có nước chảy qua bộ đếm.

Lớp cơ sở MQTTClient tự phục vụ, khởi tạo kết nối WiFi và kết nối lại khi mất kết nối. Khi có thay đổi về trạng thái kết nối WiFi, thư viện sẽ thông báo cho chúng tôi bằng cách gọi 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)

Chức năng này được sao chép trung thực từ các ví dụ. Trong trường hợp này, nó đếm số lần ngừng hoạt động (internet_outages) và thời lượng của chúng. Khi kết nối được khôi phục, thời gian rảnh sẽ được gửi đến máy chủ.

Nhân tiện, giấc ngủ cuối cùng chỉ cần thiết để làm cho hàm không đồng bộ - trong thư viện, nó được gọi thông qua chờ đợi và chỉ những hàm có phần thân chứa một sự chờ đợi khác mới có thể được gọi.

Ngoài việc kết nối với WiFi, bạn cũng cần thiết lập kết nối với nhà môi giới (máy chủ) MQTT. Thư viện cũng thực hiện điều này và chúng tôi có cơ hội làm điều gì đó hữu ích khi kết nối được thiết lập

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

Ở đây chúng tôi đăng ký một số tin nhắn - máy chủ hiện có khả năng đặt các giá trị bộ đếm hiện tại bằng cách gửi tin nhắn tương ứng.

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

Chức năng này xử lý các tin nhắn đến và tùy thuộc vào chủ đề (tiêu đề tin nhắn), các giá trị của một trong các bộ đếm sẽ được cập nhật

Một số chức năng trợ giúp

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

Chức năng này sẽ gửi tin nhắn nếu kết nối được thiết lập. Nếu không có kết nối, tin nhắn sẽ bị bỏ qua.

Và đây chỉ là một chức năng tiện lợi giúp tạo và gửi thông báo gỡ lỗi.

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

Quá nhiều văn bản và chúng tôi vẫn chưa nhấp nháy đèn LED. Đây

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

Tôi đã cung cấp 2 chế độ nhấp nháy. Nếu kết nối bị mất (hoặc mới được thiết lập), thiết bị sẽ nhấp nháy nhanh. Nếu kết nối được thiết lập, thiết bị sẽ nhấp nháy 5 giây một lần. Nếu cần, các chế độ nhấp nháy khác có thể được thực hiện tại đây.

Nhưng đèn LED chỉ mang tính nuông chiều. Chúng tôi cũng nhắm vào màn hình.

    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)

Đây chính là điều tôi đang nói đến - việc sử dụng coroutine đơn giản và tiện lợi như thế nào. Chức năng nhỏ này mô tả TOÀN BỘ trải nghiệm người dùng. Coroutine chỉ cần đợi nút được nhấn và bật màn hình trong 3 giây. Màn hình hiển thị số đo hiện tại của đồng hồ.

Vẫn còn lại một vài điều nhỏ nhặt. Đây là chức năng (lại) khởi động lại toàn bộ doanh nghiệp này. Vòng lặp chính chỉ gửi các thông tin gỡ lỗi khác nhau mỗi phút một lần. Nói chung là tôi trích dẫn như vậy - tôi nghĩ không cần phải bình luận quá nhiều

   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)

Chà, thêm một vài cài đặt và hằng số để hoàn thành mô tả

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

Tất cả bắt đầu như thế này

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

Có điều gì đó đã xảy ra với trí nhớ của tôi

Vì vậy, tất cả mã đều ở đó. Tôi đã tải các tệp lên bằng tiện ích ampy - nó cho phép bạn tải chúng lên ổ đĩa flash bên trong (ổ đĩa trong chính ESP-07) và sau đó truy cập nó từ chương trình dưới dạng các tệp thông thường. Ở đó tôi cũng đã tải lên các thư viện mqtt_as, uasyncio, ssd1306 và các bộ sưu tập mà tôi đã sử dụng (được sử dụng bên trong mqtt_as).

Chúng tôi khởi chạy và... Chúng tôi nhận được MemoryError. Hơn nữa, tôi càng cố gắng tìm hiểu chính xác bộ nhớ bị rò rỉ ở đâu, tôi càng đặt nhiều bản in gỡ lỗi thì lỗi này càng xuất hiện sớm. Một tìm kiếm ngắn trên Google đã giúp tôi hiểu rằng về nguyên tắc, bộ vi điều khiển chỉ có 30 kB bộ nhớ, trong đó 65 kB mã (bao gồm cả các thư viện) đơn giản là không thể chứa vừa.

Nhưng có một lối thoát. Hóa ra micropython không thực thi mã trực tiếp từ tệp .py - tệp này được biên dịch trước. Hơn nữa, nó được biên dịch trực tiếp trên vi điều khiển, chuyển thành mã byte, sau đó được lưu trữ trong bộ nhớ. Chà, để trình biên dịch hoạt động, bạn cũng cần một lượng RAM nhất định.

Bí quyết là cứu bộ vi điều khiển khỏi quá trình biên dịch tốn nhiều tài nguyên. Bạn có thể biên dịch các tệp trên một máy tính lớn và tải mã byte tạo sẵn lên bộ vi điều khiển. Để thực hiện việc này, bạn cần tải xuống chương trình cơ sở micropython và xây dựng tiện ích mpy-cross.

Tôi không viết Makefile mà xem xét và biên dịch thủ công tất cả các tệp cần thiết (bao gồm cả thư viện), đại loại như thế này

mpy-cross water_counter.py

Tất cả những gì còn lại là tải lên các tệp có phần mở rộng .mpy, trước tiên không quên xóa .py tương ứng khỏi hệ thống tệp của thiết bị.

Tôi đã thực hiện tất cả quá trình phát triển trong chương trình (IDE?) ESPlorer. Nó cho phép bạn tải các tập lệnh lên bộ vi điều khiển và thực thi chúng ngay lập tức. Trong trường hợp của tôi, tất cả logic và việc tạo tất cả các đối tượng đều nằm trong tệp Water_counter.py (.mpy). Nhưng để tất cả điều này bắt đầu tự động, cũng phải có một tệp có tên main.py khi bắt đầu. Hơn nữa, nó phải chính xác là .py chứ không phải .mpy được biên dịch trước. Đây là nội dung tầm thường của nó

import water_counter

Chúng tôi khởi chạy nó - mọi thứ đều hoạt động. Nhưng bộ nhớ trống nhỏ đến mức đáng báo động - khoảng 1kb. Tôi vẫn có kế hoạch mở rộng chức năng của thiết bị và kilobyte này rõ ràng là không đủ đối với tôi. Nhưng hóa ra cũng có một lối thoát cho trường hợp này.

Vấn đề là như thế này. Mặc dù các tệp được biên dịch thành mã byte và nằm trên hệ thống tệp nội bộ nhưng trên thực tế, chúng vẫn được tải vào RAM và được thực thi từ đó. Nhưng hóa ra micropython có thể thực thi mã byte trực tiếp từ bộ nhớ flash, nhưng để làm được điều này, bạn cần tích hợp nó trực tiếp vào phần sụn. Nó không khó, mặc dù nó mất khá nhiều thời gian trên netbook của tôi (chỉ ở đó tôi tình cờ có Linux).

Thuật toán như sau:

  • Tải xuống và cài đặt SDK mở ESP. Thứ này tập hợp một trình biên dịch và các thư viện cho các chương trình dành cho ESP8266. Được lắp ráp theo hướng dẫn trên trang chính của dự án (Tôi đã chọn cài đặt STANDALONE=yes)
  • Tải về các loại micropython
  • Đặt các thư viện cần thiết vào cổng/esp8266/mô-đun bên trong cây micropython
  • Chúng ta ráp firmware theo hướng dẫn trong file cổng/esp8266/README.md
  • Chúng tôi tải chương trình cơ sở lên vi điều khiển (Tôi thực hiện việc này trên Windows bằng chương trình ESP8266Flasher hoặc Python esptool)

Thế là xong, bây giờ 'nhập ssd1306' sẽ lấy mã trực tiếp từ phần sụn và RAM sẽ không bị tiêu tốn cho việc này. Với thủ thuật này, tôi chỉ tải mã thư viện lên chương trình cơ sở, trong khi mã chương trình chính được thực thi từ hệ thống tệp. Điều này cho phép bạn dễ dàng sửa đổi chương trình mà không cần biên dịch lại phần sụn. Hiện tại tôi có khoảng 8.5kb RAM trống. Điều này sẽ cho phép chúng tôi triển khai khá nhiều chức năng hữu ích khác nhau trong tương lai. Chà, nếu không có đủ bộ nhớ, thì bạn có thể đẩy chương trình chính vào phần sụn.

Vậy chúng ta nên làm gì với nó bây giờ?

Ok, phần cứng đã được hàn, firmware được viết, hộp được in ra, máy được dán lên tường và vui vẻ nhấp nháy một bóng đèn. Nhưng hiện tại tất cả chỉ là một hộp đen (theo nghĩa đen và nghĩa bóng) và nó vẫn ít được sử dụng. Đã đến lúc phải làm gì đó với các tin nhắn MQTT được gửi đến máy chủ.

“Ngôi nhà thông minh” của tôi đang hoạt động Hệ thống Majordomo. Mô-đun MQTT được bán ra khỏi hộp hoặc có thể dễ dàng cài đặt từ thị trường tiện ích bổ sung - Tôi không nhớ mình đã lấy nó từ đâu. MQTT không phải là thứ tự cung tự cấp - bạn cần có cái gọi là. nhà môi giới - một máy chủ nhận, sắp xếp và chuyển tiếp tin nhắn MQTT cho khách hàng. Tôi sử dụng mosquitto, (như Majordomo) chạy trên cùng một netbook.

Sau khi thiết bị gửi tin nhắn ít nhất một lần, giá trị sẽ ngay lập tức xuất hiện trong danh sách.

Kết nối đồng hồ nước với ngôi nhà thông minh

Các giá trị này hiện có thể được liên kết với các đối tượng hệ thống, chúng có thể được sử dụng trong các tập lệnh tự động hóa và chịu nhiều phân tích khác nhau - tất cả đều nằm ngoài phạm vi của bài viết này. Tôi có thể giới thiệu hệ thống Majordomo cho bất kỳ ai quan tâm kênh Điện Tử Trong Ống Kính — một người bạn cũng đang xây dựng một ngôi nhà thông minh và nói rõ ràng về việc thiết lập hệ thống.

Tôi sẽ chỉ cho bạn xem một vài biểu đồ. Đây là một biểu đồ đơn giản về giá trị hàng ngày

Kết nối đồng hồ nước với ngôi nhà thông minh
Có thể thấy hầu như không có ai sử dụng nước vào ban đêm. Một vài lần có người đi vệ sinh và có vẻ như bộ lọc thẩm thấu ngược hút vài lít mỗi đêm. Vào buổi sáng, mức tiêu thụ tăng đáng kể. Tôi thường sử dụng nước từ nồi hơi, nhưng sau đó tôi muốn đi tắm và tạm thời chuyển sang sử dụng nước nóng của thành phố - điều này cũng được thể hiện rõ ở biểu đồ phía dưới.

Từ biểu đồ này tôi biết được rằng đi vệ sinh cần 6-7 lít nước, đi tắm cần 20-30 lít, rửa bát cần khoảng 20 lít, và đi tắm cần 160 lít. Gia đình tôi tiêu thụ khoảng 500-600 lít mỗi ngày.

Đối với những người đặc biệt tò mò, bạn có thể xem các bản ghi cho từng giá trị riêng lẻ

Kết nối đồng hồ nước với ngôi nhà thông minh

Từ đây tôi biết được rằng khi mở vòi, nước chảy với tốc độ khoảng 1 lít/5 giây.

Nhưng ở dạng này, số liệu thống kê có lẽ không thuận tiện để xem xét. Majordomo còn có khả năng xem biểu đồ tiêu thụ theo ngày, tuần và tháng. Ví dụ: đây là biểu đồ tiêu thụ tính bằng thanh

Kết nối đồng hồ nước với ngôi nhà thông minh

Cho đến nay tôi chỉ có dữ liệu trong một tuần. Trong một tháng, biểu đồ này sẽ mang tính biểu thị nhiều hơn - mỗi ngày sẽ có một cột riêng. Hình ảnh hơi bị hỏng do điều chỉnh các giá trị mà tôi nhập thủ công (cột lớn nhất). Và vẫn chưa rõ liệu tôi đã đặt sai các giá trị đầu tiên, gần như ít hơn một khối hay đây là lỗi trong phần sụn và không phải tất cả lít đều được tính. Cần thêm thời gian.

Bản thân các đồ thị vẫn cần một chút phép thuật, tẩy trắng, vẽ tranh. Có lẽ tôi cũng sẽ xây dựng một biểu đồ về mức tiêu thụ bộ nhớ cho mục đích gỡ lỗi, đề phòng có thứ gì đó bị rò rỉ ở đó. Có lẽ bằng cách nào đó tôi sẽ hiển thị những khoảng thời gian không có Internet. Hiện tại, tất cả điều này chỉ ở cấp độ ý tưởng.

Kết luận

Hôm nay căn hộ của tôi đã trở nên thông minh hơn một chút. Với một thiết bị nhỏ như vậy, tôi sẽ thuận tiện hơn trong việc theo dõi lượng nước tiêu thụ trong nhà. Nếu trước đó tôi phẫn nộ vì “lại chúng ta tiêu thụ rất nhiều nước trong một tháng”, thì bây giờ tôi đã tìm ra được nguồn gốc của việc tiêu thụ này.

Một số người có thể thấy lạ khi nhìn vào số đọc trên màn hình nếu nó cách đồng hồ một mét. Nhưng trong một tương lai không xa, tôi dự định chuyển đến một căn hộ khác, nơi sẽ có một số ống nước đứng và rất có thể đồng hồ đo sẽ được đặt ở đầu cầu thang. Vì vậy một thiết bị đọc từ xa sẽ rất hữu ích.

Tôi cũng có kế hoạch mở rộng chức năng của thiết bị. Tôi đã xem xét các van có động cơ. Bây giờ, để chuyển nồi hơi sang dùng nước thành phố, tôi cần vặn 3 vòi vào một ngóc ngách khó tiếp cận. Sẽ thuận tiện hơn nhiều nếu thực hiện việc này bằng một nút có chỉ báo tương ứng. Tất nhiên, việc thực hiện bảo vệ chống rò rỉ là cần thiết.

Trong bài viết, tôi đã mô tả phiên bản thiết bị dựa trên ESP8266 của mình. Theo ý kiến ​​​​của tôi, tôi đã nghĩ ra một phiên bản phần mềm micropython rất thú vị bằng cách sử dụng coroutines - đơn giản và hay. Tôi đã cố gắng mô tả nhiều sắc thái và khuyết điểm mà tôi gặp phải trong chiến dịch. Có lẽ tôi đã mô tả mọi thứ quá chi tiết; cá nhân tôi, với tư cách là một độc giả, tôi dễ dàng bỏ qua những điều không cần thiết hơn là sau này mới nghĩ ra những điều chưa nói.

Như mọi khi, tôi sẵn sàng đón nhận những lời chỉ trích mang tính xây dựng.

Mã nguồn
Mạch và bảng
Mô hình trường hợp

Nguồn: www.habr.com

Thêm một lời nhận xét