Nous connectons un compteur d'eau à une maison intelligente

Une fois que les systèmes domotiques, ou comme ils sont souvent appelés «maison intelligente», étaient terriblement chers et seuls les riches pouvaient se les permettre. Aujourd'hui, sur le marché, vous pouvez trouver des kits assez bon marché avec des capteurs, des boutons / interrupteurs et des actionneurs pour contrôler l'éclairage, les prises, la ventilation, l'alimentation en eau et d'autres consommateurs. Et même le plus krivoruky DIY-shnik peut rejoindre les beaux et collecter des appareils pour une maison intelligente à peu de frais.



En règle générale, les dispositifs proposés sont soit des capteurs soit des actionneurs. Ils facilitent la mise en œuvre de scénarios tels que «allumer la lumière lorsque le détecteur de mouvement est déclenché» ou «l'interrupteur à la sortie éteint la lumière dans tout l'appartement». Mais la télémétrie n'a pas fonctionné. Au mieux, il s'agit d'un graphique de la température et de l'humidité, ou de la puissance instantanée dans une prise particulière.

Récemment, j'ai installé un compteur d'eau avec une sortie d'impulsion. Un interrupteur à lames est déclenché à travers chaque litre du compteur et ferme le contact. La seule chose qui reste est de s'accrocher aux fils et d'essayer d'en tirer profit. Par exemple, analysez la consommation d'eau par heure et jour de la semaine. Eh bien, s'il y a plusieurs élévateurs d'eau dans l'appartement, il est plus pratique de voir tous les indicateurs actuels sur un seul écran que de gravir des niches difficiles d'accès avec une lampe de poche.

Sous la coupe, ma version de l'appareil est basée sur l'ESP8266, qui compte les impulsions des compteurs d'eau et envoie des lectures au serveur domestique intelligent via MQTT. Nous allons programmer en micropython en utilisant la bibliothèque uasyncio. Lors de la création du firmware, j'ai rencontré plusieurs difficultés intéressantes, dont je parlerai également dans cet article. C'est parti!

Schéma




Le cœur de l'ensemble du circuit est le module du microcontrôleur ESP8266. L'ESP-12 était initialement prévu, mais le mien s'est révélé défectueux. Je devais me contenter du module ESP-07, qui était disponible. Heureusement, ils sont les mêmes à la fois dans les conclusions et dans les fonctionnalités, la différence ne concerne que l'antenne - l'ESP-12 l'a intégrée et l'ESP-07 en a une externe. Cependant, même sans antenne WiFi, le signal dans ma salle de bain est capté normalement.

La liaison du module est standard:

  • bouton de réinitialisation avec une bretelle et un condensateur (bien que les deux soient déjà à l'intérieur du module)
  • Activer le signal (CH_PD) tiré à l'alimentation
  • GPIO15 s'est arrêté au sol. Cela n'est nécessaire qu'au début, mais je n'ai plus rien à m'accrocher à cette jambe

Pour mettre le module en mode firmware, vous devez fermer le GPIO2 à la terre, et pour le rendre plus pratique, j'ai fourni le bouton Boot. Dans des conditions normales, cette broche est tirée au courant.

L'état de la ligne GPIO2 n'est vérifié qu'au début du travail - lorsque l'alimentation est appliquée ou immédiatement après une réinitialisation. Le module se charge donc comme d'habitude ou passe en mode firmware. Après le chargement, cette sortie peut être utilisée comme un GPIO standard. Eh bien, puisqu'il y a déjà un bouton, vous pouvez y mettre une fonction utile.

Pour la programmation et le débogage, je vais utiliser l'UART, qui a amené au peigne. Si nécessaire - je branche simplement un adaptateur USB-UART. Vous avez juste besoin de vous rappeler que le module est alimenté par 3,3 V. Si vous oubliez de commuter l'adaptateur à cette tension et appliquez 5V, le module sera très probablement grillé.

Je n'ai aucun problème avec l'électricité dans la salle de bain - la prise est située à environ un mètre des compteurs, donc je vais l'alimenter à partir de 220V. En tant que source d'alimentation, je travaillerai un petit bloc de HLK- PM03 de Tenstar Robot. Personnellement, je suis proche de l'électronique analogique et de puissance, mais voici une alimentation finie dans un petit boîtier.

Pour signaler les modes de fonctionnement, j'ai fourni une LED connectée à GPIO2. Cependant, je n'ai pas commencé à le souder, car le module ESP-07 possède déjà une LED, par ailleurs, connectée au même GPIO2. Mais que ce soit sur le tableau - tout d'un coup, je veux apporter cette LED au boîtier.

Nous passons au plus intéressant. Les compteurs d'eau n'ont pas de logique, on ne peut pas leur demander les lectures actuelles. La seule chose à notre disposition est les impulsions - fermant les contacts du commutateur à lames à chaque litre. Les conclusions des interrupteurs reed sont définies dans GPIO12 / GPIO13. Je vais activer la résistance de pull-up par programmation à l'intérieur du module.

Au départ, j'ai oublié de fournir des résistances R8 et R9, et dans ma version de la carte, elles ne le sont pas. Mais étant donné que je mets déjà le système à la disposition du public, cela vaut la peine de corriger cet oubli. Des résistances sont nécessaires pour ne pas brûler le port si le micrologiciel est buggé et met l'unité sur la broche, et le commutateur à lames court-circuite cette ligne à la masse (maximum 3,3 V / 1000 Ohm = 3,3 mA circuleront avec la résistance).

Il est temps de réfléchir à ce qu'il faut faire en cas de panne d'électricité. La première option consiste à demander au serveur les compteurs initiaux au début. Mais cela nécessiterait une complication importante du protocole d'échange. De plus, l'opérabilité de l'appareil dans ce cas dépend de l'état du serveur. Si, après avoir éteint la lumière, le serveur ne démarre pas (ou démarre plus tard), le compteur d'eau ne pourra pas demander les valeurs initiales et fonctionnera incorrectement.

J'ai donc décidé d'implémenter le stockage des valeurs de compteur dans une puce mémoire connectée via I2C. Je n'ai pas d'exigences particulières pour la taille de la mémoire flash - je dois enregistrer seulement 2 chiffres (le nombre de litres par compteur d'eau chaude et froide). Même le plus petit module fera l'affaire. Mais sur le nombre de cycles d'enregistrement, vous devez faire attention. Pour la plupart des modules, c'est 100 000 cycles, pour certains jusqu'à un million.

Il semblerait qu'un million, c'est beaucoup. Mais pendant 4 ans de vie dans mon appartement j'ai consommé un peu plus de 500 mètres cubes d'eau, soit 500 mille litres! Et 500 000 entrées en un clin d'œil. Et ce n'est que de l'eau froide. Vous pouvez bien sûr souder la puce tous les deux ans, mais il s'est avéré qu'il existe des puces FRAM. D'un point de vue programmation, c'est la même EEPROM I2C, mais avec un très grand nombre de cycles de réécriture (des centaines de millions). C'est juste jusqu'à ce que tout atteigne le magasin avec de telles puces de quelque manière que ce soit, donc pour l'instant le 24LC512 habituel se tiendra.

Circuit imprimé


Au départ, j'avais prévu de faire des frais à la maison. Par conséquent, la planche a été conçue comme unilatérale. Mais après une longue heure avec un fer à repasser laser et un masque de soudure (sans ça en quelque sorte comme il faut), j'ai quand même décidé de commander des planches aux chinois.



Presque avant de commander la carte, j'ai réalisé qu'en plus de la puce de mémoire flash sur le bus I2C, vous pouvez prendre quelque chose d'autre utile, comme un écran. Qu'est-ce que la sortie est toujours une question, mais vous devez vous reproduire sur la carte. Eh bien, puisque j'allais commander les cartes à l'usine, cela n'avait aucun sens de me limiter à une carte simple face, donc les lignes sur I2C sont les seules à l'arrière de la carte.

Un grand montant était également associé à un câblage unilatéral. Parce que la carte était dessinée d'un côté, les pistes et les composants SMD devaient être placés d'un côté, et les composants de sortie, les connecteurs et l'alimentation électrique de l'autre. Lorsque j'ai reçu les planches dans un mois, j'ai oublié le plan initial et dézippé tous les composants de la face avant. Et seulement quand il s'agissait de souder l'alimentation, il s'est avéré que le plus et le moins étaient divorcés au contraire. J'ai dû faire des fermes collectives avec des cavaliers. Dans l'image ci-dessus, j'ai déjà changé le câblage, mais la masse est projetée d'une partie de la carte à l'autre via les sorties du bouton Boot (bien qu'il soit possible de dessiner une piste sur la deuxième couche).

Il s'est avéré comme ça



Logement


La prochaine étape est le logement. Avec une imprimante 3D, ce n'est pas un problème. Je n'ai pas trop dérangé - j'ai juste dessiné une boîte de la bonne taille et fait des découpes aux bons endroits. Le couvercle est fixé au corps par de petites vis.



J'ai déjà mentionné que le bouton de démarrage peut être utilisé comme un bouton à usage général - ici, nous le présentons sur le panneau avant. Pour ce faire, j'ai dessiné un «puits» spécial où se trouve le bouton.



À l'intérieur du boîtier, il y a également des souches sur lesquelles la carte est installée et fixée avec une seule vis M3 (il n'y avait plus d'espace sur la carte)

L'affichage a été sélectionné lorsque j'ai imprimé la première version adaptée du boîtier. La ligne standard à deux lignes ne convenait pas dans ce cas, mais un écran OLED SSD1306 128x32 a été trouvé dans le cardan. C'est petit, mais je n'ai pas à le regarder tous les jours - ça va rouler.

Estimant les deux sens et la façon dont les fils seront posés à partir de lui, j'ai décidé de coller l'écran au milieu du boîtier. L'ergonomie, bien sûr, sous la plinthe - un bouton en haut, un écran en bas. Mais j'ai déjà dit que l'idée de visser l'écran était venue trop tard et qu'il était trop paresseux de réorganiser la carte pour déplacer le bouton.

L'appareil est terminé. Le module d'affichage est collé aux buses thermofusibles





Le résultat final peut être vu sur KDPV

Firmware


Passons à la partie logiciel. Pour ces petits métiers, j'aime vraiment utiliser le langage Python ( micropython ) - le code est très compact et compréhensible. Heureusement, il n'est pas nécessaire de descendre au niveau des registres pour presser les microsecondes - tout peut être fait à partir de python.

Il semble que tout soit simple, mais pas très - plusieurs fonctions indépendantes sont décrites dans l'appareil:

  • L'utilisateur enfonce un bouton et regarde l'écran
  • Les litres cochent et mettent à jour les valeurs dans la mémoire flash
  • Le module surveille le signal WiFi et se reconnecte si nécessaire
  • Eh bien, sans lumière clignotante, vous ne pouvez pas

Il est impossible de supposer qu'une fonction n'a pas fonctionné si l'autre est stupide pour une raison quelconque. J'ai déjà mangé des cactus dans d'autres projets et maintenant je vois des pépins dans le style de "manquer un autre litre, car à ce moment l'affichage a été mis à jour" ou "l'utilisateur ne peut rien faire pendant que le module se connecte au WiFi". Bien sûr, certaines choses peuvent être effectuées par le biais d'interruptions, mais vous pouvez rencontrer une limitation de la durée, l'imbrication des appels ou un changement non atomique de variables. Eh bien, le code qui fait tout et se transforme immédiatement en désordre.

Dans le projet, j'ai utilisé plus sérieusement le multitâche préemptif classique et FreeRTOS, mais dans ce cas, le modèle coroutines et la bibliothèque uasync se sont avérés beaucoup plus adaptés. De plus, la mise en œuvre Pitonovskiy de la corutine n'est qu'une bombe - pour le programmeur, tout est fait simplement et commodément. Écrivez simplement votre propre logique, dites-moi simplement à quels endroits vous pouvez basculer entre les threads.

Je suggère d'explorer les différences entre l'éviction et le multitâche compétitif en option. Passons maintenant au code.

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

Chaque compteur est traité par une instance de la classe Counter. Tout d'abord, la valeur initiale du compteur est soustraite de l'EEPROM (value_storage) - c'est ainsi que la récupération après une panne de courant est mise en œuvre.

La broche est initialisée avec un pull-up intégré à l'alimentation: si l'interrupteur à lames est fermé, il est à zéro sur la ligne, si la ligne est ouverte, la ligne est tirée à l'alimentation et le contrôleur en lit une.

En outre, une tâche distincte est lancée ici, qui interrogera la broche. Chaque compteur exécutera sa propre tâche. Voici son code

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

Un délai de 25 ms est nécessaire pour filtrer le rebond de contact, et en même temps, il régule la fréquence à laquelle la tâche se réveille (pendant que cette tâche est en sommeil, d'autres tâches fonctionnent). Toutes les 25 ms, la fonction se réveille, vérifie la broche et si les contacts de l'interrupteur à lames sont fermés, alors un autre litre a traversé le compteur et cela doit être traité.

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

Le traitement du litre suivant est trivial - le compteur augmente simplement. Eh bien, une nouvelle valeur serait bien d'écrire sur un lecteur flash.

Pour faciliter l'utilisation, des «accesseurs» sont fournis.

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

Eh bien, maintenant nous allons profiter des plaisirs de python et de la bibliothèque uasync et rendre le compteur d'objet attendable (comment cela peut-il être traduit en russe? Celui auquel on peut s'attendre?)

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

Il s'agit d'une fonction très pratique qui attend la mise à jour de la valeur du compteur - la fonction se réveille de temps en temps et vérifie l'indicateur _value_changed. La plaisanterie de cette fonction est que le code appelant peut s'endormir lors d'un appel à cette fonction et dormir jusqu'à ce qu'une nouvelle valeur soit reçue.

Mais qu'en est-il des interruptions?
Oui, à cet endroit, vous pouvez me troller, en disant qu'il a lui-même dit au sujet des interruptions, mais en fait, il a organisé un stupide sondage de l'épingle. En fait, les interruptions sont la première chose que j'ai essayée. Dans ESP8266, vous pouvez organiser une interruption sur le bord, et même écrire un gestionnaire pour cette interruption en python. Dans cette interruption, vous pouvez mettre à jour la valeur d'une variable. Cela suffirait probablement si le compteur était un appareil esclave - un appareil qui attend jusqu'à ce qu'on lui demande cette valeur.

Malheureusement (ou heureusement?) Mon appareil est actif, il doit envoyer des messages en utilisant le protocole MQTT et écrire des données dans l'EEPROM. Et ici, les restrictions existent déjà - vous ne pouvez pas allouer de mémoire et utiliser une grande pile d'interruptions, ce qui signifie que vous pouvez oublier d'envoyer des messages sur le réseau. Il y a des petits pains comme micropython.schedule () qui vous permettent d'exécuter une sorte de fonction "dès que tout de suite", mais la question est "quel est le point?". Tout à coup, nous envoyons une sorte de message en ce moment, et ici l'interruption coince et gâche les valeurs des variables. Ou, par exemple, une nouvelle valeur de compteur est arrivée du serveur alors que nous n'avons toujours pas noté l'ancien. En général, vous devez clôturer la synchronisation ou sortir différemment.

Et de temps en temps, RuntimeError plante: la pile de planification est pleine et qui sait pourquoi?

Avec un sondage explicite et uasync, il est dans ce cas en quelque sorte plus beau et plus fiable.

J'ai pris du travail avec EEPROM dans une petite classe

 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) 

En python, travailler directement avec des octets est difficile, mais ce sont des octets qui sont écrits en mémoire. J'ai dû corriger la conversion entre entier et octets à l'aide de la bibliothèque ustruct.

Afin de ne pas transférer l'objet I2C et l'adresse de la cellule mémoire à chaque fois, j'ai enveloppé le tout dans un petit classique pratique

 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) 

L'objet I2C lui-même est créé avec de tels paramètres

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

Nous abordons la chose la plus intéressante - la mise en œuvre de la communication avec le serveur via MQTT. Eh bien, le protocole lui-même n'a pas besoin d'être implémenté - une implémentation asynchrone prête à l'emploi a été trouvée sur Internet. Ici, nous allons l'utiliser.

Tous les plus intéressants sont rassemblés dans la classe CounterMQTTClient, qui est basée sur la bibliothèque MQTTClient. Commençons par la périphérie

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

Ici, des broches d'ampoules et de boutons sont créées et configurées, ainsi que des objets de compteurs d'eau froide et chaude.

Avec l'initialisation, tout n'est pas si trivial

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

Pour définir les paramètres de la bibliothèque mqtt_as, un grand dictionnaire de différents paramètres est utilisé - config. La plupart des paramètres par défaut nous conviennent, mais de nombreux paramètres doivent être définis explicitement. Afin de ne pas enregistrer les paramètres directement dans le code, je les stocke dans le fichier texte config.txt. Cela vous permet de changer le code indépendamment des paramètres, ainsi que de riveter plusieurs appareils identiques avec des paramètres différents.

Le dernier bloc de code exécute plusieurs coroutines pour servir diverses fonctions du système. Voici un exemple de coroutine au service des compteurs

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

Dans un cycle, Corutin attend une nouvelle valeur de compteur et dès qu'elle apparaît, envoie un message en utilisant le protocole MQTT. Le premier morceau de code envoie la valeur initiale même si l'eau ne coule pas dans le compteur.

La classe de base MQTTClient se sert d'elle-même, lance une connexion WiFi et se reconnecte lorsque la connexion est perdue. Lorsque l'état de la connexion WiFi change, la bibliothèque nous informe en appelant 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) 

La fonction est honnêtement léchée des exemples. Dans ce cas, il prend en compte le nombre de déconnexions (internet_outages) et leur durée. Lorsqu'une connexion est rétablie, le temps d'arrêt est envoyé au serveur.

Soit dit en passant, le dernier sommeil n'est nécessaire que pour que la fonction devienne asynchrone - dans la bibliothèque, elle est appelée via attendent, et seules les fonctions dans le corps dont il y a un autre attente peuvent être appelées.

En plus de vous connecter au WiFi, vous devez également établir une connexion avec le courtier MQTT (serveur). La bibliothèque le fait également, mais nous avons la possibilité de faire quelque chose d'utile lorsque la connexion est établie.

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

Ici, nous souscrivons à plusieurs messages - le serveur a désormais la possibilité de définir les valeurs de compteur actuelles en envoyant le message approprié.

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

Cette fonction traite les messages entrants, et selon le sujet (nom du message), les valeurs de l'un des compteurs sont mises à jour

Quelques fonctions d'assistance

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

Cette fonction envoie des messages si une connexion est établie. S'il n'y a pas de connexion, le message est ignoré.

Et ce n'est qu'une fonction pratique qui génère et envoie des messages de débogage.

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

Tant de texte, mais nous n'avons pas clignoté une LED. Ici

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

J'ai fourni 2 modes de clignotement. Si la connexion est perdue (ou en cours d'établissement), l'appareil clignote rapidement. Si la connexion est établie, l'appareil clignote toutes les 5 secondes. Si nécessaire, vous pouvez ici implémenter d'autres modes de clignotement.

Mais la LED est si choyante. Nous avons toujours salué l'affichage.

  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) 

C'est ce dont j'ai parlé - comment simple et pratique avec les coroutines. Cette petite fonction décrit TOUTES les interactions utilisateur. Corutin n'attend qu'un bouton pour appuyer et allume l'écran pendant 3 secondes. L'écran affiche les relevés actuels du compteur.

Il y a encore quelques petites choses. Voici la fonction qui exécute toute la batterie (re). Le cycle principal ne traite que de l'envoi de diverses informations de débogage une fois par minute. En général, je cite tel quel - en particulier un commentaire, je pense, n'est pas nécessaire

  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) 

Eh bien, quelques paramètres et constantes supplémentaires pour compléter la description

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

Ça commence comme ça

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


Quelque chose avec ma mémoire est devenu


Donc, tout le code est là. J'ai téléchargé les fichiers à l'aide de l'utilitaire ampy - il leur permet d'être téléchargés sur le lecteur flash interne (celui qui se trouve dans ESP-07 lui-même), puis accessibles depuis le programme en tant que fichiers normaux. Là, j'ai téléchargé les bibliothèques mqtt_as, uasyncio, ssd1306 et collections (utilisées dans mqtt_as) que j'utilise.

Nous commençons et ... Nous recevons MemoryError. De plus, plus j'essayais de comprendre exactement où la mémoire fuyait, plus j'organisais le débogage des impressions, plus tôt cette erreur s'était produite. Un court gugelezh m'a fait comprendre que, en principe, le microcontrôleur ne dispose que de 30 Ko de mémoire dans lesquels 65 Ko de code (avec les bibliothèques) ne correspondent en aucune façon.

Mais il y a un moyen. Il s'avère que micropython n'exécute pas le code directement à partir du fichier .py - ce fichier est d'abord compilé. Et il compile directement sur le microcontrôleur, se transforme en bytecode, qui est ensuite stocké en mémoire. Eh bien, pour que le compilateur fonctionne, vous avez également besoin d'une certaine quantité de RAM.

L'astuce consiste à sauver le microcontrôleur d'une compilation gourmande en ressources. Vous pouvez compiler des fichiers sur un grand ordinateur et remplir le bytecode fini dans le microcontrôleur. Pour ce faire, téléchargez le micrologiciel micropython et créez l'utilitaire mpy-cross .

Je n'ai pas écrit de Makefile, mais j'ai parcouru manuellement et compilé tous les fichiers nécessaires (y compris les bibliothèques) comme celui-ci

 mpy-cross water_counter.py 

Il ne reste plus qu'à télécharger des fichiers avec l'extension .mpy, sans oublier de supprimer d'abord le .py correspondant du système de fichiers de l'appareil.

J'ai réalisé tout le développement du programme (IDE?) ESPlorer. Il vous permet de télécharger des scripts sur le microcontrôleur et de les exécuter immédiatement. Dans mon cas, toute la logique et la création de tous les objets se trouvent dans le fichier water_counter.py (.mpy). Mais pour que tout cela démarre automatiquement, au début, il doit y avoir un autre fichier appelé main.py. Et ce devrait être exactement .py, et non un .mpy pré-compilé. Voici son contenu trivial

 import water_counter 

Nous commençons - tout fonctionne. Mais il y a dangereusement peu de mémoire libre - environ 1 Ko. J'ai toujours l'intention d'étendre les fonctionnalités de l'appareil, et ce kilo-octet ne sera évidemment pas suffisant pour moi. Mais il s'est avéré qu'il y avait une issue à cette affaire.

Voici le truc. Même si les fichiers sont compilés en bytecode et se trouvent sur le système de fichiers interne, en fait, ils sont toujours chargés dans la RAM et exécutés à partir de là. Mais il s'avère que le micropython est capable d'exécuter le bytecode directement à partir de la mémoire flash, mais pour cela, vous devez l'intégrer directement dans le firmware. Ce n'est pas difficile, même si cela a pris un temps décent sur mon netbook (seulement là, j'ai eu Linux).

L'algorithme est le suivant:

  • Téléchargez et installez ESP Open SDK . Cette chose construit un compilateur et des bibliothèques pour les programmes sous ESP8266. Il est assemblé selon les instructions de la page principale du projet (j'ai choisi le réglage STANDALONE = oui)
  • Télécharger Micropython Sorts
  • Déposez les bibliothèques nécessaires dans les ports / esp8266 / modules à l'intérieur de l'arborescence de micropython
  • Nous assemblons le firmware selon les instructions du fichier ports / esp8266 / README.md
  • Versez le firmware dans le microcontrôleur (je le fais sur Windows avec les programmes ESP8266Flasher ou Python esptool)

Voilà, maintenant 'import ssd1306' augmentera le code directement depuis le firmware et la RAM ne sera pas dépensée pour cela. Avec cette astuce, j'ai téléchargé uniquement le code de la bibliothèque dans le firmware, tandis que le code du programme principal est exécuté à partir du système de fichiers. Cela vous permet de modifier facilement le programme sans recompiler le firmware. Pour le moment, j'ai environ 8,5 Ko de RAM libre. Cela permettra de mettre en œuvre un grand nombre de fonctionnalités utiles différentes à l'avenir. Eh bien, s'il n'y a pas assez de mémoire, vous pouvez également pousser le programme principal dans le firmware.

Et qu'en faire maintenant?


Ok, le matériel est soudé, le firmware est écrit, la boîte est imprimée, l'appareil est collé au mur et clignote joyeusement avec une ampoule. Mais alors que tout cela est une boîte noire (au sens littéral et figuré) et le sens de celui-ci n'est toujours pas suffisant. Il est temps de faire quelque chose avec les messages MQTT qui sont envoyés au serveur.

Ma «maison intelligente» tourne sur le système Majordomo . Le module MQTT est soit prêt à l'emploi, soit il peut être facilement installé à partir du marché des modules complémentaires - je ne me souviens plus d'où il vient. Chose MQTT n'est pas autosuffisant - le soi-disant broker - un serveur qui reçoit, trie et redirige les messages MQTT vers les clients. J'utilise moustique, qui (comme majordomo) s'exécute tous sur le même netbook.

Une fois que l'appareil a envoyé un message au moins une fois, la valeur apparaît immédiatement dans la liste.



Ces valeurs peuvent désormais être associées à des objets système, elles peuvent être utilisées dans des scripts d'automatisation et soumises à diverses analyses - tout cela est hors de portée de cet article. Qui s'intéresse au système majordomo, je peux recommander le canal Electronics In Lens - un ami construit également une maison intelligente et parle intelligemment de la configuration du système.

Je ne montrerai que quelques graphiques. Il s'agit d'un simple graphique de valeurs quotidiennes.


On peut voir que presque personne n'utilisait d'eau la nuit. Quelques fois, quelqu'un est allé aux toilettes, et il semble qu'un filtre à osmose inverse suce quelques litres par nuit. Le matin, la consommation augmente considérablement. Habituellement, j'utilise l'eau d'une chaudière, mais je voulais ensuite prendre un bain et passer temporairement à l'eau chaude de la ville - cela est également clairement visible sur le graphique du bas.

Grâce à ce programme, j'ai appris qu'aller aux toilettes, c'est 6-7 litres d'eau, prendre une douche - 20-30 litres, laver la vaisselle environ 20 litres, et pour prendre un bain, vous avez besoin de 160 litres. Pendant une journée, ma famille consomme quelque part entre 500 et 600 litres.

Pour les plus curieux, vous pouvez consulter les entrées pour chaque valeur individuelle



De là, j'ai appris qu'avec le robinet ouvert, l'eau coule à une vitesse d'environ 1 litre en 5 secondes.

Mais sous cette forme, les statistiques ne sont probablement pas très pratiques à regarder. À majordomo, il est toujours possible de voir les graphiques de consommation par jour, semaine et mois. Par exemple, un graphique de la consommation en colonnes



Jusqu'à présent, je n'ai que des données pour une semaine. Dans un mois, ce graphique sera plus indicatif - une colonne séparée correspondra à chaque jour. Les ajustements des valeurs que j'entre gâchent manuellement l'image un peu (la plus grande colonne). Et il n'est pas encore clair si j'ai mal réglé les premières valeurs de presque un cube de moins, ou si c'est un bug dans le firmware et que tous les litres ne sont pas entrés dans la compensation. Cela prend plus de temps.

Il faut encore évoquer les graphiques eux-mêmes, blanchir, colorer. Peut-être que je vais également construire un graphique de la consommation de mémoire à des fins de débogage - soudain, quelque chose fuit. Peut-être que je vais en quelque sorte afficher les périodes où Internet n'était pas disponible. Alors que tout cela tourne au niveau des idées.

Conclusion


Aujourd'hui, mon appartement est devenu un peu plus intelligent. Avec un si petit appareil, il sera plus pratique pour moi de surveiller la consommation d'eau dans la maison. Si plus tôt j'étais indigné "encore une fois ils ont consommé beaucoup d'eau en un mois", maintenant je peux trouver la source de cette consommation.

Il semblerait étrange à quelqu'un de regarder les lectures sur l'écran s'il est à un mètre du compteur lui-même. Mais dans un avenir pas trop lointain, je prévois de déménager dans un autre appartement, où il y aura plusieurs colonnes montantes, et les compteurs eux-mêmes seront très probablement situés sur le palier. Un appareil de lecture à distance serait donc très utile.

Je prévois également d'étendre les fonctionnalités de l'appareil. Je regarde déjà les vannes motorisées. Maintenant, pour changer l'eau de la chaudière, je dois ouvrir 3 robinets dans une niche inaccessible. Il serait beaucoup plus pratique de le faire avec un seul bouton avec l'indication appropriée. Eh bien, bien sûr, il vaut la peine de mettre en œuvre une protection contre les fuites.

Dans l'article, j'ai expliqué ma version de l'appareil basé sur ESP8266. À mon avis, j'ai obtenu une version très intéressante du microprogramme micropython utilisant coroutine - simple et jolie.J'ai essayé de décrire les nombreuses nuances et écoles que j'ai rencontrées avec la campagne. J'ai peut-être tout décrit avec trop de détails, pour moi personnellement, en tant que lecteur, il est plus facile de gaspiller trop que de penser ce qui n'a pas été dit.

Comme toujours, je suis ouvert à des critiques constructives. Circuit de

code source
et
modèle de boîtier de carte

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


All Articles