Conectamos el contador de agua a la casa inteligente

Érase una vez, los sistemas domóticos, o como se les suele llamar "hogares inteligentes", eran terriblemente caros y sólo los ricos podían permitírselos. Hoy en el mercado se pueden encontrar kits bastante económicos con sensores, botones / interruptores y actuadores para controlar la iluminación, enchufes, ventilación, suministro de agua y otros consumidores. E incluso el aficionado al bricolaje más corrupto puede unirse a la belleza y ensamblar dispositivos para una casa inteligente por un precio económico.

Conectamos el contador de agua a la casa inteligente

Como regla general, los dispositivos propuestos son sensores o actuadores. Facilitan la implementación de escenarios como "cuando se activa el sensor de movimiento, enciende la luz" o "el interruptor cerca de la salida apaga la luz en todo el apartamento". Pero de alguna manera no funcionó con la telemetría. En el mejor de los casos, se trata de un gráfico de temperatura y humedad, o de potencia instantánea en un tomacorriente en particular.

Recientemente instalé medidores de agua con salida de pulsos. Por cada litro que ha pasado por el contador, se activa el interruptor de láminas y cierra el contacto. Lo único que queda por hacer es agarrarse a los cables e intentar sacarle algún beneficio. Por ejemplo, analizar el consumo de agua por horas y días de la semana. Bueno, si hay varios elevadores de agua en el apartamento, entonces es más conveniente ver todos los indicadores actuales en una pantalla que escalar nichos de difícil acceso con una linterna.

Debajo del corte está mi versión de un dispositivo basado en ESP8266, que cuenta los pulsos de los medidores de agua y envía lecturas a través de MQTT al servidor doméstico inteligente. Programaremos en micropython usando la biblioteca uasyncio. Al crear el firmware, me encontré con varias dificultades interesantes, de las que también hablaré en este artículo. ¡Ir!

esquema

Conectamos el contador de agua a la casa inteligente

El corazón de todo el circuito es un módulo del microcontrolador ESP8266. Inicialmente se planeó el ESP-12, pero el mío resultó defectuoso. Tuve que contentarme con el módulo ESP-07, que estaba disponible. Afortunadamente, son iguales tanto en conclusiones como en funcionalidad, la única diferencia está en la antena: el ESP-12 la tiene incorporada, mientras que el ESP-07 tiene una externa. Sin embargo, incluso sin una antena WiFi, la señal en mi baño se capta con normalidad.

La encuadernación del módulo es estándar:

  • botón de reinicio con pull-up y condensador (aunque ambos ya están dentro del módulo)
  • La señal de habilitación (CH_PD) se eleva al poder.
  • GPIO15 es tirado al suelo. Esto sólo es necesario al principio, pero todavía no tengo nada que sujetar a esta pierna; ya no lo necesito.

Para transferir el módulo al modo firmware, debe cerrar GPIO2 al suelo y, para hacerlo más conveniente, proporcioné el botón de arranque. En el estado normal, este pin se activa.

El estado de la línea GPIO2 se verifica solo al comienzo de la operación, cuando se aplica energía o inmediatamente después de un reinicio. Entonces, el módulo arranca como de costumbre o entra en modo de firmware. Una vez cargado, este pin se puede utilizar como un GPIO normal. Bueno, como ya hay un botón allí, puedes colgarle alguna función útil.

Para programar y depurar usaré el UART que traje al peine. Cuando es necesario, simplemente conecto allí un adaptador USB-UART. Solo hay que recordar que el módulo funciona con 3.3V. Si olvida cambiar el adaptador a este voltaje y aplicar 5 V, lo más probable es que el módulo se queme.

No tengo problemas con la electricidad en el baño: el tomacorriente está ubicado aproximadamente a un metro de los medidores, por lo que lo alimentaré con 220 V. Como fuente de energía, tendré una pequeña bloque HLK-PM03 por Tenstar Robot. Personalmente, tengo dificultades con la electrónica analógica y de potencia, y aquí tengo una fuente de alimentación lista para usar en un estuche pequeño.

Para señalar los modos de funcionamiento, proporcioné un LED conectado a GPIO2. Sin embargo, no lo soldé, es decir. el módulo ESP-07 ya tiene un LED conectado al mismo GPIO2. Pero que esté en el tablero: de repente quiero llevar este LED a la carcasa.

Pasemos a lo más interesante. Los contadores de agua no tienen lógica, no se les puede pedir lecturas actuales. Lo único que tenemos a nuestra disposición son los impulsos: cerrar los contactos del interruptor de láminas por cada litro. Tengo las salidas del interruptor de láminas en GPIO12/GPIO13. Activaré la resistencia pull-up mediante programación dentro del módulo.

Inicialmente, olvidé proporcionar las resistencias R8 y R9 y mi versión de la placa no las tiene. Pero como ya he publicado el diagrama para que todos lo vean, vale la pena corregir este descuido. Se necesitan resistencias para no quemar el puerto si el firmware falla y configura el pin en uno, y el interruptor de láminas corta esta línea a tierra (con la resistencia fluirá un máximo de 3.3 V/1000 ohmios = 3.3 mA).

Es hora de pensar qué hacer si se corta la electricidad. La primera opción es pedir al servidor los valores iniciales de los contadores al inicio. Pero esto requeriría una complicación significativa del protocolo de intercambio. Además, el rendimiento del dispositivo en este caso depende del estado del servidor. Si después de apagar la luz el servidor no arrancaba (o arrancaba más tarde), entonces el contador de agua no podría solicitar los valores iniciales y funcionaría incorrectamente.

Por lo tanto, decidí implementar el guardado de valores de contador en un chip de memoria conectado a través de I2C. No tengo ningún requisito especial para el tamaño de la memoria flash: solo necesita guardar 2 números (el número de litros según los medidores de agua fría y caliente). Incluso el módulo más pequeño servirá. Pero hay que prestar atención al número de ciclos de grabación. Para la mayoría de los módulos esto es 100 mil ciclos, para algunos hasta un millón.

Parecería que un millón es mucho. Pero durante 4 años de vivir en mi departamento consumí un poco más de 500 metros cúbicos de agua, ¡son 500 mil litros! Y 500 mil registros en flash. Y eso es sólo agua fría. Por supuesto, puedes volver a soldar el chip cada dos años, pero resultó que hay chips FRAM. Desde el punto de vista de la programación, esta es la misma EEPROM I2C, solo que con una gran cantidad de ciclos de reescritura (cientos de millones). Eso es solo hasta que todavía no pueda llegar a una tienda con tales microcircuitos, por lo que por ahora el 24LC512 habitual se mantendrá.

Placa de circuito

Inicialmente planeé hacer una tabla en casa. Por lo tanto, el tablero se diseñó unilateralmente. Pero después de pasar una hora con una plancha láser y una máscara de soldadura (de alguna manera no es común sin ella), decidí pedir placas a los chinos.

Conectamos el contador de agua a la casa inteligente

Casi antes de pedir la placa, me di cuenta de que, además del chip de memoria flash, se podía conectar algo más útil al bus I2C, por ejemplo, una pantalla. Aún es una pregunta qué enviar exactamente, pero es necesario reproducirlo en el tablero. Bueno, como iba a pedir placas en fábrica, no tenía sentido limitarme a una placa de un solo lado, por lo que las líneas I2C son las únicas en la parte posterior de la placa.

Una jamba grande también estaba conectada con el cableado unidireccional. Porque la placa se dibujó por un lado, luego se planeó colocar las pistas y los componentes SMD en un lado y los componentes de salida, conectores y fuente de alimentación en el otro. Cuando recibí las placas un mes después, me olvidé del plano original y soldé todos los componentes en la parte frontal. Y solo cuando llegó el momento de soldar la fuente de alimentación, resultó que los más y los menos estaban divorciados al revés. Tuve que cultivar con saltadores. En la imagen de arriba, ya cambié el cableado, pero la tierra se transfiere de una parte de la placa a otra a través de los pines del botón de arranque (aunque sería posible dibujar una pista en la segunda capa).

resultó así

Conectamos el contador de agua a la casa inteligente

viviendas

El siguiente paso es el cuerpo. Si tienes una impresora 3D, esto no es un problema. No me molesté mucho: simplemente dibujé una caja del tamaño correcto e hice recortes en los lugares correctos. La tapa se fija al cuerpo con pequeños tornillos autorroscantes.

Conectamos el contador de agua a la casa inteligente

Ya mencioné que el botón de Arranque se puede usar como botón de uso general, así que llevémoslo al panel frontal. Para hacer esto, dibujé un "pozo" especial donde se encuentra el botón.

Conectamos el contador de agua a la casa inteligente

También hay talones dentro de la caja, en los que se instala la placa y se fija con un solo tornillo M3 (no había más espacio en la placa)

Ya seleccioné la pantalla cuando imprimí la primera versión de muestra del estuche. En este caso no cabía un lector estándar de dos líneas, pero en la parte inferior había una pantalla OLED SSD1306 128×32. Es un poco pequeño, pero no tengo que mirarlo todos los días; es demasiado para mí.

Habiendo estimado de esta manera cómo se colocarán los cables, decidí colocar la pantalla en el medio de la caja. La ergonomía, por supuesto, está debajo del zócalo: el botón está arriba y la pantalla abajo. Pero ya dije que la idea de atornillar la pantalla llegó demasiado tarde y me dio pereza volver a cablear la placa para mover el botón.

Dispositivo ensamblado. El módulo de visualización se pega a los mocos con pegamento caliente.

Conectamos el contador de agua a la casa inteligente

Conectamos el contador de agua a la casa inteligente

El resultado final se puede ver en KDPV

Firmware

Pasemos a la parte del software. Para manualidades tan pequeñas, me gusta mucho usar el lenguaje Python (micropitón) - el código es muy compacto y comprensible. Afortunadamente, no es necesario bajar al nivel de registros para exprimir microsegundos; todo se puede hacer desde Python.

Parece que todo es simple, pero no muy: el dispositivo tiene varias funciones independientes:

  • El usuario toca un botón y mira la pantalla.
  • Los litros marcan y actualizan los valores en la memoria flash.
  • El módulo monitorea la señal WiFi y se reconecta si es necesario
  • Bueno, sin una bombilla parpadeante, no puedes en absoluto.

No se puede asumir que una función no funcionó si la otra falla por algún motivo. Ya he comido cactus en otros proyectos y ahora sigo viendo fallos como “se perdió otro litro porque la pantalla se estaba actualizando en ese momento” o “el usuario no puede hacer nada mientras el módulo se conecta a WiFi”. Por supuesto, algunas cosas se pueden hacer mediante interrupciones, pero puede encontrarse con una limitación de duración, anidamiento de llamadas o cambios no atómicos de variables. Bueno, el código que hace todo e inmediatamente se convierte en un desastre.

В proyecto mas serio Utilicé la multitarea preventiva clásica y FreeRTOS, pero en este caso el modelo resultó ser mucho más adecuado corrutinas y bibliotecas uasync . Además, la implementación de corrutinas en Python es simplemente una bomba: todo se hace de manera simple y conveniente para el programador. Simplemente escribe tu propia lógica, solo dime dónde puedes cambiar entre hilos.

Propongo estudiar las diferencias entre multitarea preventiva y competitiva como opcional. Ahora finalmente lleguemos al código.

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

Cada contador es manejado por una instancia de la clase Counter. En primer lugar, el valor inicial del contador se resta de la EEPROM (value_storage); así es como se implementa la recuperación después de un corte de energía.

El pin se inicializa con un pull-up incorporado a la fuente de alimentación: si el interruptor de láminas está cerrado, la línea es cero, si la línea está abierta, se acerca a la fuente de alimentación y el controlador lee uno.

Además, aquí se inicia una tarea separada que sondeará el pin. Cada contador ejecutará su propia tarea. Aquí está su código.

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

Se necesita un retraso de 25 ms para filtrar el rebote de contactos y, al mismo tiempo, regula la frecuencia con la que se activa la tarea (mientras esta tarea está inactiva, otras tareas están funcionando). Cada 25 ms, la función se activa, verifica el pin y, si los contactos del interruptor de láminas están cerrados, entonces ha pasado otro litro por el contador y es necesario procesarlo.

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

        self._value_storage.write(self._value)

Procesar el siguiente litro es trivial: el contador simplemente aumenta. Bueno, sería bueno escribir un nuevo valor en una unidad flash USB.

Para facilitar su uso, se proporcionan "accesorios".

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

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

Bueno, ahora usemos las delicias de Python y la biblioteca uasync y hagamos que el objeto contador sea esperable (¿cómo puedo traducirlo al ruso? ¿El que se puede esperar?)

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

        return self.value()

    __iter__ = __await__  

Esta es una función muy útil que espera hasta que se actualice el valor del contador; la función se activa de vez en cuando y verifica el indicador _value_changed. El truco de esta función es que el código de llamada puede quedar dormido en una llamada a esta función y dormir hasta que se reciba un nuevo valor.

Pero ¿qué pasa con las interrupciones?Sí, en este punto puedes trollearme, diciendo que él mismo habló de las interrupciones, pero en realidad organizó una estúpida encuesta. En realidad, las interrupciones son lo primero que probé. En ESP8266, puede organizar una interrupción en el frente e incluso escribir un controlador de interrupción para esta interrupción en Python. En esta interrupción, puede actualizar el valor de una variable. Probablemente, esto sería suficiente si el contador fuera un dispositivo esclavo, uno que espera hasta que se le solicite este valor.

Desafortunadamente (¿o afortunadamente?), mi dispositivo está activo, él mismo debería enviar mensajes a través del protocolo MQTT y escribir datos en EEPROM. Y aquí ya entran las restricciones: no puede asignar memoria en interrupciones y usar una pila grande, lo que significa que puede olvidarse de enviar mensajes a través de la red. Hay bollos como micropython.schedule () que te permiten ejecutar algún tipo de función "lo antes posible", pero surge la pregunta "¿cuál es el punto?". De repente, estamos enviando algún tipo de mensaje en este momento, y luego irrumpe una interrupción y estropea los valores de las variables. O, por ejemplo, nos ha llegado un nuevo valor de contador del servidor y aún no hemos registrado el anterior. En general, es necesario bloquear la sincronización o salir de alguna otra manera.

Y de vez en cuando RuntimeError: programa fallas completas de la pila y ¿quién sabe por qué?

Con sondeo explícito y uasync, en este caso, de alguna manera resulta más hermoso y más confiable.

Saqué el trabajo con EEPROM en una clase pequeña.

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)

Es difícil trabajar con bytes directamente en Python, y son los bytes los que se escriben en la memoria. Tuve que limitar la conversión entre un número entero y bytes usando la biblioteca ustruct.

Para no transmitir el objeto I2C y la dirección de la celda de memoria cada vez, lo envolví todo en un pequeño y conveniente clásico.

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)

El objeto I2C en sí se crea con estos parámetros

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

Nos acercamos a lo más interesante: la implementación de la comunicación con el servidor a través de MQTT. Bueno, no es necesario implementar el protocolo en sí; lo encontré en Internet. implementación asincrónica lista para usar. Aquí lo usaremos.

Todo lo más interesante está recogido en la clase CounterMQTTClient, que se basa en la biblioteca MQTTClient. Empecemos por la periferia.

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

Aquí se crean y configuran bombillas y pines de botones, así como objetos de contadores de agua fría y caliente.

Con la inicialización no todo es tan 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())

Para configurar los parámetros de la biblioteca mqtt_as, se utiliza un gran diccionario de diferentes configuraciones: config. La mayoría de las configuraciones predeterminadas funcionan para nosotros, pero muchas configuraciones deben establecerse explícitamente. Para no prescribir las configuraciones directamente en el código, las guardo en un archivo de texto config.txt. Esto le permite cambiar el código independientemente de la configuración, así como remachar varios dispositivos idénticos con diferentes parámetros.

El último bloque de código inicia varias rutinas para realizar diversas funciones del sistema. A continuación se muestra un ejemplo de una corrutina que sirve contadores.

    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 corrutina espera en un bucle un nuevo valor de contador y, tan pronto como aparece, envía un mensaje a través del protocolo MQTT. El primer fragmento de código envía el valor inicial incluso si no fluye agua por el contador.

La clase base MQTTClient se sirve a sí misma, inicia una conexión WiFi y se vuelve a conectar cuando se pierde la conexión. Cuando el estado de la conexión WiFi cambia, la biblioteca nos informa llamando a 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)

Honestamente, la función está lamida de los ejemplos. En este caso, cuenta el número de cortes (internet_outages) y su duración. Cuando se restablece la conexión, se envía un tiempo de inactividad al servidor.

Por cierto, el último sueño solo es necesario para que la función se vuelva asincrónica: en la biblioteca se llama a través de await, y solo se pueden llamar funciones en cuyo cuerpo hay otro await.

Además de conectarse a WiFi, también debe establecer una conexión con el corredor MQTT (servidor). Esto también lo hace la biblioteca y tenemos la oportunidad de hacer algo útil cuando se establece la conexión.

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

Aquí nos suscribimos a varios mensajes: el servidor ahora tiene la capacidad de establecer los valores actuales de los contadores enviando el mensaje apropiado.

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

Esta función procesa los mensajes entrantes y, según el tema (nombre del mensaje), se actualizan los valores de uno de los contadores.

Un par de funciones auxiliares

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

Esta función envía un mensaje si se establece la conexión. Si no hay conexión, el mensaje se ignora.

Y esta es solo una función conveniente que genera y envía mensajes de depuración.

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

Tanto texto y aún no hemos hecho parpadear un LED. Aquí

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

Proporcioné 2 modos de parpadeo. Si se pierde la conexión (o recién se está estableciendo), el dispositivo parpadeará rápidamente. Si se establece la conexión, el dispositivo parpadea cada 5 segundos. Si es necesario, aquí se pueden implementar otros modos de parpadeo.

Pero el LED es así, mimoso. También golpeamos la exhibición.

    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)

De esto es de lo que estaba hablando: de lo simple y conveniente que son las corrutinas. Esta pequeña función describe TODA la interacción del usuario. La rutina simplemente espera a que se presione el botón y enciende la pantalla durante 3 segundos. La pantalla muestra las lecturas actuales del medidor.

Todavía quedan un par de cositas. Aquí está la función que (re)inicia toda esta economía. El bucle principal solo se ocupa de enviar información de depuración diversa una vez por minuto. En general, lo doy como está; no necesito comentar específicamente, creo

   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)

Bueno, un par de configuraciones y constantes más para completar la descripción.

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

todo empieza asi

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

Algo le paso a mi memoria

Entonces, todo el código está ahí. Cargué los archivos usando la utilidad ampy: te permite cargarlos en la unidad flash interna (la que está en el ESP-07) y luego acceder a ella desde el programa como archivos normales. Allí también subí las bibliotecas mqtt_as, uasyncio, ssd1306 y colecciones que usé (usadas dentro de mqtt_as).

Empezamos y... Recibimos MemoryError. Además, cuanto más intentaba comprender exactamente dónde se estaba perdiendo la memoria, cuanto más depuraba las impresiones y antes se producía este error. Un breve google me llevó a comprender que en el microcontrolador, en principio, solo hay 30 kb de memoria, en los que 65 kb de código (junto con las bibliotecas) no caben de ninguna manera.

Pero hay una salida. Resulta que micropython no ejecuta código directamente desde un archivo .py; este archivo se compila primero. Además, se compila directamente en el microcontrolador, se convierte en código de bytes y luego se almacena en la memoria. Bueno, el compilador también necesita una cierta cantidad de RAM para funcionar.

El truco consiste en salvar al microcontrolador de una compilación que consume muchos recursos. Puede compilar archivos en una computadora grande y cargar un código de bytes ya preparado al microcontrolador. Para hacer esto, necesita descargar el firmware de micropython y compilar utilidad mpy-cross.

No escribí un Makefile, pero revisé y compilé manualmente todos los archivos necesarios (incluidas las bibliotecas), algo como esto

mpy-cross water_counter.py

Solo queda completar los archivos con la extensión .mpy, recordando eliminar primero los archivos .py correspondientes del sistema de archivos del dispositivo.

Todo el desarrollo lo hice en el programa (IDE?) ESPlorer. Le permite cargar scripts al microcontrolador y ejecutarlos inmediatamente. En mi caso, toda la lógica y la creación de todos los objetos se encuentran en el archivo water_counter.py (.mpy). Pero para que todo esto comience automáticamente, también debe haber un archivo llamado main.py al inicio. Además, debe ser exactamente .py y no .mpy precompilado. Aquí están sus contenidos triviales.

import water_counter

Empezamos, todo funciona. Pero la memoria libre es amenazadoramente pequeña: alrededor de 1 kb. Todavía tengo planes de ampliar la funcionalidad del dispositivo y, obviamente, este kilobyte no será suficiente para mí. Pero resultó que hay una salida.

El punto es este. Aunque los archivos se compilan en código de bytes y residen en el sistema de archivos interno, en realidad se cargan en la RAM y se ejecutan desde allí de todos modos. Pero resulta que Micropython puede ejecutar código de bytes directamente desde la memoria flash, pero para ello es necesario integrarlo directamente en el firmware. No es difícil, aunque en mi netbook me llevó bastante tiempo (solo que allí tenía Linux).

El algoritmo es el siguiente:

  • Descargar e instalar SDK abierto ESP. Esta cosa reúne un compilador y bibliotecas para programas para el ESP8266. Montado según las instrucciones de la página principal del proyecto (elegí la configuración STANDALONE=sí)
  • descargar tipos de micropython
  • Coloque las bibliotecas necesarias en ports/esp8266/modules dentro del árbol de micropython
  • Recopilamos el firmware según las instrucciones del archivo. puertos/esp8266/README.md
  • Subir el firmware al microcontrolador (yo lo hago en Windows usando los programas ESP8266Flasher o el esptool de Python)

Todo, ahora 'importar ssd1306' generará el código directamente desde el firmware y no se gastará RAM para esto. Con este truco, cargué solo el código de la biblioteca en el firmware, mientras que el código del programa principal se ejecuta desde el sistema de archivos. Esto facilita la modificación del programa sin tener que volver a compilar el firmware. Por el momento tengo libres unos 8.5kb de RAM. Esto nos permitirá implementar muchas funciones útiles diferentes en el futuro. Bueno, si no hay suficiente memoria, puede insertar el programa principal en el firmware.

¿Y qué hacer con eso ahora?

Ok, se suelda el trozo de hierro, se escribe el firmware, se imprime la caja, se pega el dispositivo a la pared y la luz parpadea alegremente. Pero hasta ahora todo esto es una caja negra (literal y figurativamente) y todavía tiene poco sentido. Es hora de hacer algo con los mensajes MQTT que se envían al servidor.

Mi "hogar inteligente" sigue girando sistema mayordomo. El módulo MQTT viene listo para usar o se instala fácilmente desde el mercado de complementos; no recuerdo de dónde vino. MQTT no es algo autosuficiente: necesita lo que se llama. corredor: un servidor que acepta, clasifica y reenvía mensajes a clientes MQTT. Yo uso mosquitto, que (como majordomo) se ejecuta en la misma netbook.

Después de que el dispositivo envíe un mensaje al menos una vez, el valor aparecerá inmediatamente en la lista.

Conectamos el contador de agua a la casa inteligente

Estos valores ahora se pueden asociar con objetos del sistema, se pueden usar en scripts de automatización y someterse a diversos análisis; todo esto está fuera del alcance de este artículo. ¿Quién está interesado en el sistema mayordomo? Se lo puedo recomendar. canal Electrónica en lente - un amigo también está construyendo una casa inteligente y habla de manera inteligible sobre cómo configurar el sistema.

Sólo les mostraré un par de gráficos. Este es un gráfico simple de valores por día.

Conectamos el contador de agua a la casa inteligente
Se puede observar que casi nadie usaba el agua por la noche. Un par de veces alguien fue al baño y parece que el filtro de ósmosis inversa aspira un par de litros por noche. Por la mañana el consumo aumenta notablemente. Normalmente uso agua de una caldera, pero luego quise darme un baño y cambié temporalmente al agua caliente de la ciudad; esto también se ve claramente en el gráfico inferior.

De este gráfico aprendí que para ir al baño se necesitan entre 6 y 7 litros de agua, para ducharse se necesitan entre 20 y 30 litros, para lavar los platos se necesitan unos 20 litros y para bañarse se necesitan 160 litros. Mi familia consume entre 500 y 600 litros al día.

Para aquellos que tengan especial curiosidad, pueden consultar los registros de cada valor individual.

Conectamos el contador de agua a la casa inteligente

De aquí aprendí que cuando el grifo está abierto, el agua fluye a una velocidad de aproximadamente 1 litro en 5 segundos.

Pero en esta forma las estadísticas probablemente no sean muy cómodas de observar. Majordomo también tiene la posibilidad de ver gráficos de consumo por día, semana y mes. Aquí, por ejemplo, hay un gráfico de consumo en barras.

Conectamos el contador de agua a la casa inteligente

Hasta ahora solo tengo una semana de datos. En un mes, este gráfico será más revelador: cada día corresponderá a una columna separada. La imagen se estropea un poco por los ajustes de los valores que introduzco manualmente (la columna más grande). Y todavía no está claro si configuré incorrectamente los primeros valores, casi un cubo menos, o si se trata de un error en el firmware y no se tuvieron en cuenta todos los litros. Necesito mas tiempo.

Por encima de los gráficos, todavía es necesario conjurar, blanquear y pintar. Quizás también construya un gráfico del consumo de memoria con fines de depuración; de repente, algo se filtra allí. Quizás de alguna manera pueda mostrar los períodos en los que no existía Internet. Mientras todo esto gira al nivel de la idea.

Conclusión

Hoy mi apartamento se ha vuelto un poco más inteligente. Con un dispositivo tan pequeño, me resultará más cómodo controlar el consumo de agua en la casa. Si antes me indignaba “nuevamente se consumía mucha agua en un mes”, ahora puedo encontrar el origen de este consumo.

A alguien le parecerá extraño mirar las lecturas en la pantalla si está a un metro del propio medidor. Pero en un futuro no muy lejano, planeo mudarme a otro apartamento, donde habrá varios elevadores de agua y los medidores probablemente estarán ubicados en el rellano. Por tanto, un dispositivo de lectura remota sería muy útil.

También planeo ampliar la funcionalidad del dispositivo. Ya estoy mirando válvulas motorizadas. Ahora, para cambiar la caldera de agua de la ciudad, necesito abrir 3 grifos en un nicho de difícil acceso. Sería mucho más conveniente hacerlo con un botón con la indicación correspondiente. Bueno, por supuesto, vale la pena implementar protección contra fugas.

En el artículo, hablé de mi versión del dispositivo basado en ESP8266. En mi opinión, obtuve una versión muy interesante del firmware de Micropython que usa rutinas: simple y bonita. Intenté describir los muchos matices y obstáculos que encontré durante la campaña. Quizás describí todo con demasiado detalle, para mí personalmente, como lector, es más fácil desperdiciar el exceso que pensar en lo que no se dijo más tarde.

Como siempre, estoy abierto a críticas constructivas.

Código fuente
Esquema y tablero.
modelo de caso

Fuente: habr.com

Compre alojamiento confiable para sitios con protección DDoS, servidores VPS VDS 🔥 Compra alojamiento web fiable con protección DDoS, servidores VPS VDS | ProHoster