Nous connectons le compteur d'eau à la maison intelligente

Autrefois, les systèmes domotiques, ou « maison intelligente », comme on les appelait souvent, étaient terriblement chers et seuls les riches pouvaient se les permettre. Aujourd'hui, sur le marché, vous pouvez trouver des kits relativement bon marché comprenant 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 bricoleur le plus véreux peut se lancer dans la beauté et assembler des appareils pour une maison intelligente à un prix avantageux.

Nous connectons le compteur d'eau à la maison intelligente

Typiquement, les dispositifs proposés sont soit des capteurs, soit des actionneurs. Ils facilitent la mise en œuvre de scénarios tels que « lorsqu'un détecteur de mouvement se déclenche, allumez les lumières » ou « l'interrupteur près de la sortie éteint les lumières dans tout l'appartement ». Mais d’une manière ou d’une autre, les choses n’ont pas fonctionné avec la télémétrie. Au mieux, il s'agit d'un graphique de la température et de l'humidité, ou de la puissance instantanée sur une prise spécifique.

J'ai récemment installé des compteurs d'eau avec sortie impulsionnelle. Pour chaque litre qui passe dans le compteur, l'interrupteur à lames s'active et ferme le contact. Il ne reste plus qu'à s'accrocher aux fils et 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 colonnes montantes d'eau dans l'appartement, il est alors plus pratique de voir tous les indicateurs actuels sur un seul écran que de grimper dans des niches difficiles d'accès avec une lampe de poche.

Sous la coupe se trouve ma version d'un appareil basé sur ESP8266, qui compte les impulsions des compteurs d'eau et envoie les relevés via MQTT au serveur de la maison intelligente. Nous programmerons 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. Aller!

Conduire

Nous connectons le compteur d'eau à la maison intelligente

Le cœur de l'ensemble du circuit est un module sur le microcontrôleur ESP8266. L'ESP-12 était initialement prévu, mais le mien s'est avéré défectueux. Il a fallu se contenter du module ESP-07, qui était disponible. Heureusement, ils sont les mêmes en termes de broches et de fonctionnalités, la seule différence réside dans l'antenne - l'ESP-12 en a une intégrée, tandis que l'ESP-07 en a une externe. Cependant, même sans antenne WiFi, le signal dans ma salle de bain est reçu normalement.

Câblage des modules standards :

  • bouton de réinitialisation avec pull-up et condensateur (bien que les deux soient déjà à l'intérieur du module)
  • Le signal d'activation (CH_PD) est mis sous tension
  • GPIO15 est tiré au sol. Ce n'est nécessaire qu'au début, mais je n'ai toujours rien à attacher à cette jambe, je n'en ai plus besoin

Pour mettre le module en mode firmware, vous devez court-circuiter GPIO2 à la masse, et pour le rendre plus pratique, j'ai fourni un bouton de démarrage. Dans des conditions normales, cette broche est mise sous tension.

L'état de la ligne GPIO2 n'est vérifié qu'au début du fonctionnement - à la mise sous tension ou immédiatement après une réinitialisation. Ainsi, le module démarre comme d'habitude ou passe en mode firmware. Une fois chargée, cette broche peut être utilisée comme un GPIO classique. Eh bien, comme il y a déjà un bouton, vous pouvez y attacher une fonction utile.

Pour la programmation et le débogage, j'utiliserai l'UART, qui est envoyé vers un peigne. Lorsque cela est nécessaire, j'y connecte simplement un adaptateur USB-UART. N'oubliez pas que le module est alimenté en 3.3V. Si vous oubliez de commuter l'adaptateur sur cette tension et de fournir 5 V, le module grillera très probablement.

Je n'ai aucun problème d'électricité dans la salle de bain - la prise est située à environ un mètre des compteurs, je serai donc alimenté en 220V. Comme source d'alimentation, j'aurai un petit bloc HLK-PM03 par Tenstar Robot. Personnellement, j'ai du mal avec l'électronique analogique et de puissance, mais voici une alimentation prête à l'emploi dans un petit boîtier.

Pour signaler les modes de fonctionnement, j'ai fourni une LED connectée au GPIO2. Cependant, je ne l'ai pas dessoudé, car... Le module ESP-07 possède déjà une LED et il est également connecté au GPIO2. Mais laissez-le être sur la carte, au cas où je voudrais afficher cette LED sur le boîtier.

Passons à la partie la plus intéressante. Les compteurs d'eau n'ont pas de logique, vous ne pouvez pas leur demander les relevés actuels. La seule chose dont nous disposons, ce sont des impulsions - fermer les contacts de l'interrupteur à lames tous les litre. Mes sorties de commutateur Reed sont connectées au GPIO12/GPIO13. J'activerai la résistance de rappel par programme à l'intérieur du module.

Au départ, j'avais oublié de fournir les résistances R8 et R9 et ma version de la carte n'en possède pas. Mais comme je publie déjà le schéma à la vue de tous, cela vaut la peine de corriger cet oubli. Des résistances sont nécessaires pour ne pas brûler le port si le micrologiciel présente un problème et règle la broche sur une, et que le commutateur à lames court-circuite cette ligne à la masse (avec la résistance, un maximum de 3.3 V/1000 3.3 Ohm = XNUMX mA circulera).

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 début les valeurs initiales des compteurs au serveur. Mais cela nécessiterait une complication importante du protocole d'échange. De plus, les performances de l'appareil dépendent dans ce cas de l'état du serveur. Si le serveur ne démarrait pas après la mise hors tension (ou démarré plus tard), le compteur d'eau ne pourrait pas demander les valeurs initiales et ne fonctionnerait pas correctement.

Par conséquent, j'ai décidé d'implémenter la sauvegarde des valeurs du compteur dans une puce mémoire connectée via I2C. Je n'ai pas d'exigences particulières concernant la taille de la mémoire flash - il vous suffit de sauvegarder 2 chiffres (le nombre de litres selon les compteurs d'eau chaude et froide). Même le plus petit module fera l’affaire. Mais il faut faire attention au nombre de cycles d'enregistrement. Pour la plupart des modules, cela représente 100 XNUMX cycles, pour certains jusqu'à un million.

Il semblerait qu'un million, c'est beaucoup. Mais pendant les 4 années de vie dans mon appartement, j'ai consommé un peu plus de 500 mètres cubes d'eau, soit 500 mille litres ! Et 500 2 enregistrements en flash. Et ce n'est que de l'eau froide. Vous pouvez bien sûr ressouder la puce tous les deux ans, mais il s'avère qu'il existe des puces FRAM. D'un point de vue programmation, il s'agit de la même EEPROM I24C, mais avec un très grand nombre de cycles de réécriture (des centaines de millions). C'est juste que je ne peux toujours pas me rendre au magasin avec de tels microcircuits, donc pour l'instant, le 512LCXNUMX habituel tiendra.

Circuit imprimé

Au départ, j'avais prévu de fabriquer la planche à la maison. Le tableau a donc été conçu de manière unilatérale. Mais après avoir passé une heure avec un fer laser et un masque de soudure (ce n'est pas comme il faut sans cela), j'ai quand même décidé de commander les planches aux Chinois.

Nous connectons le compteur d'eau à la maison intelligente

Presque avant de commander la carte, j'ai réalisé qu'en plus de la puce mémoire flash, je pouvais connecter autre chose d'utile au bus I2C, comme un écran. Ce qu'il faut exactement afficher reste une question, mais il doit être acheminé sur la carte. Bon, puisque j'allais commander des cartes à l'usine, ça ne servait à rien de me limiter à une carte simple face, donc les lignes I2C sont les seules à l'arrière de la carte.

Il y avait aussi un gros problème avec le câblage unidirectionnel. Parce que La carte a été dessinée sur un seul côté, de sorte que les pistes et les composants CMS devaient être placés d'un côté, et les composants de sortie, les connecteurs et l'alimentation de l'autre. Lorsque j'ai reçu les cartes un mois plus tard, j'ai oublié le plan original et j'ai soudé tous les composants sur la face avant. Et ce n'est que lorsqu'il s'est agi de souder l'alimentation qu'il s'est avéré que le plus et le moins étaient câblés à l'envers. J'ai dû cultiver avec des cavaliers. Dans l'image ci-dessus, j'ai déjà modifié le câblage, mais la masse est transférée d'une partie de la carte à l'autre via les broches du bouton Boot (même s'il serait possible de tracer une piste sur la deuxième couche).

Ça s'est passé comme ça

Nous connectons le compteur d'eau à la maison intelligente

logement

La prochaine étape est le corps. Si vous possédez une imprimante 3D, ce n'est pas un problème. Je ne me suis 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 avec de petites vis autotaraudeuses.

Nous connectons le compteur d'eau à la maison intelligente

J'ai déjà mentionné que le bouton Boot peut être utilisé comme bouton à usage général - nous l'afficherons donc sur le panneau avant. Pour ce faire, j'ai dessiné un « puits » spécial où vit le bouton.

Nous connectons le compteur d'eau à la maison intelligente

A l'intérieur du boîtier se trouvent également des plots sur lesquels la carte est installée et fixée avec une seule vis M3 (il n'y avait plus de place sur la carte)

J'ai déjà sélectionné l'affichage lorsque j'ai imprimé le premier exemple de version du boîtier. Un lecteur standard à deux lignes ne rentrait pas dans ce boîtier, mais en bas se trouvait un écran OLED SSD1306 128×32. C’est un peu petit, mais je n’ai pas besoin de le regarder tous les jours, c’est trop pour moi.

Comprenant de quelle manière et comment les fils seraient acheminés, j'ai décidé de coller l'écran au milieu du boîtier. L'ergonomie, bien sûr, est en dessous de la moyenne - le bouton est en haut, l'écran est en bas. Mais j'ai déjà dit que l'idée de fixer l'écran est arrivée trop tard et que j'ai eu la flemme de recâbler la carte pour déplacer le bouton.

L'appareil est assemblé. Le module d'affichage est collé à la morve avec de la colle chaude

Nous connectons le compteur d'eau à la maison intelligente

Nous connectons le compteur d'eau à la maison intelligente

Le résultat final peut être vu sur KDPV

Firmware

Passons à la partie logicielle. Pour les petits bricolages comme celui-ci, j'aime vraiment utiliser Python (micropython) - le code s'avère très compact et compréhensible. Heureusement, il n'est pas nécessaire de descendre au niveau du registre pour gagner des microsecondes - tout peut être fait à partir de Python.

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

  • L'utilisateur appuie sur le 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 une ampoule clignotante, c'est impossible

Vous ne pouvez pas supposer qu’une fonction n’a pas fonctionné si une autre est bloquée pour une raison quelconque. J'ai déjà fait le plein de cactus dans d'autres projets et maintenant je vois encore des problèmes du style "j'ai raté un autre litre parce que l'affichage était en cours de mise à jour à ce moment-là" ou "l'utilisateur ne peut rien faire pendant que le module se connecte à Wifi." Bien sûr, certaines choses peuvent être effectuées via des interruptions, mais vous pouvez rencontrer des limitations de durée, d'imbrication des appels ou de modifications non atomiques des variables. Eh bien, le code qui fait tout se transforme rapidement en bouillie.

В projet plus sérieux J'ai utilisé le multitâche préemptif classique et FreeRTOS, mais dans ce cas le modèle s'est avéré beaucoup plus adapté coroutines et bibliothèques uasync . De plus, l'implémentation Python des coroutines est tout simplement incroyable - tout est fait de manière simple et pratique pour le programmeur. Écrivez simplement votre propre logique, dites-moi simplement à quels endroits vous pouvez basculer entre les flux.

Je suggère d'étudier les différences entre le multitâche préemptif et compétitif en tant que matière facultative. Passons maintenant enfin 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 géré 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 réalisée.

La broche est initialisée avec un pull-up intégré vers l'alimentation : si le commutateur à lames est fermé, la ligne est nulle, si la ligne est ouverte, elle est tirée vers l'alimentation et le contrôleur en lit un.

Une tâche distincte est également 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 des contacts, et en même temps il régule la fréquence de réveil de la tâche (pendant que cette tâche est en veille, d'autres tâches sont en cours d'exécution). Toutes les 25 ms, la fonction se réveille, vérifie la broche et si les contacts du commutateur Reed sont fermés, alors un autre litre a traversé le compteur et doit être traité.

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

        self._value_storage.write(self._value)

Traiter le litre suivant est trivial - le compteur augmente simplement. Eh bien, ce serait bien d'écrire la nouvelle valeur sur une clé USB.

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, profitons maintenant des plaisirs de Python et de la bibliothèque uasync et créons un objet compteur waitable (comment pouvons-nous traduire cela en russe ? Celui auquel vous pouvez vous 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 tellement pratique qui attend que la valeur du compteur soit mise à jour - la fonction se réveille de temps en temps et vérifie l'indicateur _value_changed. Ce qui est intéressant à propos de cette fonction, c'est que le code appelant peut s'endormir lors de l'appel de cette fonction et dormir jusqu'à ce qu'une nouvelle valeur soit reçue.

Et les interruptions ?Oui, à ce stade, vous pouvez me troller en disant que vous avez vous-même parlé d'interruptions, mais en réalité, vous avez fait un stupide sondage. En fait, les interruptions sont la première chose que j'ai essayée. Dans l'ESP8266, vous pouvez organiser une interruption de bord et même écrire un gestionnaire pour cette interruption en Python. Dans cette interruption, la valeur d'une variable peut être mise à jour. Cela serait probablement suffisant si le compteur était un appareil esclave - un appareil qui attend qu'on lui demande cette valeur.

Malheureusement (ou heureusement ?) mon appareil est actif, il doit lui-même envoyer des messages via le protocole MQTT et écrire des données dans l'EEPROM. Et ici, les restrictions entrent en jeu - vous ne pouvez pas allouer de mémoire lors des interruptions et utiliser une grande pile, ce qui signifie que vous pouvez oublier d'envoyer des messages sur le réseau. Il existe des petits pains comme micropython.schedule() qui vous permettent d'exécuter certaines fonctions « dès que possible », mais la question se pose : « à quoi ça sert ? Que se passe-t-il si nous envoyons une sorte de message en ce moment, puis qu'une interruption arrive et gâche les valeurs des variables. Ou, par exemple, une nouvelle valeur de compteur arrive du serveur alors que nous n'avons pas encore noté l'ancienne. En général, vous devez bloquer la synchronisation ou en sortir d'une manière ou d'une autre.

Et de temps en temps RuntimeError : planifiez des crashs complets de la pile et qui sait pourquoi ?

Avec des sondages explicites et uasync, dans ce cas, cela s'avère plus beau et plus fiable

J'ai apporté le travail avec EEPROM à 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, il est difficile de travailler directement avec les octets, mais ce sont les octets qui sont écrits en mémoire. J'ai dû clôturer la conversion entre entier et octets en utilisant la bibliothèque ustruct.

Afin de ne pas transférer à chaque fois l'objet I2C et l'adresse de la cellule mémoire, j'ai tout enveloppé 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 ces paramètres

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

Nous arrivons à la partie la plus intéressante : la mise en œuvre de la communication avec le serveur via MQTT. Eh bien, il n'est pas nécessaire d'implémenter le protocole lui-même - je l'ai trouvé sur Internet implémentation asynchrone prête à l'emploi. C'est ce que nous utiliserons.

Toutes les choses les plus intéressantes sont rassemblées dans la classe CounterMQTTClient, 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, vous pouvez créer et configurer des broches et des boutons d'ampoule, ainsi que des objets de compteur 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 fonctionnement 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 écrire les paramètres directement dans le code, je les stocke dans le fichier texte config.txt. Cela permet de modifier le code quels que soient les réglages, ainsi que de riveter plusieurs appareils identiques avec des paramètres différents.

Le dernier bloc de code démarre plusieurs coroutines pour remplir diverses fonctions du système. Par exemple, voici une coroutine qui dessert les 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))

La coroutine attend en boucle une nouvelle valeur de compteur et, dès son apparition, envoie un message via le protocole MQTT. Le premier morceau de code envoie la valeur initiale même si aucune eau ne traverse le compteur.

La classe de base MQTTClient se sert elle-même, initie une connexion WiFi et se reconnecte lorsque la connexion est perdue. Lorsqu'il y a des changements dans l'état de la connexion WiFi, la bibliothèque nous en 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 copiée à partir d'exemples. Dans ce cas, il compte le nombre de pannes (internet_outages) et leur durée. Lorsque la connexion est rétablie, un temps d'inactivité est envoyé au serveur.

À propos, le dernier sommeil n'est nécessaire que pour rendre la fonction asynchrone - dans la bibliothèque, elle est appelée via wait, et seules les fonctions dont le corps contient un autre wait peuvent être appelées.

En plus de vous connecter au WiFi, vous devez également établir une connexion au courtier MQTT (serveur). La bibliothèque fait cela aussi, et 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 nous abonnons à plusieurs messages - le serveur a désormais la possibilité de définir les valeurs actuelles du compteur en envoyant le message correspondant.

    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 (titre du message), les valeurs d'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 un message si la 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, et nous n’avons pas encore fait clignoter 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 clignotants. Si la connexion est perdue (ou si elle vient juste d'être établie), l'appareil clignotera rapidement. Si la connexion est établie, l'appareil clignote une fois toutes les 5 secondes. Si nécessaire, d'autres modes de clignotement peuvent être implémentés ici.

Mais la LED ne fait que chouchouter. Nous avons également visé 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 de cela dont je parlais : à quel point c'est simple et pratique avec les coroutines. Cette petite fonction décrit TOUTE l’expérience utilisateur. La coroutine attend simplement que le bouton soit enfoncé et allume l'écran pendant 3 secondes. L'écran affiche les relevés actuels du compteur.

Il reste encore quelques petites choses. Voici la fonction qui (re)démarre toute cette entreprise. La boucle principale envoie simplement diverses informations de débogage une fois par minute. En général, je le cite tel quel - je ne pense pas qu'il soit nécessaire de trop commenter

   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)

Tout commence comme ça

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

Quelque chose est arrivé à ma mémoire

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

On lance et... On obtient une MemoryError. De plus, plus j'essayais de comprendre où exactement la mémoire fuyait, plus je plaçais d'impressions de débogage, plus cette erreur apparaissait tôt. Une courte recherche sur Google m'a amené à comprendre que le microcontrôleur ne dispose, en principe, que de 30 Ko de mémoire, dans lesquels 65 Ko de code (y compris les bibliothèques) ne peuvent tout simplement pas tenir.

Mais il existe une issue. Il s'avère que micropython n'exécute pas le code directement à partir d'un fichier .py - ce fichier est compilé en premier. De plus, il est compilé directement sur le microcontrôleur, transformé 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 les fichiers sur un grand ordinateur et télécharger le bytecode prêt à l'emploi dans le microcontrôleur. Pour ce faire, vous devez télécharger le firmware micropython et construire utilitaire mpy-cross.

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

mpy-cross water_counter.py

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

J'ai fait tout le développement dans le 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, il doit également y avoir un fichier appelé main.py au démarrage. De plus, il doit s'agir exactement de .py, et non de .mpy précompilé. Voici son contenu trivial

import water_counter

Nous le lançons - tout fonctionne. Mais la mémoire libre est extrêmement petite - environ 1 Ko. J'ai toujours l'intention d'étendre les fonctionnalités de l'appareil, et ce kilo-octet ne me suffit clairement pas. Mais il s’est avéré qu’il existe également une issue pour cette affaire.

Voici le truc. Même si les fichiers sont compilés en bytecode et résident sur le système de fichiers interne, en réalité ils sont toujours chargés dans la RAM et exécutés à partir de là. Mais il s'avère que Micropython peut exécuter du 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 certain temps sur mon netbook (seulement là, j'avais Linux).

L'algorithme est le suivant:

  • ачать и установить SDK ouvert ESP. Cette chose assemble un compilateur et des bibliothèques pour les programmes pour l'ESP8266. Assemblé selon les instructions sur la page principale du projet (j'ai choisi le paramètre STANDALONE=yes)
  • télécharger tris micropython
  • Placez les bibliothèques requises dans ports/esp8266/modules à l'intérieur de l'arborescence micropython
  • Nous assemblons le firmware selon les instructions du fichier ports/esp8266/README.md
  • Nous téléchargeons le firmware sur le microcontrôleur (je le fais sous Windows en utilisant le programme ESP8266Flasher ou Python esptool)

Voilà, maintenant 'import ssd1306' supprimera le code directement du firmware et la RAM ne sera pas consommé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 nous permettra d’implémenter de nombreuses fonctionnalités utiles différentes à l’avenir. Eh bien, s'il n'y a pas assez de mémoire du tout, vous pouvez alors insérer le programme principal dans le firmware.

Alors, que devrions-nous 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 une ampoule fait clignoter joyeusement. Mais pour l’instant, tout cela n’est qu’une boîte noire (au propre comme au figuré) et cela ne sert encore à rien. Il est temps de faire quelque chose avec les messages MQTT envoyés au serveur.

Ma « maison intelligente » tourne Système Majordome. Le module MQTT est soit prêt à l'emploi, soit facilement installé à partir du marché des modules complémentaires - je ne me souviens pas d'où je l'ai obtenu. MQTT n'est pas une chose autonome - vous avez besoin d'un soi-disant. courtier - un serveur qui reçoit, trie et transmet les messages MQTT aux clients. J'utilise mosquitto, qui (comme majordomo) fonctionne sur le même netbook.

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

Nous connectons le compteur d'eau à la maison intelligente

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 dépasse le cadre de cet article. Je peux recommander le système majordomo à toute personne intéressée canal électronique dans l'objectif — un ami construit également une maison intelligente et parle clairement de la configuration du système.

Je vais juste vous montrer quelques graphiques. Ceci est un simple graphique des valeurs quotidiennes

Nous connectons le compteur d'eau à la maison intelligente
On constate que presque personne n’utilisait l’eau la nuit. Plusieurs fois, quelqu'un est allé aux toilettes et il semble que le filtre à osmose inverse aspire quelques litres par nuit. Le matin, la consommation augmente considérablement. J'utilise habituellement l'eau d'une chaudière, mais j'ai ensuite voulu prendre un bain et je suis temporairement passé à l'eau chaude de la ville - cela est également clairement visible dans le graphique du bas.

Grâce à ce graphique, j'ai appris qu'aller aux toilettes nécessite 6 à 7 litres d'eau, prendre une douche nécessite 20 à 30 litres, faire la vaisselle nécessite environ 20 litres et prendre un bain nécessite 160 litres. Ma famille en consomme entre 500 et 600 litres par jour.

Pour ceux qui sont particulièrement curieux, vous pouvez consulter les enregistrements pour chaque valeur individuelle

Nous connectons le compteur d'eau à la maison intelligente

De là, j'ai appris que lorsque le robinet est ouvert, l'eau s'écoule à une vitesse d'environ 1 litre toutes les 5 s.

Mais sous cette forme, les statistiques ne sont probablement pas très pratiques à consulter. Majordomo a également la possibilité de visualiser des graphiques de consommation par jour, semaine et mois. Voici par exemple un graphique de consommation en barres

Nous connectons le compteur d'eau à la maison intelligente

Jusqu'à présent, je n'ai que des données pour une semaine. Dans un mois, ce graphique sera plus indicatif - chaque jour aura une colonne distincte. Le tableau est légèrement gâché par les ajustements des valeurs que je saisis manuellement (la plus grande colonne). Et il n'est pas encore clair si j'ai mal réglé les toutes premières valeurs, presque un cube de moins, ou s'il s'agit d'un bug du firmware et que tous les litres n'ont pas été comptés. Besoin de plus de temps.

Les graphiques eux-mêmes ont encore besoin d'un peu de magie, de blanchiment, de peinture. Peut-être que je créerai également un graphique de la consommation de mémoire à des fins de débogage - au cas où quelque chose y fuirait. Peut-être que j'afficherai d'une manière ou d'une autre les périodes où il n'y avait pas Internet. Pour l’instant, tout cela est au niveau des idées.

Conclusion

Aujourd'hui, mon appartement est devenu un peu plus intelligent. Avec un si petit appareil, il me sera plus pratique de surveiller la consommation d'eau dans la maison. Si plus tôt je m'indignais du « encore une fois, on a consommé beaucoup d'eau en un mois », maintenant je peux trouver la source de cette consommation.

Certains peuvent trouver étrange de regarder les lectures sur l'écran s'il se trouve à un mètre du compteur lui-même. Mais dans un avenir pas très lointain, j'envisage de déménager dans un autre appartement, où il y aura plusieurs colonnes montantes d'eau, et les compteurs eux-mêmes seront très probablement situés sur le palier. Un appareil de lecture à distance sera 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 passer la chaudière à l'eau de ville, je dois ouvrir 3 robinets dans une niche difficile d'accès. Il serait beaucoup plus pratique de le faire avec un seul bouton avec l'indication correspondante. Eh bien, bien sûr, cela vaut la peine de mettre en place une protection contre les fuites.

Dans l'article, j'ai décrit ma version d'un appareil basé sur ESP8266. À mon avis, j'ai proposé une version très intéressante du firmware micropython utilisant des coroutines - simple et agréable. J'ai essayé de décrire bon nombre des nuances et des lacunes que j'ai rencontrées au cours de la campagne. Peut-être ai-je tout décrit avec trop de détails ; personnellement, en tant que lecteur, il m’est plus facile de sauter les choses inutiles que de réfléchir plus tard à ce qui n’a pas été dit.

Comme toujours, je suis ouvert aux critiques constructives.

Code source
Circuit et carte
Modèle de boîtier

Source: habr.com

Ajouter un commentaire