نربط عداد المياه بمنزل ذكي

مرة واحدة كانت أنظمة التشغيل الآلي للمنزل ، أو كما يطلق عليها في كثير من الأحيان "المنزل الذكي" ، كانت باهظة الثمن للغاية والأغنياء فقط يمكن أن تحملها. اليوم في السوق ، يمكنك العثور على مجموعات رخيصة الثمن تمامًا مع أجهزة استشعار وأزرار / مفاتيح ومشغلات للتحكم في الإضاءة والمقابس والتهوية وإمدادات المياه والمستهلكين الآخرين. وحتى أكثر krivoruky DIY-shnik يمكن أن ينضم إلى الأجهزة الجميلة ويجمعها لمنزل ذكي وغير مكلف.



كقاعدة ، تكون الأجهزة المقترحة إما أجهزة استشعار أو مشغلات. إنها تجعل من السهل تنفيذ سيناريوهات مثل "تشغيل الضوء عند تشغيل مستشعر الحركة" أو "المفتاح عند المخرج يطفئ الضوء في جميع أنحاء الشقة". لكن القياس عن بعد بطريقة ما لم ينجح. في أفضل الأحوال ، هذا رسم بياني لدرجة الحرارة والرطوبة ، أو الطاقة الآنية في منفذ معين.

لقد قمت مؤخرًا بتثبيت عداد مياه بإخراج نبضي. يتم تشغيل مفتاح القصب من خلال كل لتر من خلال العداد ويغلق الاتصال. الشيء الوحيد المتبقي هو التمسك بالأسلاك ومحاولة الاستفادة منها. على سبيل المثال ، قم بتحليل استهلاك المياه بساعة ويوم الأسبوع. حسنًا ، إذا كان هناك عدة رافعات للمياه في الشقة ، فمن الملائم رؤية جميع المؤشرات الحالية على شاشة واحدة من تسلق منافذ يصعب الوصول إليها باستخدام مصباح يدوي.

تحت القطع ، تعتمد إصداري للجهاز على ESP8266 ، الذي يحسب النبضات من عدادات المياه ويرسل القراءات إلى خادم المنزل الذكي عبر MQTT. سنبرمج في ميكروبيثون باستخدام مكتبة uasyncio. عند إنشاء البرامج الثابتة ، صادفت العديد من الصعوبات المثيرة للاهتمام ، والتي سأناقشها أيضًا في هذه المقالة. دعنا نذهب!

مخطط




قلب الدائرة بأكملها هو الوحدة النمطية على متحكم ESP8266. كان ESP-12 مخططًا في الأصل ، لكن تبين لي أنه معيب. كان علي أن أكون راضيًا عن وحدة ESP-07 ، التي كانت متاحة. لحسن الحظ ، فإنهما متماثلان في الاستنتاجات والوظيفة ، والفرق هو فقط في الهوائي - يحتوي ESP-12 على مدمج ، و ESP-07 على واحد خارجي. ومع ذلك ، حتى بدون هوائي WiFi ، يتم التقاط الإشارة في حمامي بشكل طبيعي.

ربط الوحدة قياسي:

  • زر إعادة الضبط مع حمالة ومكثف (على الرغم من وجودهما بالفعل داخل الوحدة)
  • سحب إشارة تمكين (CH_PD) إلى الطاقة
  • انسحب GPIO15 على الأرض. هذا ضروري فقط في البداية ، ولكن لا يزال لدي شيء للتشبث بهذه الساق بعد الآن

لوضع الوحدة في وضع البرامج الثابتة ، تحتاج إلى إغلاق GPIO2 على الأرض ، ولجعلها أكثر ملاءمة ، قمت بتوفير زر التمهيد. في الحالة العادية ، يتم سحب هذا الدبوس إلى الطاقة.

يتم التحقق من حالة خط GPIO2 فقط في بداية العمل - عند تطبيق الطاقة أو مباشرة بعد إعادة التعيين. لذلك يتم تحميل الوحدة إما كالمعتاد ، أو تذهب إلى وضع البرامج الثابتة. بعد التحميل ، يمكن استخدام هذا الإخراج باعتباره GPIO عادي. حسنًا ، نظرًا لوجود زر بالفعل هناك ، يمكنك وضع بعض الوظائف المفيدة عليه.

للبرمجة والتصحيح ، سأستخدم UART ، الذي تم إحضاره إلى المشط. عند الضرورة - أقوم فقط بتوصيل محول USB-UART. تحتاج فقط إلى تذكر أن الوحدة تعمل بالطاقة 3.3V. إذا نسيت تبديل المحول إلى هذا الجهد وتطبيق 5V ، فمن المرجح أن تحترق الوحدة.

ليس لدي أي مشاكل في الكهرباء في الحمام - يقع المنفذ على بعد متر من الأمتار ، لذلك سأقوم بتشغيله من 220 فولت. كمصدر للطاقة ، سأعمل على كتلة صغيرة من HLK-PM03 من تينستار روبوت. أنا شخصياً محكم في التعامل مع الإلكترونيات التناظرية والكهربائية ، ولكن هنا مصدر طاقة منتهي في حالة صغيرة.

للإشارة إلى أوضاع التشغيل ، قمت بتوفير LED متصل بـ GPIO2. ومع ذلك ، لم أبدأ في لحامها ، لأن تحتوي وحدة ESP-07 بالفعل على LED ، علاوة على ذلك ، متصل بنفس GPIO2. ولكن دعها تكون على اللوحة - فجأة أريد أن أحمل هذا LED إلى العلبة.

ننتقل إلى الأكثر إثارة للاهتمام. عدادات المياه ليس لها منطق ؛ لا يمكن أن يطلب منها القراءات الحالية. الشيء الوحيد المتاح لنا هو البقول - إغلاق جهات اتصال مفتاح القصب كل لتر. يتم وضع استنتاجات مفاتيح القصب في GPIO12 / GPIO13. سأقوم بتشغيل المقاوم للسحب برمجيًا داخل الوحدة النمطية.

في البداية ، نسيت تقديم مقاومات R8 و R9 ، وفي نسختي من اللوحة ليست كذلك. ولكن بما أنني أقوم بالفعل بعرض المخطط على الملأ ، فمن الجدير تصحيح هذا الإشراف. هناك حاجة إلى مقاومات حتى لا تحرق المنفذ إذا كانت البرامج الثابتة هي عربات التي تجرها الدواب وتضع الوحدة على الدبوس ، ويحول مفتاح القصب هذا الخط إلى الأرض (بحد أقصى 3.3 فولت / 1000 أوم = 3.3 مللي أمبير يتدفق مع المقاوم).

حان الوقت للتفكير في ما يجب فعله إذا انقطعت الكهرباء. الخيار الأول هو أن تطلب من الخادم العدادات الأولية في البداية. لكن هذا يتطلب تعقيدًا كبيرًا لبروتوكول التبادل. علاوة على ذلك ، يعتمد تشغيل الجهاز في هذه الحالة على حالة الخادم. إذا ، بعد إيقاف تشغيل الضوء ، لم يبدأ الخادم (أو بدأ في وقت لاحق) ، فلن يتمكن عداد المياه من طلب القيم الأولية وسيعمل بشكل غير صحيح.

لذلك ، قررت تنفيذ تخزين قيم العداد في شريحة ذاكرة متصلة عبر I2C. ليس لدي أي متطلبات خاصة لحجم ذاكرة الفلاش - أحتاج إلى حفظ رقمين فقط (عدد اللترات بواسطة عدادات المياه الساخنة والباردة). حتى أصغر وحدة ستفعل. ولكن في عدد دورات التسجيل ، عليك الانتباه. بالنسبة لمعظم الوحدات ، هذا هو 100 ألف دورة ، لبعضها يصل إلى مليون.

يبدو أن المليون كثير. ولكن لمدة 4 سنوات من العيش في شقتي ، استهلكت أكثر من 500 متر مكعب من الماء ، أي 500 ألف لتر! و 500 ألف مدخل في الفلاش. وهذه مياه باردة فقط. يمكنك بالطبع لحام الشريحة كل عامين ، ولكن اتضح أن هناك رقائق FRAM. من وجهة نظر البرمجة ، هذا هو نفس I2C EEPROM ، ولكن مع عدد كبير جدًا من دورات إعادة الكتابة (مئات الملايين). هذا فقط حتى يصل كل شيء إلى المتجر بمثل هذه الرقائق بأي شكل من الأشكال ، لذلك في الوقت الحالي سيقف 24LC512 المعتاد.

لوحة الدوائر


في البداية ، خططت لرسم رسم في المنزل. لذلك ، تم تصميم اللوحة كجانب واحد. ولكن بعد ساعة طويلة بمكواة الليزر وقناع اللحام (بدونها بطريقة أو بأخرى) ، ما زلت قررت طلب ألواح من الصينيين.



قبل طلب اللوحة تقريبًا ، أدركت أنه بالإضافة إلى شريحة الذاكرة المحمولة على ناقل I2C ، يمكنك التقاط شيء آخر مفيد ، مثل شاشة العرض. ما يجب أن يخرج إليه بالضبط ما زال سؤالًا ، لكنك تحتاج إلى التكاثر على السبورة. حسنًا ، نظرًا لأنني كنت سأطلب الألواح في المصنع ، فلا معنى لقصر نفسي على لوحة أحادية الجانب ، لذا فإن الخطوط الموجودة على I2C هي الخطوط الوحيدة على ظهر اللوحة.

ارتبطت دعامة واحدة كبيرة أيضًا بالأسلاك من جانب واحد. لأن تم رسم اللوحة على جانب واحد ، وتم التخطيط لوضع المسارات ومكونات SMD على جانب واحد ، ومكونات الإخراج والموصلات وإمدادات الطاقة من جهة أخرى. عندما استلمت اللوحات في شهر ، نسيت الخطة الأولية وفك ضغط جميع المكونات على الجانب الأمامي. وفقط عندما يتعلق الأمر بلحام مصدر الطاقة ، اتضح أن الطرح زائد وناقص تم فصلهما على العكس. اضطررت إلى المزارع الجماعية مع لاعبا. في الصورة أعلاه ، قمت بالفعل بتغيير الأسلاك ، ولكن يتم طرح الأرض من جزء من اللوحة إلى الجزء الآخر من خلال مخرجات زر التمهيد (على الرغم من أنه من الممكن رسم مسار على الطبقة الثانية).

اتضح مثل هذا



الإسكان


الخطوة التالية هي السكن. مع طابعة ثلاثية الأبعاد ، هذه ليست مشكلة. لم أزعجني كثيرًا - لقد قمت برسم صندوق بالحجم الصحيح وقمت بعمل قواطع في الأماكن المناسبة. يتم إرفاق الغطاء بالجسم على مسامير صغيرة.



لقد ذكرت بالفعل أنه يمكن استخدام زر التمهيد كزر للأغراض العامة - هنا نأتي به إلى اللوحة الأمامية. للقيام بذلك ، وجهت "بئر" خاص حيث يوجد الزر.



يوجد داخل العلبة أيضًا جذوع يتم تثبيت اللوحة عليها وتثبيتها بمسمار M3 واحد (لم يعد هناك مساحة على اللوحة)

تم اختيار الشاشة عندما طبعت أول نسخة مناسبة من العلبة. لم يتناسب الخطان القياسيان في هذه الحالة ، ولكن تم العثور على شاشة OLED SSD1306 128x32 في الانحراف. إنه صغير ، لكني لست مضطرًا لأن ألقي نظرة عليه كل يوم - ستسير.

لتقدير كل من الطريقة وكيف سيتم وضع الأسلاك منه ، قررت التمسك بالشاشة في منتصف العلبة. بيئة العمل ، بالطبع ، تحت اللوح - زر في الأعلى ، شاشة في الأسفل. لكنني قلت بالفعل أن فكرة شد الشاشة جاءت متأخرة جدًا وكان من الكسل جدًا إعادة ترتيب اللوحة لتحريك الزر.

الجهاز مكتمل. يتم لصق وحدة العرض على فوهات تذوب الساخنة





يمكن رؤية النتيجة النهائية على KDPV

البرامج الثابتة


دعنا ننتقل إلى جزء البرنامج. بالنسبة لمثل هذه الحرف الصغيرة ، أحب حقًا استخدام لغة Python ( micropython ) - الرمز مضغوط للغاية ومفهوم. لحسن الحظ ، ليست هناك حاجة للذهاب إلى مستوى السجلات من أجل الضغط على الميكروثانية - كل شيء يمكن القيام به من الثعبان.

يبدو أن الأمر كله بسيط ، ولكن ليس للغاية - يتم تحديد العديد من الوظائف المستقلة في الجهاز:

  • يضغط المستخدم على زر وينظر إلى الشاشة
  • وضع علامة لتر وتحديث القيم في ذاكرة فلاش
  • تقوم الوحدة بمراقبة إشارة WiFi وإعادة الاتصال إذا لزم الأمر
  • حسنًا ، بدون ضوء وامض ، لا يمكنك فعل ذلك على الإطلاق

من المستحيل افتراض أن إحدى الوظائف لم تنجح إذا كانت الأخرى غبية لسبب ما. لقد أكلت بالفعل الصبار في مشاريع أخرى ، والآن أرى خللًا في أسلوب "فاته لتر آخر ، لأنه في هذه اللحظة تم تحديث الشاشة" أو "لا يمكن للمستخدم القيام بأي شيء أثناء اتصال الوحدة بشبكة WiFi". بالطبع ، يمكن القيام ببعض الأشياء من خلال المقاطعات ، ولكن يمكنك أن تواجه قيودًا على المدة أو تعشيش المكالمات أو التغيير غير الذري للمتغيرات. حسنًا ، الشفرة التي تفعل كل شيء وتتحول فورًا إلى فوضى.

في المشروع ، استخدمت تعدد المهام الوقائي الكلاسيكي و FreeRTOS بشكل أكثر جدية ، ولكن في هذه الحالة تبين أن نموذج Coroutines ومكتبة uasync أكثر ملاءمة. علاوة على ذلك ، فإن تنفيذ Pitonovskiy للكوروتين هو مجرد قنبلة - بالنسبة للمبرمج ، تم عمل كل شيء ببساطة وسهولة. فقط اكتب منطقك الخاص ، فقط أخبرني في الأماكن التي يمكنك التبديل فيها بين المواضيع.

أقترح استكشاف الاختلافات بين التزاحم وتعدد المهام التنافسية بشكل اختياري. الآن ، دعنا ننتقل أخيرًا إلى الرمز.

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

تتم معالجة كل عداد بمثيل من فئة العداد. بادئ ذي بدء ، يتم طرح القيمة الأولية للعداد من EEPROM (value_storage) - هذه هي الطريقة التي يتم بها تنفيذ الاسترداد من انقطاع التيار الكهربائي.

يتم تهيئة الدبوس بسحب مدمج إلى الطاقة: إذا تم إغلاق مفتاح القصب ، فإنه يكون صفرًا على الخط ، إذا كان الخط مفتوحًا ، يتم سحب الخط إلى الطاقة وقراءة وحدة التحكم واحدة.

أيضا ، تم إطلاق مهمة منفصلة هنا ، والتي ستستطلع الدبوس. سيقوم كل عداد بتشغيل مهمته الخاصة. ها هي رمزها

  """ 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 مللي ثانية ، وتتحقق من الدبوس وإذا كانت جهات اتصال مفتاح القصب مغلقة ، فإن لترًا آخر يمر عبر العداد وهذا يحتاج إلى المعالجة.

  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 

حسنًا ، سنستغل الآن ملذات الثعبان ومكتبة uasync ونجعل الكائن المضاد قابلًا للانتظار (كيف يمكن ترجمته إلى اللغة الروسية؟ ما يمكن توقعه؟)

  def __await__(self): while not self._value_changed: yield from asyncio.sleep(0) return self.value() __iter__ = __await__ 

هذه وظيفة مناسبة تنتظر حتى يتم تحديث قيمة العداد - تستيقظ الوظيفة من وقت لآخر وتتحقق من علامة _value_changed. نكتة هذه الوظيفة هي أن رمز الاتصال يمكن أن ينام على استدعاء لهذه الوظيفة والنوم حتى يتم تلقي قيمة جديدة.

ولكن ماذا عن المقاطعات؟
نعم ، في هذا المكان ، يمكنك التصيد بي ، قائلاً إنه هو نفسه قال عن الانقطاعات ، ولكنه في الواقع رتب استطلاعًا غبيًا للدبوس. في الواقع ، المقاطعات هي أول شيء جربته. في ESP8266 ، يمكنك تنظيم مقاطعة على الحافة ، وحتى كتابة معالج لهذه المقاطعة في الثعبان. في هذه المقاطعة ، يمكنك تحديث قيمة متغير. على الأرجح ، سيكون هذا كافيًا إذا كان العداد جهازًا تابعًا - جهاز ينتظر حتى يُطلب منه هذه القيمة.

لسوء الحظ (أو لحسن الحظ؟) جهازي نشط ، يجب عليه إرسال رسائل باستخدام بروتوكول MQTT وكتابة البيانات إلى EEPROM. وهنا تأتي القيود بالفعل - لا يمكنك تخصيص ذاكرة واستخدام مجموعة كبيرة في المقاطعات ، مما يعني أنه يمكنك نسيان إرسال الرسائل عبر الشبكة. هناك كعكات مثل 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) 

في بايثون ، من الصعب العمل مع وحدات البايت مباشرةً ، ولكن وحدات البايت المكتوبة على الذاكرة. اضطررت إلى إصلاح التحويل بين العدد الصحيح والبايت باستخدام مكتبة 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.txt. هذا يسمح لك بتغيير الرمز بغض النظر عن الإعدادات ، وكذلك تثبيت العديد من الأجهزة المتطابقة مع معلمات مختلفة.

تقوم الكتلة الأخيرة من التعليمات البرمجية بتشغيل العديد من coroutines لخدمة وظائف مختلفة للنظام. فيما يلي مثال على coroutine الذي يخدم العدادات

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

في الدورة ، ينتظر Corutin قيمة عداد جديدة وبمجرد ظهورها ، يرسل رسالة باستخدام بروتوكول 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) 

الوظيفة تلعق بصدق من الأمثلة. في هذه الحالة ، يعتبر عدد عمليات قطع الاتصال (internet_outages) ومدتها. عند استعادة الاتصال ، يتم إرسال وقت التوقف إلى الخادم.

بالمناسبة ، هناك حاجة إلى النوم الأخير فقط حتى تصبح الوظيفة غير متزامنة - في المكتبة يتم استدعاؤها عبر الانتظار ، ويمكن فقط استدعاء الوظائف الموجودة في الجسم التي يوجد بها انتظار آخر.

بالإضافة إلى الاتصال بشبكة 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)) 

الكثير من النص ، لكننا لم نومض ضوء LED. هنا

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

لقد قدمت وضعي وميض. إذا فُقد الاتصال (أو يتم إنشاؤه للتو) ، فسيومض الجهاز بسرعة. إذا تم تأسيس الاتصال ، يومض الجهاز مرة كل 5 ثوانٍ. إذا لزم الأمر ، يمكنك هنا تنفيذ أوضاع وامضة أخرى.

لكن LED مدللة للغاية. ما زلنا نلوح على الشاشة.

  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) 

هذا ما تحدثت عنه - كم هو بسيط ومريح مع coroutines. تصف هذه الوظيفة الصغيرة تفاعل المستخدم بالكامل. ينتظر Corutin فقط زرًا للضغط وتشغيل الشاشة لمدة 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. علاوة على ذلك ، كلما حاولت أن أفهم بالضبط أين كانت الذاكرة تتسرب ، كلما قمت بترتيب تصحيح المطبوعات ، حدث هذا الخطأ مبكرًا. قادني غوغيليز قصير إلى فهم أنه ، من حيث المبدأ ، لا تحتوي وحدة التحكم الدقيقة إلا على 30 كيلوبايت من الذاكرة التي لا يتناسب فيها 65 كيلوبايت من الكود (مع المكتبات) بأي شكل من الأشكال.

لكن هناك طريق اتضح أن ميكروبيثون لا يقوم بتنفيذ التعليمات البرمجية مباشرة من ملف .py - يتم تجميع هذا الملف أولاً. ويتم تجميعه مباشرة على وحدة التحكم الدقيقة ، ويتحول إلى رمز ثانوي ، يتم تخزينه بعد ذلك في الذاكرة. حسنًا ، لكي يعمل المترجم ، تحتاج أيضًا إلى قدر معين من ذاكرة الوصول العشوائي.

الحيلة هي إنقاذ الميكروكونترولر من التجميع المكثف للموارد. يمكنك تجميع الملفات على جهاز كمبيوتر كبير ، وتعبئة الرمز البايت النهائي في وحدة التحكم الدقيقة. للقيام بذلك ، قم بتنزيل البرنامج الثابت micropython وقم بإنشاء الأداة المساعدة mpy-cross .

لم أكتب ملف Makefile ، لكنني مررت يدويًا وجمعت جميع الملفات الضرورية (بما في ذلك المكتبات) مثل هذا

 mpy-cross water_counter.py 

يبقى فقط لتحميل الملفات ذات الامتداد .mpy ، مع عدم نسيان حذف .py المطابق أولاً من نظام الملفات الخاص بالجهاز.

أجريت كل التطوير في البرنامج (IDE؟) ESPlorer. يسمح لك بتحميل البرامج النصية إلى وحدة التحكم الدقيقة وتنفيذها على الفور. في حالتي ، يقع كل منطق وإنشاء جميع الكائنات في ملف water_counter.py (.mpy). ولكن لكي يبدأ كل هذا تلقائيًا ، في البداية يجب أن يكون هناك ملف آخر يسمى main.py. ويجب أن يكون بالضبط .py ، وليس pmp. هنا هو محتواه التافه

 import water_counter 

نبدأ - كل شيء يعمل. ولكن هناك القليل من الذاكرة الخالية بشكل خطير - حوالي 1 كيلوبايت. لا يزال لدي خطط لتوسيع وظائف الجهاز ، ومن الواضح أن هذا الكيلوبايت لن يكون كافياً بالنسبة لي. ولكن اتضح أن هناك مخرجًا لهذه الحالة.

هذا هو الشيء. على الرغم من أن الملفات يتم تجميعها في كود ثانوي وتقع على نظام الملفات الداخلي ، إلا أنها في الواقع لا تزال يتم تحميلها في ذاكرة الوصول العشوائي وتنفيذها من هناك. ولكن اتضح أن ميكروبيثون قادر على تنفيذ البايت كود مباشرة من ذاكرة فلاش ، ولكن لهذا تحتاج إلى تضمينه مباشرة في البرامج الثابتة. هذا ليس صعبًا ، على الرغم من أنه استغرق قدرًا كبيرًا من الوقت على نتبووك (هناك فقط حصلت على لينكس).

الخوارزمية هي كما يلي:

  • قم بتنزيل ESP Open SDK وتثبيته. يقوم هذا الشيء ببناء مترجم ومكتبات للبرامج تحت ESP8266. يتم تجميعه وفقًا للتعليمات الواردة في الصفحة الرئيسية للمشروع (اخترت الإعداد STANDALONE = نعم)
  • تنزيل أنواع Micropython
  • قم بإسقاط المكتبات الضرورية في المنافذ / esp8266 / الوحدات داخل شجرة الميكروبيثون
  • نقوم بتجميع البرامج الثابتة وفقًا للتعليمات الواردة في ملف المنافذ / esp8266 / README.md
  • قم بصب البرامج الثابتة في وحدة التحكم الدقيقة (أقوم بهذا على Windows باستخدام برامج ESP8266Flasher أو Python esptool)

هذا كل شيء ، الآن ستقوم عملية الاستيراد ssd1306 برفع الشفرة مباشرة من البرامج الثابتة ولن يتم إنفاق ذاكرة الوصول العشوائي لهذا الغرض. بهذه الخدعة ، قمت بتنزيل رمز المكتبة فقط في البرنامج الثابت ، بينما يتم تنفيذ رمز البرنامج الرئيسي من نظام الملفات. هذا يسمح لك بتعديل البرنامج بسهولة دون إعادة تجميع البرامج الثابتة. في الوقت الحالي لدي حوالي 8.5 كيلو بايت من ذاكرة الوصول العشوائي. سيسمح هذا بتنفيذ الكثير من الوظائف المفيدة المختلفة في المستقبل. حسنًا ، إذا لم تكن هناك ذاكرة كافية على الإطلاق ، فيمكنك أيضًا دفع البرنامج الرئيسي إلى البرامج الثابتة.

وماذا تفعل به الآن؟


حسنًا ، الجهاز ملحوم ، البرنامج الثابت مكتوب ، الصندوق مطبوع ، الجهاز عالق في الحائط ويومض بسعادة بمصباح كهربائي. ولكن في حين أن هذا كله عبارة عن صندوق أسود (بالمعنى الحرفي والمجازي) وما زال معناه غير كافٍ. حان الوقت للقيام بشيء مع رسائل MQTT التي يتم إرسالها إلى الخادم.

بيتي الذكي يدور حول نظام ماجوردومو . وحدة MQTT إما خارج الصندوق ، أو يمكن تثبيتها بسهولة من سوق الإضافات - لم أعد أتذكر مصدرها. الشيء MQTT ليس الاكتفاء الذاتي - ما يسمى ب وسيط - خادم يقوم باستلام وفرز وإعادة توجيه رسائل MQTT إلى العملاء. أستخدم البعوض ، الذي (مثل ماجوردومو) يعمل على نفس النت بوك.

بعد أن يرسل الجهاز رسالة مرة واحدة على الأقل ، ستظهر القيمة على الفور في القائمة.



يمكن الآن ربط هذه القيم بكائنات النظام ، ويمكن استخدامها في البرامج النصية للأتمتة وإخضاعها لتحليل مختلف - كل هذا خارج نطاق هذه المقالة. من يهتم بنظام ماجوردومو ، يمكنني أن أوصي بقناة Electronics In Lens - يقوم صديق أيضًا ببناء منزل ذكي ويتحدث بشكل ذكي عن إعداد النظام.

سوف أعرض فقط بعض الرسوم البيانية. هذا رسم بياني بسيط للقيم اليومية.


يمكن ملاحظة أنه لم يستخدم أحد الماء تقريبًا في الليل. بضع مرات ذهب شخص ما إلى المرحاض ، ويبدو أن مرشح التناضح العكسي يمتص بضع لترات في الليلة. في الصباح ، يزيد الاستهلاك بشكل ملحوظ. عادةً ما أستخدم الماء من غلاية ، ولكن بعد ذلك أردت أن أستحم وانتقل مؤقتًا إلى الماء الساخن في المدينة - وهذا أيضًا مرئي بوضوح على الرسم البياني السفلي.

من هذا الجدول ، علمت أن الذهاب إلى المرحاض هو 6-7 لترات من الماء ، والاستحمام - 20-30 لترًا ، وغسل الأطباق حوالي 20 لترًا ، وللحمام ، تحتاج إلى 160 لترًا. ليوم واحد ، تستهلك عائلتي حوالي 500-600 لتر.

بالنسبة إلى الأكثر فضولًا ، يمكنك إلقاء نظرة على الإدخالات لكل قيمة فردية



من هنا علمت أنه مع فتح الصنبور ، يتدفق الماء بسرعة حوالي 1 لتر في 5 ثوانٍ.

ولكن في هذا الشكل ، ربما لا تكون الإحصائيات ملائمة للمشاهدة. في majordomo لا تزال هناك فرصة لرؤية مخططات الاستهلاك حسب اليوم والأسبوع والشهر. على سبيل المثال ، رسم بياني للاستهلاك في الأعمدة



حتى الآن لدي بيانات لمدة أسبوع فقط. في غضون شهر ، سيكون هذا الرسم البياني أكثر دلالة - سيتوافق عمود منفصل مع كل يوم. التعديلات التي أدخلتها على القيم التي تفسدها يدوياً تفسد الصورة قليلاً (العمود الأكبر). وليس من الواضح حتى الآن ما إذا كنت قد حددت القيم الأولى بشكل غير صحيح تقريبًا مكعبًا أقل تقريبًا ، أو ما إذا كان هذا خطأ في البرنامج الثابت ولم تذهب جميع اللترات إلى المقاصة. يستغرق المزيد من الوقت.

لا يزال من الضروري استحضار الرسوم البيانية نفسها ، تبييض اللون. ربما سأبني أيضًا رسمًا بيانيًا لاستهلاك الذاكرة لأغراض التصحيح - فجأة حدث تسرب لشيء ما. ربما سأعرض فترات بطريقة ما عندما لم يكن الإنترنت متاحًا. في حين أن كل هذا يدور على مستوى الأفكار.

الخلاصة


اليوم ، أصبحت شقتي أكثر ذكاءً قليلاً. مع هذا الجهاز الصغير ، سيكون من الأنسب بالنسبة لي مراقبة استهلاك المياه في المنزل. إذا كنت في وقت سابق ساخطًا "مرة أخرى استهلكوا الكثير من الماء في شهر واحد" ، يمكنني الآن العثور على مصدر هذا الاستهلاك.

قد يبدو غريباً أن يشاهد أحد القراءات على الشاشة إذا كان على بعد متر من جهاز القياس نفسه. ولكن في المستقبل غير البعيد ، أخطط للانتقال إلى شقة أخرى ، حيث سيكون هناك العديد من المصاعد ، ومن المرجح أن تكون العدادات نفسها عند الهبوط. لذا سيكون جهاز القراءة عن بعد مفيدًا جدًا.

أخطط أيضًا لتوسيع وظائف الجهاز. أنا بالفعل أنظر إلى الصمامات الآلية. الآن لتحويل مياه مدينة الغلاية ، أحتاج إلى تحويل 3 صنابير في مكان لا يمكن الوصول إليه. سيكون أكثر ملاءمة للقيام بذلك مع زر واحد مع الإشارة المناسبة. حسنًا ، بالطبع ، من المفيد تنفيذ الحماية ضد التسريبات.

في المقالة أخبرت إصداري من الجهاز بناءً على ESP8266. في رأيي ، حصلت على نسخة مثيرة للاهتمام للغاية من البرامج الثابتة micropython باستخدام coroutine - بسيطة وجميلة.حاولت أن أصف العديد من الفروق الدقيقة والمدارس التي واجهتها مع الحملة. ربما وصفت كل شيء بتفاصيل أكثر من اللازم ، بالنسبة لي شخصيًا ، كقارئ ، من السهل تبديد الكثير من الوقت للتفكير في ما لم يُقال.

كالعادة ، أنا منفتح على النقد البناء. دائرة

رمز المصدر ونموذج حالة اللوحة

Source: https://habr.com/ru/post/ar411259/


All Articles